Bladeren bron

Working encoding

Billy Barrow 2 jaren geleden
bovenliggende
commit
b11c14c9cb

+ 30 - 11
src/Video/Encoder.vala

@@ -7,7 +7,7 @@ namespace Publicate.Video {
         public string output_dir { get; private set; }
         public EncodingProfile profile { get; private set; }
 
-        public signal void progress_changed(double fraction, bool completed);
+        public signal void progress_changed(double fraction_change, bool completed);
 
         public Encoder(File input, string output_dir, EncodingProfile profile) {
             input_file = input.get_path();
@@ -17,21 +17,40 @@ namespace Publicate.Video {
 
         public void encode() throws Error {
             progress_changed(0.0, false);
-            var output_file = output_dir + "/" + profile.output_name();
-            var first_pass = new Subprocess.newv(profile.get_first_pass_command(input_file, output_file), GLib.SubprocessFlags.NONE);
-            first_pass.wait();
-            if(first_pass.get_exit_status() != 0) {
-                throw new Error.literal(Quark.from_string("first-pass-failed"), 19, "Failed to analyse video");
+            var output_file = get_output_file().get_path();
+            var first_command = profile.get_first_pass_command(input_file, output_file);
+            if(first_command.length != 0) {
+                var first_pass = new Subprocess.newv(first_command, GLib.SubprocessFlags.NONE);
+                first_pass.wait();
+                if(first_pass.get_exit_status() != 0) {
+                    throw new Error.literal(Quark.from_string("first-pass-failed"), 19, "Failed to analyse video");
+                }
             }
 
             progress_changed(0.5, false);
-            var second_pass = new Subprocess.newv(profile.get_second_pass_command(input_file, output_file), GLib.SubprocessFlags.NONE);
-            second_pass.wait();
-            if(second_pass.get_exit_status() != 0) {
-                throw new Error.literal(Quark.from_string("second-pass-failed"), 19, "Failed to encode video");
+            var second_command = profile.get_second_pass_command(input_file, output_file);
+            if(second_command.length != 0) {
+                var second_pass = new Subprocess.newv(second_command, GLib.SubprocessFlags.NONE);
+                second_pass.wait();
+                if(second_pass.get_exit_status() != 0) {
+                    throw new Error.literal(Quark.from_string("second-pass-failed"), 19, "Failed to encode video");
+                }
             }
 
-            progress_changed(1.0, true);
+            progress_changed(0.5, true);
+        }
+
+        public File get_output_file() {
+            return File.new_for_path(output_dir + "/" + profile.output_name());
+        }
+
+        public Ppub.VideoDescription get_video_description() {
+            return new Ppub.VideoDescription() {
+                file_name = profile.output_name(),
+                codecs = profile.codec_names,
+                size = profile.size,
+                version_label = profile.version_label()
+            };
         }
 
 

+ 5 - 0
src/Video/EncodingProfile.vala

@@ -5,6 +5,7 @@ namespace Publicate.Video {
 
         public string size { get; protected set; }
         public string codec_names { get; protected set; }
+        public double fps { get; protected set; }
 
         protected double get_low_framerate(double input_framerate) {
             if(input_framerate >= 50) {
@@ -17,8 +18,12 @@ namespace Publicate.Video {
         public abstract string[] get_second_pass_command(string input_path, string output_path);
 
         public abstract string output_name();
+        public abstract string version_label();
+        
         public abstract bool suitable_for(VideoInfo info);
 
+        public abstract void setup_for(VideoInfo info);
+
     }
 
 }

+ 81 - 0
src/Video/TheoraProfiles.vala

@@ -0,0 +1,81 @@
+
+namespace Publicate.Video {
+
+    public static Invercargill.Enumerable<EncodingProfile> construct_theora_profiles() {
+        return Invercargill.ate(new EncodingProfile[] {
+            new TheoraVideo360pProfile (),
+            new TheoraVideo240pProfile (),
+        });
+    }
+
+    public class TheoraVideo360pProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-s", size,
+                "-c:v", "theora",
+                "-c:a", "libvorbis",
+                "-b:v", "300k",
+                "-y",
+                output_path
+            };
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return new string[0];
+        }
+        public override string output_name () {
+            return @"360p.ogv";
+        }
+        public override string version_label (){
+            return @"360p Theora";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 360;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "theora, vorbis";
+            var width = (int)(info.ratio_frac * 360);
+            size = @"$(width)x360";
+            fps = get_low_framerate (info.frame_rate);
+        }
+    }
+
+    public class TheoraVideo240pProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-s", size,
+                "-c:v", "theora",
+                "-c:a", "libvorbis",
+                "-b:v", "150k",
+                "-y",
+                output_path
+            };
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return new string[0];
+        }
+        public override string output_name () {
+            return @"240p.ogv";
+        }
+        public override string version_label (){
+            return @"240p Theora";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 240;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "theora, vorbis";
+            var width = (int) (info.ratio_frac * 240);
+            size = @"$(width)x240";
+            fps = get_low_framerate (info.frame_rate);
+        }
+    }
+
+}

+ 49 - 7
src/Video/VideoProcessor.vala

@@ -13,6 +13,11 @@ namespace Publicate.Video {
         public VideoProcessor() {
 
             var hbox = new Box(Orientation.HORIZONTAL, 8);
+            hbox.margin_bottom = 18;
+            hbox.margin_top = 18;
+            hbox.margin_start = 18;
+            hbox.margin_end = 18;
+
             var vbox = new Box(Orientation.VERTICAL, 8);
 
             hbox.append (vbox);
@@ -22,7 +27,7 @@ namespace Publicate.Video {
             progress_bar = new ProgressBar ();
             cancel_button = new Button.from_icon_name ("process-stop-symbolic");
 
-            hbox.append(cancel_button);
+            //  hbox.append(cancel_button);
             vbox.append (label);
             vbox.append(progress_bar);
 
@@ -38,21 +43,58 @@ namespace Publicate.Video {
             var video_info = new VideoInfo(video_file);
             yield video_info.read_info();
 
-            var profiles = new Invercargill.Sequence<EncodingProfile>();
+            var profiles = construct_vp9_profiles().concat(construct_theora_profiles());
             var use_profiles = profiles.where(p => p.suitable_for(video_info));
 
-            var encoders = use_profiles.select<Encoder>(p => new Encoder(video_file, "/tmp/", p));
-            encoders.parallel_iterate(e => e.encode());
+            use_profiles.iterate(p => p.setup_for(video_info));
+            var encoders = use_profiles.select<Encoder>(p => new Encoder(video_file, "/tmp/", p)).to_sequence();
+            foreach (var encoder in encoders) {
+                encoder.progress_changed.connect((frac, done) => {
+                    var amount = frac;
+                    amount = amount / (double)encoders.count();
+                    Idle.add(() => { progress_bar.fraction += amount; return false; });
+
+                });
+            }
+
+            progress_bar.fraction = 0.0;
+            label.label = "Encoding Video…";
+            yield encode_video(encoders);
+
+            progress_bar.fraction = 1;
+            label.label = "Writing PPUB…";
+
+            var manifest = new Ppub.VideoManifest();
+            manifest.streams = encoders.select<Ppub.VideoDescription>(e => e.get_video_description()).to_sequence();
+            manifest.duration = video_info.duration;
+            var ratio_parts = video_info.aspect_ratio.split(":");
+            manifest.ratio = new double[] { double.parse(ratio_parts[0]), double.parse(ratio_parts[1]) };
+
+            return new VideoProcessResult(manifest, encoders.select<File>(e => e.get_output_file()).to_sequence());
+        }
+
+        private async void encode_video(Invercargill.Enumerable<Encoder> encoders) {
+            SourceFunc callback = encode_video.callback;
+        
+            ThreadFunc<bool> run = () => {
+                encoders.parallel_iterate(e => e.encode());
+                Idle.add((owned) callback);
+                return true;
+            };
+            new Thread<bool>("encoder thread", run);
+            yield;
+        }
 
-            return null;
+        public void complete() {
+            close();
         }
 
     }
 
     public class VideoProcessResult {
 
-        Ppub.VideoManifest manifest { get; private set; }
-        Invercargill.Enumerable<File> video_files { get; private set; }
+        public Ppub.VideoManifest manifest { get; private set; }
+        public Invercargill.Enumerable<File> video_files { get; private set; }
 
         public VideoProcessResult(Ppub.VideoManifest manifest, Invercargill.Enumerable<File> videos) {
             this.manifest = manifest;

+ 213 - 0
src/Video/Vp9Profiles.vala

@@ -0,0 +1,213 @@
+
+namespace Publicate.Video {
+
+    public static Invercargill.Enumerable<EncodingProfile> construct_vp9_profiles() {
+        return Invercargill.ate(new EncodingProfile[] {
+            new Vp9Video1080pHighFramerateProfile (),
+            new Vp9Video1080pLowFramerateProfile (),
+            new Vp9Video720pProfile(),
+            new Vp9Video480pProfile(),
+        });
+    }
+
+    public class Vp9Video1080pHighFramerateProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 1);
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 2);
+        }
+        public override string output_name () {
+            return @"1080p_$(fps)fps.webm";
+        }
+        public override string version_label (){
+            return @"1080p$(fps) VP9";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 1080 && get_low_framerate (info.frame_rate) != info.frame_rate;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "vp9, opus";
+            var width = (int) (info.ratio_frac * 1080);
+            size = @"$(width)x1080";
+            fps = info.frame_rate;
+        }
+
+        private string[] get_command(string input_path, string output_path, int pass) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-vf", @"scale=$(size)",
+                "-b:v", "3000k",
+                "-minrate", "1500k",
+                "-maxrate", "2610k",
+                "-tile-columns", "2",
+                "-g", "240",
+                "-threads", "8",
+                "-quality", "good",
+                "-crf", "31",
+                "-c:v", "libvpx-vp9",
+                "-c:a", "libopus",
+                "-pass", pass.to_string(),
+                "-speed", "4",
+                "-passlogfile", @"$(output_path).logfile",
+                "-y",
+                output_path
+            };
+        }
+
+    }
+
+    public class Vp9Video1080pLowFramerateProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 1);
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 2);
+        }
+        public override string output_name () {
+            return @"1080p.webm";
+        }
+        public override string version_label (){
+            return @"1080p VP9";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 1080;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "vp9, opus";
+            var width = (int) (info.ratio_frac * 1080);
+            size = @"$(width)x1080";
+            fps = get_low_framerate (info.frame_rate);
+        }
+
+        private string[] get_command(string input_path, string output_path, int pass) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-vf", @"scale=$(size)",
+                "-b:v", "1800k",
+                "-minrate", "900k",
+                "-maxrate", "2610k",
+                "-tile-columns", "2",
+                "-g", "240",
+                "-threads", "8",
+                "-quality", "good",
+                "-crf", "31",
+                "-c:v", "libvpx-vp9",
+                "-c:a", "libopus",
+                "-pass", pass.to_string(),
+                "-speed", "4",
+                "-passlogfile", @"$(output_path).logfile",
+                "-y",
+                output_path
+            };
+        }
+
+    }
+
+    public class Vp9Video720pProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 1);
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 2);
+        }
+        public override string output_name () {
+            return @"720p.webm";
+        }
+        public override string version_label (){
+            return @"720p VP9";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 720;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "vp9, opus";
+            var width = (int) (info.ratio_frac * 720);
+            size = @"$(width)x720";
+            fps = get_low_framerate (info.frame_rate);
+        }
+
+        private string[] get_command(string input_path, string output_path, int pass) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-vf", @"scale=$(size)",
+                "-b:v", "1024k",
+                "-minrate", "512k",
+                "-maxrate", "1485k",
+                "-tile-columns", "2",
+                "-g", "240",
+                "-threads", "8",
+                "-quality", "good",
+                "-crf", "32",
+                "-c:v", "libvpx-vp9",
+                "-c:a", "libopus",
+                "-pass", pass.to_string(),
+                "-speed", "4",
+                "-passlogfile", @"$(output_path).logfile",
+                "-y",
+                output_path
+            };
+        }
+
+    }
+
+    public class Vp9Video480pProfile : EncodingProfile {
+        
+        public override string[] get_first_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 1);
+        }
+        public override string[] get_second_pass_command (string input_path, string output_path) {
+            return get_command (input_path, output_path, 2);
+        }
+        public override string output_name () {
+            return @"480p.webm";
+        }
+        public override string version_label (){
+            return @"480p VP9";
+        }
+        public override bool suitable_for (VideoInfo info) {
+            return info.height >= 480;
+        }
+        public override void setup_for (VideoInfo info) {
+            codec_names = "vp9, opus";
+            var width = (int) (info.ratio_frac * 480);
+            size = @"$(width)x480";
+            fps = get_low_framerate (info.frame_rate);
+        }
+
+        private string[] get_command(string input_path, string output_path, int pass) {
+            return new string[] {
+                "ffmpeg",
+                "-i", input_path,
+                "-r", fps.to_string(),
+                "-vf", @"scale=$(size)",
+                "-b:v", "750k",
+                "-minrate", "375k",
+                "-maxrate", "1088k",
+                "-tile-columns", "2",
+                "-g", "240",
+                "-threads", "8",
+                "-quality", "good",
+                "-crf", "33",
+                "-c:v", "libvpx-vp9",
+                "-c:a", "libopus",
+                "-pass", pass.to_string(),
+                "-speed", "4",
+                "-passlogfile", @"$(output_path).logfile",
+                "-y",
+                output_path
+            };
+        }
+
+    }
+
+}

+ 36 - 7
src/Wizards/Video.vala

@@ -92,22 +92,51 @@ namespace Publicate.Wizards {
             
             print(metadata.to_string());
 
+            var processor = new Video.VideoProcessor();
+            var result = yield processor.process_video(video_file.selected_file.get_path(), window);
+
             var builder = new Ppub.Builder();
             builder.add_metadata(metadata);
 
-            var article_data = @"# $(title.text)\n\n".to_utf8();
-            var article_stream = new MemoryInputStream.from_data((uint8[])article_data);
-            var compression_info = new Ppub.CompressionInfo(article_stream);
-            article_stream.seek(0, SeekType.SET);
-
-            builder.add_asset("article.md", "text/markdown", article_stream, Invercargill.empty<string>(), compression_info);
+            result.manifest.author = metadata.author;
+            result.manifest.title = metadata.title;
+            result.manifest.description_file_name = "video_description.md";
+
+            var manifest_data = result.manifest.to_string();
+            print(manifest_data);
+            var manifest_stream = new MemoryInputStream.from_data((uint8[])manifest_data.to_utf8());
+            var compression_info = new Ppub.CompressionInfo(manifest_stream);
+            manifest_stream.seek(0, SeekType.SET);
+
+            builder.add_asset("video.ppvm", "application/x-ppvm", manifest_stream, Invercargill.empty<string>(), compression_info);
+
+            var description_data = @"**$(title.text)**".to_utf8();
+            var description_stream = new MemoryInputStream.from_data((uint8[])description_data);
+            compression_info = new Ppub.CompressionInfo(description_stream);
+            description_stream.seek(0, SeekType.SET);
+
+            builder.add_asset("video_description.md", "text/markdown", description_stream, Invercargill.empty<string>(), compression_info);
+
+            foreach (var result_file in result.video_files) {
+                var stream = yield result_file.read_async(Priority.DEFAULT, null);
+                var compression = new Ppub.CompressionInfo(stream, false);
+                stream.seek(0, SeekType.SET, null);
+                var sample = new uint8[2048];
+                size_t sample_size;
+                yield stream.read_all_async(sample, Priority.DEFAULT, null, out sample_size);
+                stream.seek(0, SeekType.SET, null);
+    
+                var mimetype = Ppub.guess_mimetype(result_file.get_basename(), sample);
+                builder.add_asset(result_file.get_basename(), mimetype, stream, Invercargill.empty<string>(), compression);
+            }
 
             var output_stream = file.replace(null, false, FileCreateFlags.REPLACE_DESTINATION);
             builder.write(output_stream);
 
             output_stream.close();
-
+            
             open(file);
+            processor.complete();
         }
 
         public void reset() {