Initial commit
[fillmore] / src / marina / import.vala
diff --git a/src/marina/import.vala b/src/marina/import.vala
new file mode 100644 (file)
index 0000000..7c002cb
--- /dev/null
@@ -0,0 +1,469 @@
+/* Copyright 2009-2010 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution. 
+ */
+
+using Logging;
+
+namespace Model {
+
+public class ClipImporter : MultiFileProgressInterface, Object {
+    enum ImportState {
+        FETCHING,
+        IMPORTING,
+        CANCELLED
+    }
+
+    string import_directory;
+
+    ImportState import_state;
+    bool import_done;
+    bool all_done;
+    ClipFetcher our_fetcher;
+
+    Gst.Pad video_pad;
+    Gst.Pad audio_pad;
+    
+    Gst.Pipeline pipeline = null;
+    Gst.Element filesink;
+    Gst.Element video_convert;
+    Gst.Element audio_convert;
+    Gst.Element mux;
+
+    Gst.Bin video_decoder;
+    Gst.Bin audio_decoder;
+
+    int current_file_importing = 0;
+
+    int64 current_time;
+    int64 total_time;
+    int64 previous_time;
+
+    Gee.ArrayList<string> filenames = new Gee.ArrayList<string>();
+    Gee.ArrayList<ClipFetcher> queued_fetchers = new Gee.ArrayList<ClipFetcher>();
+    Gee.ArrayList<string> queued_filenames = new Gee.ArrayList<string>();
+    Gee.ArrayList<string> no_import_formats = new Gee.ArrayList<string>();
+
+    public signal void clip_complete(ClipFile f);
+    public signal void importing_started(int num_clips);
+    public signal void error_occurred(string error);
+
+    public ClipImporter() {
+        import_directory = GLib.Environment.get_home_dir();
+        import_directory += "/.lombard_fillmore_import/";
+
+        GLib.DirUtils.create(import_directory, 0777);
+
+        no_import_formats.add("YUY2");
+        no_import_formats.add("Y41B");
+
+        import_state = ImportState.FETCHING;
+    }
+
+    public void add_filename(string filename) {
+        filenames.add(filename);
+    }
+
+    bool on_timer_callback() {
+        int64 time;
+        Gst.Format format = Gst.Format.TIME;
+
+        if (all_done) 
+            return false;
+
+        if (pipeline.query_position(ref format, out time) &&
+            format == Gst.Format.TIME) {
+            if (time > previous_time)
+                current_time += time - previous_time;
+            previous_time = time;
+            if (current_time >= total_time) {
+                fraction_updated(1.0);
+                return false;
+            } else
+                fraction_updated(current_time / (double)total_time);    
+        }
+        return true;
+    }
+
+    void start_import() {
+        import_state = ImportState.IMPORTING;
+        current_file_importing = 0;
+        importing_started(queued_fetchers.size);
+        Timeout.add(50, on_timer_callback);
+    }
+
+    void cancel() {
+        all_done = true;
+        import_state = ImportState.CANCELLED;
+        if (pipeline != null) {
+            pipeline.set_state(Gst.State.NULL);
+        }
+    }
+
+    public void start() throws Error {
+        process_curr_file();
+    }
+
+    void process_curr_file() throws Error {
+        if (import_state == ImportState.FETCHING) {
+            if (current_file_importing == filenames.size) {
+                if (queued_fetchers.size == 0)
+                    done();
+                else
+                    start_import();
+            } else {
+                emit(this, Facility.IMPORT, Level.VERBOSE, 
+                    "fetching %s".printf(filenames[current_file_importing]));
+                our_fetcher = new Model.ClipFetcher(filenames[current_file_importing]);
+                our_fetcher.ready.connect(on_fetcher_ready);
+            }
+        }
+
+        if (import_state == ImportState.IMPORTING) {
+            if (current_file_importing == queued_fetchers.size) {
+                fraction_updated(1.0);
+                all_done = true;
+            } else
+                do_import(queued_fetchers[current_file_importing]);
+        }
+    }
+
+    // TODO: Rework this
+    void complete() {
+        all_done = true;
+    }
+
+    void do_import_complete() throws Error{
+        if (import_state == ImportState.IMPORTING) {
+            our_fetcher.clipfile.filename = append_extension(
+                                                   queued_filenames[current_file_importing], "mov");
+            clip_complete(our_fetcher.clipfile);
+        } else
+            total_time += our_fetcher.clipfile.length;
+
+        current_file_importing++;
+
+        if (current_file_importing <= filenames.size)
+            process_curr_file();
+        else
+            warning ("do_import_complete: current_file_importing out of bounds! %d %d".printf(
+                                                          current_file_importing, filenames.size));
+    }
+
+    bool need_to_import(Fetcher f) {
+        //for now, use the clip as is
+        return false;
+        /*
+        if (f.clipfile.is_of_type(MediaType.VIDEO)) {
+            uint32 format;
+            if (f.clipfile.get_video_format(out format)) {
+                foreach (string s in no_import_formats) {
+                    if (format == *(uint32*)s) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+        */
+    }
+
+    void on_fetcher_ready(Fetcher f) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_fetcher_ready");
+        try {
+            if (f.error_string != null) {
+                error_occurred(f.error_string);
+                do_import_complete();
+                return;
+            }
+
+            if (need_to_import(f)) {
+                string checksum;
+                if (md5_checksum_on_file(f.clipfile.filename, out checksum)) {
+                    string base_filename = import_directory + isolate_filename(f.clipfile.filename);
+
+                    int index = 0;
+                    string new_filename = base_filename;
+                    while (true) {
+                        string existing_checksum;
+                        if (get_file_md5_checksum(new_filename, out existing_checksum)) {
+                            if (checksum == existing_checksum) {
+                                // Re-fetch this clip to get the correct caps
+                                filenames[current_file_importing] =
+                                                            append_extension(new_filename, "mov");
+                                current_file_importing--;
+                                total_time -= f.clipfile.length;
+                                break;
+                            }
+                            index++;
+                            new_filename = base_filename + index.to_string();
+                        } else {
+                            // Truly need to import
+                            save_file_md5_checksum(new_filename, checksum);
+                            queued_filenames.add(new_filename);
+                            queued_fetchers.add(f as ClipFetcher);
+                            break;
+                        }
+                    }
+                } else
+                    error("Cannot get md5 checksum for file %s!", f.clipfile.filename);
+            } else {
+                clip_complete(f.clipfile);
+            }
+            do_import_complete();
+        } catch (Error e) {
+            error_occurred(e.message);
+        }
+    }
+
+    void do_import(ClipFetcher f) throws Error {
+        file_updated(f.clipfile.filename, current_file_importing);
+        previous_time = 0;
+
+        our_fetcher = f;
+        import_done = false;
+        pipeline = new Gst.Pipeline("pipeline");
+        pipeline.set_auto_flush_bus(false);
+
+        Gst.Bus bus = pipeline.get_bus();
+        bus.add_signal_watch();
+
+        bus.message["state-changed"] += on_state_changed;
+        bus.message["eos"] += on_eos;
+        bus.message["error"] += on_error;
+        bus.message["warning"] += on_warning;
+
+        mux = make_element("qtmux");
+
+        filesink = make_element("filesink");
+        filesink.set("location", append_extension(queued_filenames[current_file_importing], "mov"));
+
+        pipeline.add_many(mux, filesink);
+
+        if (f.clipfile.is_of_type(MediaType.VIDEO)) {
+            video_convert = make_element("ffmpegcolorspace");
+            pipeline.add(video_convert);
+
+            video_decoder = new SingleDecodeBin(Gst.Caps.from_string(
+                                                               "video/x-raw-yuv"),
+                                                               "videodecodebin", 
+                                                               f.clipfile.filename);
+            video_decoder.pad_added.connect(on_pad_added);
+
+            pipeline.add(video_decoder);
+
+            if (!video_convert.link(mux))
+                error("do_import: Cannot link video converter to mux!");
+        }
+        if (f.clipfile.is_of_type(MediaType.AUDIO)) {
+            audio_convert = make_element("audioconvert");
+            pipeline.add(audio_convert);
+
+            // setup for importing h.264 and other int flavors.  change to audio/x-raw-float
+            // if you need to import ogg and other float flavors.  see bug 2055
+            audio_decoder = new SingleDecodeBin(
+                Gst.Caps.from_string("audio/x-raw-int"),
+                                                    "audiodecodebin", f.clipfile.filename);
+            audio_decoder.pad_added.connect(on_pad_added);
+
+            pipeline.add(audio_decoder);
+
+            if (!audio_convert.link(mux))
+                error("do_import: Cannot link audio convert to mux!");
+        }
+
+        if (!mux.link(filesink))
+            error("do_import: Cannot link mux to filesink!");
+
+        emit(this, Facility.IMPORT, Level.VERBOSE, 
+            "Starting import to %s...".printf(queued_filenames[current_file_importing]));
+        pipeline.set_state(Gst.State.PLAYING);
+    }
+
+    void on_pad_added(Gst.Pad p) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added");
+
+        string str = p.caps.to_string();
+        Gst.Pad sink = null;
+
+        if (str.has_prefix("video")) {
+            video_pad = p;
+            sink = video_convert.get_compatible_pad(p, p.caps);
+        } else if (str.has_prefix("audio")) {
+            audio_pad = p;
+            sink = audio_convert.get_compatible_pad(p, p.caps);
+        } else {
+            //error_occurred here gives a segfault
+            warning("Unrecognized prefix %s".printf(str));
+            return;
+        }
+
+        if (p.link(sink) != Gst.PadLinkReturn.OK) {
+            error("Cannot link pad in importer!");
+        }
+    }
+
+    void on_error(Gst.Bus bus, Gst.Message message) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error");
+        Error e;
+        string text;
+        message.parse_error(out e, out text);
+        warning("%s\n", text);
+        error_occurred(text);
+    }
+
+    void on_warning(Gst.Bus bus, Gst.Message message) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_warning");
+        Error e;
+        string text;
+        message.parse_warning(out e, out text);
+        warning("%s", text);
+    }
+
+    void on_state_changed(Gst.Bus b, Gst.Message m) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_changed");
+        if (m.src != pipeline) 
+            return;
+
+        Gst.State old_state;
+        Gst.State new_state;
+        Gst.State pending;
+
+        m.parse_state_changed (out old_state, out new_state, out pending);
+
+        if (old_state == new_state) 
+            return;
+
+        emit(this, Facility.IMPORT, Level.VERBOSE,
+            "Import State in %s".printf(new_state.to_string()));
+        if (new_state == Gst.State.PAUSED) {
+            if (!import_done) {
+                if (video_pad != null) {
+                    our_fetcher.clipfile.video_caps = video_pad.caps;
+                }
+                if (audio_pad != null) {
+                    our_fetcher.clipfile.audio_caps = audio_pad.caps;
+                }
+                emit(this, Facility.IMPORT, Level.VERBOSE,
+                    "Got clipfile info for: %s".printf(our_fetcher.clipfile.filename));
+            }
+        } else if (new_state == Gst.State.NULL) {
+            if (import_state == ImportState.CANCELLED) {
+                GLib.FileUtils.remove(append_extension(queued_filenames[current_file_importing], 
+                                                                                            "mov"));
+                GLib.FileUtils.remove(append_extension(queued_filenames[current_file_importing], 
+                                                                                            "md5"));
+            } else {
+                if (import_done) 
+                    try {
+                        do_import_complete();
+                    } catch (Error e) {
+                        error_occurred(e.message);
+                    }
+            }
+        }
+    }
+
+    void on_eos() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_eos");
+        import_done = true;
+        pipeline.set_state(Gst.State.NULL);
+    }
+}
+
+public class LibraryImporter : Object {
+    protected Project project;
+    public ClipImporter importer;
+
+    public signal void started(ClipImporter i, int num);
+    
+    public LibraryImporter(Project p) {
+        project = p;
+        
+        importer = new ClipImporter();
+        importer.clip_complete.connect(on_clip_complete);
+        importer.error_occurred.connect(on_error_occurred);
+        importer.importing_started.connect(on_importer_started);
+    }
+
+    void on_importer_started(ClipImporter i, int num) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_importer_started");
+        started(i, num);
+    }
+
+    void on_error_occurred(string error) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error_occurred");
+        emit(this, Facility.DEVELOPER_WARNINGS, Level.INFO, error);
+        project.error_occurred("Error importing", "An error occurred importing this file.");
+    }
+
+    protected virtual void append_existing_clipfile(ClipFile f) {
+
+    }
+
+    protected virtual void on_clip_complete(ClipFile f) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_complete");
+        ClipFile cf = project.find_clipfile(f.filename);
+        if (cf == null) {
+            project.add_clipfile(f);
+        }
+    }
+
+    public void add_file(string filename) throws Error {
+        ClipFile cf = project.find_clipfile(filename);
+
+        if (cf != null)
+            append_existing_clipfile(cf);
+        else
+            importer.add_filename(filename);
+    }
+
+    public void start() throws Error {
+        importer.start();
+    }
+}
+
+public class TimelineImporter : LibraryImporter {
+    Track track;
+    int64 time_to_add;
+    bool both_tracks;
+
+    public TimelineImporter(Track track, Project p, int64 time_to_add, bool both_tracks) {
+        base(p);
+        this.track = track;
+        this.time_to_add = time_to_add;
+        this.both_tracks = both_tracks;
+    }
+
+    void add_to_both(ClipFile clip_file) {
+        if (both_tracks) {
+            Track other_track;
+            if (track is Model.VideoTrack) {
+                other_track = project.find_audio_track();
+            } else {
+                other_track = project.find_video_track();
+            }
+            if (other_track != null) {
+                project.add(other_track, clip_file, time_to_add);
+            }
+        }
+    }
+
+    protected override void append_existing_clipfile(ClipFile f) {
+        project.undo_manager.start_transaction("Create Clip");
+        project.add(track, f, time_to_add);
+        add_to_both(f);
+        project.undo_manager.end_transaction("Create Clip");
+    }
+
+    protected override void on_clip_complete(ClipFile f) {
+        project.undo_manager.start_transaction("Create Clip");
+        base.on_clip_complete(f);
+        project.add(track, f, time_to_add);
+        add_to_both(f);
+        project.undo_manager.end_transaction("Create Clip");
+    }
+}
+}