Ver código fonte

Start of video support

Billy Barrow 2 anos atrás
pai
commit
3bddbe1e4f

+ 1 - 1
README.md

@@ -13,10 +13,10 @@ It is still a work in progress, and also depends on some of my other home grown
 - Updating PPUB metadata.
 - Editing markdown files within a PPUB
 - Editing plain text files within a PPUB
+- Spellchecking
 
 ## What is still to come
 
-- Spellchecking
 - Ability to generate PPIX files (PPub IndeX) once LibPpub has the functionality.
 - Templated creation of files that don't already exist in a PPUB.
 - Editing PPVM (PPub Video Manifest) files.

+ 7 - 0
src/StartupMenu.vala

@@ -10,6 +10,7 @@ namespace Publicate {
         private Stack stack;
 
         private Wizards.StandardWizard standard_wizard;
+        private Wizards.VideoWizard video_wizard;
         private ViewerWindow window;
 
         public StartupMenu(ViewerWindow win) {
@@ -54,6 +55,8 @@ namespace Publicate {
             var new_video_action_row = new ActionRow();
             new_video_action_row.title = "New Video Publication";
             new_video_action_row.subtitle = "Create a video publication from an existing video.";
+            new_video_action_row.activatable = true;
+            new_video_action_row.activated.connect(() => stack.visible_child = video_wizard);
             action_list.append (new_video_action_row);
 
             var edit_action_row = new ActionRow();
@@ -68,6 +71,10 @@ namespace Publicate {
             standard_wizard.open.connect(open_file);
             stack.add_child(standard_wizard);
 
+            video_wizard = new Wizards.VideoWizard(window);
+            video_wizard.cancelled.connect(wizard_cancelled);
+            video_wizard.open.connect(open_file);
+            stack.add_child(video_wizard);
         }
 
 

+ 40 - 0
src/Video/Encoder.vala

@@ -0,0 +1,40 @@
+
+namespace Publicate.Video {
+
+    public class Encoder {
+
+        public string input_file { get; private set; }
+        public string output_dir { get; private set; }
+        public EncodingProfile profile { get; private set; }
+
+        public signal void progress_changed(double fraction, bool completed);
+
+        public Encoder(File input, string output_dir, EncodingProfile profile) {
+            input_file = input.get_path();
+            this.output_dir = output_dir;
+            this.profile = profile;
+        }
+
+        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");
+            }
+
+            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");
+            }
+
+            progress_changed(1.0, true);
+        }
+
+
+    }
+
+}

+ 24 - 0
src/Video/EncodingProfile.vala

@@ -0,0 +1,24 @@
+
+namespace Publicate.Video {
+
+    public abstract class EncodingProfile {
+
+        public string size { get; protected set; }
+        public string codec_names { get; protected set; }
+
+        protected double get_low_framerate(double input_framerate) {
+            if(input_framerate >= 50) {
+                return input_framerate / 2;
+            }
+            return input_framerate;
+        }
+
+        public abstract string[] get_first_pass_command(string input_path, string output_path);
+        public abstract string[] get_second_pass_command(string input_path, string output_path);
+
+        public abstract string output_name();
+        public abstract bool suitable_for(VideoInfo info);
+
+    }
+
+}

+ 0 - 0
src/Video/TheoraProfiles.vala


+ 63 - 0
src/Video/VideoInfo.vala

@@ -0,0 +1,63 @@
+
+namespace Publicate.Video {
+
+    public class VideoInfo {
+
+        public string path { get; private set; }
+        public int height { get; private set; }
+        public int width { get; private set; }
+        public string aspect_ratio { get; private set; }
+        public double ratio_frac { get; private set; }
+        public double frame_rate { get; private set; }
+        public double duration { get; private set; }
+
+        public VideoInfo(File video) {
+            path = video.get_path();
+        }
+
+        public async void read_info() throws Error {
+
+            var probe = new Subprocess.newv(new string[] {
+                    "ffprobe",
+                    "-v", "quiet", 
+                    "-print_format", "json",
+                    "-show_format",
+                    "-show_streams",
+                    path
+                }, GLib.SubprocessFlags.STDOUT_PIPE);
+
+            yield probe.wait_async();
+            
+            if(probe.get_exit_status() != 0) {
+                throw new Error.literal(Quark.from_string("probe-failed"), 18, "Failed to probe video");
+            }
+
+            var parser = new Json.Parser();
+            yield parser.load_from_stream_async(probe.get_stdout_pipe());
+
+            var root = parser.get_root().get_object();
+            var streams = root.get_array_member("streams");
+
+            streams.foreach_element((a, i, n) => {
+                var stream = n.get_object();
+                var type = stream.get_string_member("codec_type");
+                if(type == "video") {
+                    width = (int)stream.get_int_member("width");
+                    height = (int)stream.get_int_member("height");
+                    aspect_ratio = stream.get_string_member("display_aspect_ratio");
+                    ratio_frac = ((double)width / (double)height);
+
+                    var fps_string = stream.get_string_member("r_frame_rate");
+                    var fps_parts = fps_string.split("/");
+                    frame_rate = double.parse(fps_parts[0]) / double.parse(fps_parts[1]);
+                }
+            });
+
+            duration = double.parse(root.get_object_member("format").get_string_member("duration"));
+
+        }
+
+
+    }
+
+}

+ 63 - 0
src/Video/VideoProcessor.vala

@@ -0,0 +1,63 @@
+using Gtk;
+using Adw;
+
+namespace Publicate.Video {
+
+
+    public class VideoProcessor : Adw.Window {
+
+        private ProgressBar progress_bar;
+        private Label label;
+        private Button cancel_button;    
+
+        public VideoProcessor() {
+
+            var hbox = new Box(Orientation.HORIZONTAL, 8);
+            var vbox = new Box(Orientation.VERTICAL, 8);
+
+            hbox.append (vbox);
+            content = hbox;
+
+            label = new Label("Reading Video…");
+            progress_bar = new ProgressBar ();
+            cancel_button = new Button.from_icon_name ("process-stop-symbolic");
+
+            hbox.append(cancel_button);
+            vbox.append (label);
+            vbox.append(progress_bar);
+
+        }
+
+        public async VideoProcessResult? process_video(string path, ViewerWindow window) {
+
+            this.modal = true;
+            this.transient_for = window;
+            present();
+
+            var video_file = File.new_for_path(path);
+            var video_info = new VideoInfo(video_file);
+            yield video_info.read_info();
+
+            var profiles = new Invercargill.Sequence<EncodingProfile>();
+            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());
+
+            return null;
+        }
+
+    }
+
+    public class VideoProcessResult {
+
+        Ppub.VideoManifest manifest { get; private set; }
+        Invercargill.Enumerable<File> video_files { get; private set; }
+
+        public VideoProcessResult(Ppub.VideoManifest manifest, Invercargill.Enumerable<File> videos) {
+            this.manifest = manifest;
+            video_files = videos;
+        }
+    }
+
+}

+ 0 - 0
src/Video/Vp9Profiles.vala


+ 74 - 1
src/Wizards/Video.vala

@@ -5,6 +5,7 @@ namespace Publicate.Wizards {
 
     public class VideoWizard : Box, Wizard {
 
+        private VideoChooserRow video_file;
         private EntryRow title;
         private EntryRow author;
         private EntryRow author_email;
@@ -26,6 +27,10 @@ namespace Publicate.Wizards {
             group.title = "Publication Details";
             group.description = "These can be changed later";
 
+            video_file = new VideoChooserRow(window);
+            video_file.title = "Video";
+            group.add(video_file);
+
             title = new EntryRow ();
             title.title = "Title";
             group.add(title);
@@ -48,7 +53,7 @@ namespace Publicate.Wizards {
             var next_button = new Button.with_label ("Create");
             next_button.hexpand = true;
             next_button.add_css_class ("suggested-action");
-            next_button.clicked.connect (() => create_ppub());
+            next_button.clicked.connect (() => create_ppub.begin());
 
             var button_box = new Box(Orientation.HORIZONTAL, 0);
             button_box.add_css_class ("linked");
@@ -109,8 +114,76 @@ namespace Publicate.Wizards {
             title.text = "";
             author.text = "";
             author_email.text = "";
+            video_file.reset();
         }
 
     }
 
+    private class VideoChooserRow : ActionRow {
+
+        private ViewerWindow toplevel;
+
+        public File? selected_file {get; set;}
+
+        public VideoChooserRow(ViewerWindow window) {
+            toplevel = window;
+            reset();
+
+            var button = new Button.from_icon_name("document-open-symbolic");
+            button.clicked.connect(() => open_video.begin());
+            button.margin_bottom = 8;
+            button.margin_top = 8;
+            button.add_css_class("flat");
+            add_suffix(button);
+
+            activatable_widget = button;
+            activatable = true;
+        }
+
+        public void reset() {
+            selected_file = null;
+            subtitle = "Not selected";
+        }
+
+        public async void open_video() throws Error {
+
+            var dialog = new FileDialog();
+            var filters = new GLib.ListStore(Type.OBJECT);
+            var video_filter = new FileFilter();
+            var all_filter = new FileFilter();
+            filters.append(video_filter);
+            filters.append(all_filter);
+            video_filter.add_pattern("*.mp4");
+            video_filter.add_pattern("*.m4v");
+            video_filter.add_pattern("*.mov");
+            video_filter.add_pattern("*.avi");
+            video_filter.add_pattern("*.mkv");
+            video_filter.add_pattern("*.webm");
+            video_filter.add_pattern("*.ogg");
+            video_filter.add_pattern("*.ogv");
+            video_filter.name = "Video Files";
+            all_filter.add_pattern("*");
+            all_filter.name = "All Files";
+
+            dialog.filters = filters;
+            dialog.set_initial_file(selected_file);
+            var file = yield dialog.open(toplevel, null);
+
+            if(file == null) {
+                return;
+            }
+            
+            selected_file = file;
+            subtitle = selected_file.get_basename();
+
+            var info = new Video.VideoInfo(file);
+            yield info.read_info();
+
+            print(@"Video Info:\nWidth = $(info.width);\nHeight = $(info.height);\nFPS = $(info.frame_rate);\nRatio = $(info.ratio_frac);\n Ratio (display) = $(info.aspect_ratio);\n Duration = $(info.duration);\n");
+
+        }
+
+
+    }
+
 }

+ 7 - 0
src/meson.build

@@ -19,10 +19,17 @@ sources += files('Editors/UnsupportedEditor.vala')
 sources += files('Wizards/Wizard.vala')
 sources += files('Wizards/Standard.vala')
 sources += files('Wizards/Video.vala')
+sources += files('Video/VideoProcessor.vala')
+sources += files('Video/VideoInfo.vala')
+sources += files('Video/EncodingProfile.vala')
+sources += files('Video/TheoraProfiles.vala')
+sources += files('Video/Vp9Profiles.vala')
+sources += files('Video/Encoder.vala')
 
 dependencies = [
     dependency('glib-2.0'),
     dependency('gobject-2.0'),
+    dependency('json-glib-1.0'),
     dependency('gio-2.0'),
     dependency('gee-0.8'),
     dependency('libadwaita-1'),