Initial commit
[fillmore] / src / marina / import.vala
1 /* Copyright 2009-2010 Yorba Foundation
2  *
3  * This software is licensed under the GNU Lesser General Public License
4  * (version 2.1 or later).  See the COPYING file in this distribution. 
5  */
6
7 using Logging;
8
9 namespace Model {
10
11 public class ClipImporter : MultiFileProgressInterface, Object {
12     enum ImportState {
13         FETCHING,
14         IMPORTING,
15         CANCELLED
16     }
17
18     string import_directory;
19
20     ImportState import_state;
21     bool import_done;
22     bool all_done;
23     ClipFetcher our_fetcher;
24
25     Gst.Pad video_pad;
26     Gst.Pad audio_pad;
27     
28     Gst.Pipeline pipeline = null;
29     Gst.Element filesink;
30     Gst.Element video_convert;
31     Gst.Element audio_convert;
32     Gst.Element mux;
33
34     Gst.Bin video_decoder;
35     Gst.Bin audio_decoder;
36
37     int current_file_importing = 0;
38
39     int64 current_time;
40     int64 total_time;
41     int64 previous_time;
42
43     Gee.ArrayList<string> filenames = new Gee.ArrayList<string>();
44     Gee.ArrayList<ClipFetcher> queued_fetchers = new Gee.ArrayList<ClipFetcher>();
45     Gee.ArrayList<string> queued_filenames = new Gee.ArrayList<string>();
46     Gee.ArrayList<string> no_import_formats = new Gee.ArrayList<string>();
47
48     public signal void clip_complete(ClipFile f);
49     public signal void importing_started(int num_clips);
50     public signal void error_occurred(string error);
51
52     public ClipImporter() {
53         import_directory = GLib.Environment.get_home_dir();
54         import_directory += "/.lombard_fillmore_import/";
55
56         GLib.DirUtils.create(import_directory, 0777);
57
58         no_import_formats.add("YUY2");
59         no_import_formats.add("Y41B");
60
61         import_state = ImportState.FETCHING;
62     }
63
64     public void add_filename(string filename) {
65         filenames.add(filename);
66     }
67
68     bool on_timer_callback() {
69         int64 time;
70         Gst.Format format = Gst.Format.TIME;
71
72         if (all_done) 
73             return false;
74
75         if (pipeline.query_position(ref format, out time) &&
76             format == Gst.Format.TIME) {
77             if (time > previous_time)
78                 current_time += time - previous_time;
79             previous_time = time;
80             if (current_time >= total_time) {
81                 fraction_updated(1.0);
82                 return false;
83             } else
84                 fraction_updated(current_time / (double)total_time);    
85         }
86         return true;
87     }
88
89     void start_import() {
90         import_state = ImportState.IMPORTING;
91         current_file_importing = 0;
92         importing_started(queued_fetchers.size);
93         Timeout.add(50, on_timer_callback);
94     }
95
96     void cancel() {
97         all_done = true;
98         import_state = ImportState.CANCELLED;
99         if (pipeline != null) {
100             pipeline.set_state(Gst.State.NULL);
101         }
102     }
103
104     public void start() throws Error {
105         process_curr_file();
106     }
107
108     void process_curr_file() throws Error {
109         if (import_state == ImportState.FETCHING) {
110             if (current_file_importing == filenames.size) {
111                 if (queued_fetchers.size == 0)
112                     done();
113                 else
114                     start_import();
115             } else {
116                 emit(this, Facility.IMPORT, Level.VERBOSE, 
117                     "fetching %s".printf(filenames[current_file_importing]));
118                 our_fetcher = new Model.ClipFetcher(filenames[current_file_importing]);
119                 our_fetcher.ready.connect(on_fetcher_ready);
120             }
121         }
122
123         if (import_state == ImportState.IMPORTING) {
124             if (current_file_importing == queued_fetchers.size) {
125                 fraction_updated(1.0);
126                 all_done = true;
127             } else
128                 do_import(queued_fetchers[current_file_importing]);
129         }
130     }
131
132     // TODO: Rework this
133     void complete() {
134         all_done = true;
135     }
136
137     void do_import_complete() throws Error{
138         if (import_state == ImportState.IMPORTING) {
139             our_fetcher.clipfile.filename = append_extension(
140                                                    queued_filenames[current_file_importing], "mov");
141             clip_complete(our_fetcher.clipfile);
142         } else
143             total_time += our_fetcher.clipfile.length;
144
145         current_file_importing++;
146
147         if (current_file_importing <= filenames.size)
148             process_curr_file();
149         else
150             warning ("do_import_complete: current_file_importing out of bounds! %d %d".printf(
151                                                           current_file_importing, filenames.size));
152     }
153
154     bool need_to_import(Fetcher f) {
155         //for now, use the clip as is
156         return false;
157         /*
158         if (f.clipfile.is_of_type(MediaType.VIDEO)) {
159             uint32 format;
160             if (f.clipfile.get_video_format(out format)) {
161                 foreach (string s in no_import_formats) {
162                     if (format == *(uint32*)s) {
163                         return false;
164                     }
165                 }
166                 return true;
167             }
168         }
169         return false;
170         */
171     }
172
173     void on_fetcher_ready(Fetcher f) {
174         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_fetcher_ready");
175         try {
176             if (f.error_string != null) {
177                 error_occurred(f.error_string);
178                 do_import_complete();
179                 return;
180             }
181
182             if (need_to_import(f)) {
183                 string checksum;
184                 if (md5_checksum_on_file(f.clipfile.filename, out checksum)) {
185                     string base_filename = import_directory + isolate_filename(f.clipfile.filename);
186
187                     int index = 0;
188                     string new_filename = base_filename;
189                     while (true) {
190                         string existing_checksum;
191                         if (get_file_md5_checksum(new_filename, out existing_checksum)) {
192                             if (checksum == existing_checksum) {
193                                 // Re-fetch this clip to get the correct caps
194                                 filenames[current_file_importing] =
195                                                             append_extension(new_filename, "mov");
196                                 current_file_importing--;
197                                 total_time -= f.clipfile.length;
198                                 break;
199                             }
200                             index++;
201                             new_filename = base_filename + index.to_string();
202                         } else {
203                             // Truly need to import
204                             save_file_md5_checksum(new_filename, checksum);
205                             queued_filenames.add(new_filename);
206                             queued_fetchers.add(f as ClipFetcher);
207                             break;
208                         }
209                     }
210                 } else
211                     error("Cannot get md5 checksum for file %s!", f.clipfile.filename);
212             } else {
213                 clip_complete(f.clipfile);
214             }
215             do_import_complete();
216         } catch (Error e) {
217             error_occurred(e.message);
218         }
219     }
220
221     void do_import(ClipFetcher f) throws Error {
222         file_updated(f.clipfile.filename, current_file_importing);
223         previous_time = 0;
224
225         our_fetcher = f;
226         import_done = false;
227         pipeline = new Gst.Pipeline("pipeline");
228         pipeline.set_auto_flush_bus(false);
229
230         Gst.Bus bus = pipeline.get_bus();
231         bus.add_signal_watch();
232
233         bus.message["state-changed"] += on_state_changed;
234         bus.message["eos"] += on_eos;
235         bus.message["error"] += on_error;
236         bus.message["warning"] += on_warning;
237
238         mux = make_element("qtmux");
239
240         filesink = make_element("filesink");
241         filesink.set("location", append_extension(queued_filenames[current_file_importing], "mov"));
242
243         pipeline.add_many(mux, filesink);
244
245         if (f.clipfile.is_of_type(MediaType.VIDEO)) {
246             video_convert = make_element("ffmpegcolorspace");
247             pipeline.add(video_convert);
248
249             video_decoder = new SingleDecodeBin(Gst.Caps.from_string(
250                                                                "video/x-raw-yuv"),
251                                                                "videodecodebin", 
252                                                                f.clipfile.filename);
253             video_decoder.pad_added.connect(on_pad_added);
254
255             pipeline.add(video_decoder);
256
257             if (!video_convert.link(mux))
258                 error("do_import: Cannot link video converter to mux!");
259         }
260         if (f.clipfile.is_of_type(MediaType.AUDIO)) {
261             audio_convert = make_element("audioconvert");
262             pipeline.add(audio_convert);
263
264             // setup for importing h.264 and other int flavors.  change to audio/x-raw-float
265             // if you need to import ogg and other float flavors.  see bug 2055
266             audio_decoder = new SingleDecodeBin(
267                 Gst.Caps.from_string("audio/x-raw-int"),
268                                                     "audiodecodebin", f.clipfile.filename);
269             audio_decoder.pad_added.connect(on_pad_added);
270
271             pipeline.add(audio_decoder);
272
273             if (!audio_convert.link(mux))
274                 error("do_import: Cannot link audio convert to mux!");
275         }
276
277         if (!mux.link(filesink))
278             error("do_import: Cannot link mux to filesink!");
279
280         emit(this, Facility.IMPORT, Level.VERBOSE, 
281             "Starting import to %s...".printf(queued_filenames[current_file_importing]));
282         pipeline.set_state(Gst.State.PLAYING);
283     }
284
285     void on_pad_added(Gst.Pad p) {
286         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added");
287
288         string str = p.caps.to_string();
289         Gst.Pad sink = null;
290
291         if (str.has_prefix("video")) {
292             video_pad = p;
293             sink = video_convert.get_compatible_pad(p, p.caps);
294         } else if (str.has_prefix("audio")) {
295             audio_pad = p;
296             sink = audio_convert.get_compatible_pad(p, p.caps);
297         } else {
298             //error_occurred here gives a segfault
299             warning("Unrecognized prefix %s".printf(str));
300             return;
301         }
302
303         if (p.link(sink) != Gst.PadLinkReturn.OK) {
304             error("Cannot link pad in importer!");
305         }
306     }
307
308     void on_error(Gst.Bus bus, Gst.Message message) {
309         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error");
310         Error e;
311         string text;
312         message.parse_error(out e, out text);
313         warning("%s\n", text);
314         error_occurred(text);
315     }
316
317     void on_warning(Gst.Bus bus, Gst.Message message) {
318         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_warning");
319         Error e;
320         string text;
321         message.parse_warning(out e, out text);
322         warning("%s", text);
323     }
324
325     void on_state_changed(Gst.Bus b, Gst.Message m) {
326         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_changed");
327         if (m.src != pipeline) 
328             return;
329
330         Gst.State old_state;
331         Gst.State new_state;
332         Gst.State pending;
333
334         m.parse_state_changed (out old_state, out new_state, out pending);
335
336         if (old_state == new_state) 
337             return;
338
339         emit(this, Facility.IMPORT, Level.VERBOSE,
340             "Import State in %s".printf(new_state.to_string()));
341         if (new_state == Gst.State.PAUSED) {
342             if (!import_done) {
343                 if (video_pad != null) {
344                     our_fetcher.clipfile.video_caps = video_pad.caps;
345                 }
346                 if (audio_pad != null) {
347                     our_fetcher.clipfile.audio_caps = audio_pad.caps;
348                 }
349                 emit(this, Facility.IMPORT, Level.VERBOSE,
350                     "Got clipfile info for: %s".printf(our_fetcher.clipfile.filename));
351             }
352         } else if (new_state == Gst.State.NULL) {
353             if (import_state == ImportState.CANCELLED) {
354                 GLib.FileUtils.remove(append_extension(queued_filenames[current_file_importing], 
355                                                                                             "mov"));
356                 GLib.FileUtils.remove(append_extension(queued_filenames[current_file_importing], 
357                                                                                             "md5"));
358             } else {
359                 if (import_done) 
360                     try {
361                         do_import_complete();
362                     } catch (Error e) {
363                         error_occurred(e.message);
364                     }
365             }
366         }
367     }
368
369     void on_eos() {
370         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_eos");
371         import_done = true;
372         pipeline.set_state(Gst.State.NULL);
373     }
374 }
375
376 public class LibraryImporter : Object {
377     protected Project project;
378     public ClipImporter importer;
379
380     public signal void started(ClipImporter i, int num);
381     
382     public LibraryImporter(Project p) {
383         project = p;
384         
385         importer = new ClipImporter();
386         importer.clip_complete.connect(on_clip_complete);
387         importer.error_occurred.connect(on_error_occurred);
388         importer.importing_started.connect(on_importer_started);
389     }
390
391     void on_importer_started(ClipImporter i, int num) {
392         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_importer_started");
393         started(i, num);
394     }
395
396     void on_error_occurred(string error) {
397         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error_occurred");
398         emit(this, Facility.DEVELOPER_WARNINGS, Level.INFO, error);
399         project.error_occurred("Error importing", "An error occurred importing this file.");
400     }
401
402     protected virtual void append_existing_clipfile(ClipFile f) {
403
404     }
405
406     protected virtual void on_clip_complete(ClipFile f) {
407         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_complete");
408         ClipFile cf = project.find_clipfile(f.filename);
409         if (cf == null) {
410             project.add_clipfile(f);
411         }
412     }
413
414     public void add_file(string filename) throws Error {
415         ClipFile cf = project.find_clipfile(filename);
416
417         if (cf != null)
418             append_existing_clipfile(cf);
419         else
420             importer.add_filename(filename);
421     }
422
423     public void start() throws Error {
424         importer.start();
425     }
426 }
427
428 public class TimelineImporter : LibraryImporter {
429     Track track;
430     int64 time_to_add;
431     bool both_tracks;
432
433     public TimelineImporter(Track track, Project p, int64 time_to_add, bool both_tracks) {
434         base(p);
435         this.track = track;
436         this.time_to_add = time_to_add;
437         this.both_tracks = both_tracks;
438     }
439
440     void add_to_both(ClipFile clip_file) {
441         if (both_tracks) {
442             Track other_track;
443             if (track is Model.VideoTrack) {
444                 other_track = project.find_audio_track();
445             } else {
446                 other_track = project.find_video_track();
447             }
448             if (other_track != null) {
449                 project.add(other_track, clip_file, time_to_add);
450             }
451         }
452     }
453
454     protected override void append_existing_clipfile(ClipFile f) {
455         project.undo_manager.start_transaction("Create Clip");
456         project.add(track, f, time_to_add);
457         add_to_both(f);
458         project.undo_manager.end_transaction("Create Clip");
459     }
460
461     protected override void on_clip_complete(ClipFile f) {
462         project.undo_manager.start_transaction("Create Clip");
463         base.on_clip_complete(f);
464         project.add(track, f, time_to_add);
465         add_to_both(f);
466         project.undo_manager.end_transaction("Create Clip");
467     }
468 }
469 }