Initial commit
[fillmore] / src / marina / clip.vala
1 /* Copyright 2009 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 enum MediaType {
12     AUDIO,
13     VIDEO
14 }
15
16 public class Gap {
17     public int64 start;
18     public int64 end;
19
20     public Gap(int64 start, int64 end) {
21         this.start = start;
22         this.end = end;
23     }
24
25     public bool is_empty() {
26         return start >= end;
27     }
28
29     public Gap intersect(Gap g) {
30         return new Gap(int64.max(start, g.start), int64.min(end, g.end));
31     }
32 }
33
34 public class ClipFile : Object {
35     public string filename;
36     int64 _length;
37     public int64 length {
38         public get {
39             if (!online) {
40                 warning("retrieving length while clip offline");
41             }
42             return _length;
43         }
44         
45         public set {
46             _length = value;
47         }
48     }
49
50     bool online;
51
52     public Gst.Caps video_caps;    // or null if no video
53     public Gst.Caps audio_caps;    // or null if no audio
54     public Gdk.Pixbuf thumbnail = null;
55
56     public signal void updated();
57
58     public ClipFile(string filename, int64 length = 0) {
59         this.filename = filename;
60         this.length = length;
61         online = false;
62     }
63
64     public bool is_online() {
65         return online;
66     }
67
68     public void set_online(bool o) {
69         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "set_online");
70         online = o;
71         updated();
72     }
73
74     public void set_thumbnail(Gdk.Pixbuf b) {
75         // TODO: Investigate this
76         // 56x56 - 62x62 icon size does not work for some reason when
77         // we display the thumbnail while dragging the clip.
78
79         thumbnail = b.scale_simple(64, 44, Gdk.InterpType.BILINEAR);
80     }
81
82     public bool has_caps_structure(MediaType m) {
83         if (m == MediaType.AUDIO) {
84             if (audio_caps == null || audio_caps.get_size() < 1)
85                 return false;
86         } else if (m == MediaType.VIDEO) {
87             if (video_caps == null || video_caps.get_size() < 1)
88                 return false;
89         }
90         return true;
91     }
92
93     public bool is_of_type(MediaType t) {
94         if (t == MediaType.VIDEO)
95             return video_caps != null;
96         return audio_caps != null;
97     }
98
99     bool get_caps_structure(MediaType m, out Gst.Structure s) {
100         if (!has_caps_structure(m))
101             return false;
102         if (m == MediaType.AUDIO) {
103             s = audio_caps.get_structure(0);
104         } else if (m == MediaType.VIDEO) {
105             s = video_caps.get_structure(0);
106         }
107         return true;
108     }
109
110     public bool get_frame_rate(out Fraction rate) {
111         Gst.Structure structure;
112         if (!get_caps_structure(MediaType.VIDEO, out structure))
113             return false;
114         return structure.get_fraction("framerate", out rate.numerator, out rate.denominator);
115     }
116
117     public bool get_dimensions(out int w, out int h) {
118         Gst.Structure s;
119
120         if (!get_caps_structure(MediaType.VIDEO, out s))
121             return false;
122
123         return s.get_int("width", out w) && s.get_int("height", out h);
124     }
125
126     public bool get_sample_rate(out int rate) {
127         Gst.Structure s;
128         if (!get_caps_structure(MediaType.AUDIO, out s))
129             return false;
130
131         return s.get_int("rate", out rate);
132     }
133
134     public bool get_video_format(out uint32 fourcc) {
135         Gst.Structure s;
136
137         if (!get_caps_structure(MediaType.VIDEO, out s))
138             return false;
139
140         return s.get_fourcc("format", out fourcc);
141     }
142
143     public bool get_num_channels(out int channels) {
144         Gst.Structure s;
145         if (!get_caps_structure(MediaType.AUDIO, out s)) {
146             return false;
147         }
148
149         return s.get_int("channels", out channels);
150     }
151
152     public bool get_num_channels_string(out string s) {
153         int i;
154         if (!get_num_channels(out i))
155             return false;
156
157         if (i == 1)
158             s = "Mono";
159         else if (i == 2)
160             s = "Stereo";
161         else if ((i % 2) == 0)
162             s = "Surround %d.1".printf(i - 1);
163         else
164             s = "%d".printf(i);
165         return true;
166     }
167 }
168
169 public abstract class Fetcher : Object {
170     protected Gst.Element filesrc;
171     protected Gst.Element decodebin;
172     protected Gst.Pipeline pipeline;
173
174     public ClipFile clipfile;
175     public string error_string;
176
177     protected abstract void on_pad_added(Gst.Pad pad);
178     protected abstract void on_state_change(Gst.Bus bus, Gst.Message message);
179
180     public signal void ready(Fetcher fetcher);
181
182     protected void do_error(string error) {
183         error_string = error;
184         pipeline.set_state(Gst.State.NULL);
185     }
186
187     protected void on_warning(Gst.Bus bus, Gst.Message message) {
188         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_warning");
189         Error error;
190         string text;
191         message.parse_warning(out error, out text);
192         warning("%s", text);
193     }
194
195     protected void on_error(Gst.Bus bus, Gst.Message message) {
196         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error");
197         Error error;
198         string text;
199         message.parse_error(out error, out text);
200         do_error(text);
201     }
202 }
203
204 public class ClipFetcher : Fetcher {  
205     public signal void clipfile_online(bool online);
206
207     public ClipFetcher(string filename) throws Error {
208         clipfile = new ClipFile(filename);
209
210         clipfile_online.connect(clipfile.set_online);
211
212         filesrc = make_element("filesrc");
213         filesrc.set("location", filename);
214
215         decodebin = (Gst.Bin) make_element("decodebin");
216         pipeline = new Gst.Pipeline("pipeline");
217         pipeline.set_auto_flush_bus(false);
218         if (pipeline == null)
219             error("can't construct pipeline");
220         pipeline.add_many(filesrc, decodebin);
221
222         if (!filesrc.link(decodebin))
223             error("can't link filesrc");
224         decodebin.pad_added.connect(on_pad_added);
225
226         Gst.Bus bus = pipeline.get_bus();
227
228         bus.add_signal_watch();
229         bus.message["state-changed"] += on_state_change;
230         bus.message["error"] += on_error;
231         bus.message["warning"] += on_warning;
232
233         error_string = null;
234         pipeline.set_state(Gst.State.PLAYING);
235     }
236
237     public string get_filename() { return clipfile.filename; }
238
239     protected override void on_pad_added(Gst.Pad pad) {
240         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added");
241         Gst.Pad fake_pad;
242         Gst.Element fake_sink;
243         try {
244             if (pad.caps.to_string().has_prefix("video")) {
245                 fake_sink = make_element("fakesink");
246                 pipeline.add(fake_sink);
247                 fake_pad = fake_sink.get_static_pad("sink");
248
249                 if (!fake_sink.sync_state_with_parent()) {
250                     error("could not sync state with parent");
251                 }
252             } else {
253                 fake_sink = make_element("fakesink");
254                 pipeline.add(fake_sink);
255                 fake_pad = fake_sink.get_static_pad("sink");
256
257                 if (!fake_sink.sync_state_with_parent()) {
258                     error("could not sync state with parent");
259                 }
260             }
261             pad.link(fake_pad);
262         }
263         catch (Error e) {
264         }
265     }
266
267     Gst.Pad? get_pad(string prefix) {
268         foreach(Gst.Pad pad in decodebin.pads) {
269             string caps = pad.caps.to_string();
270             if (caps.has_prefix(prefix)) {
271                 return pad;
272             }
273         }
274         return null;
275     }
276
277     protected override void on_state_change(Gst.Bus bus, Gst.Message message) {
278         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_change");
279         if (message.src != pipeline)
280             return;
281
282         Gst.State old_state;
283         Gst.State new_state;
284         Gst.State pending;
285
286         message.parse_state_changed(out old_state, out new_state, out pending);
287         if (new_state == old_state) 
288             return;
289
290         if (new_state == Gst.State.PLAYING) {
291             Gst.Pad? pad = get_pad("video");
292             if (pad != null) {
293                 clipfile.video_caps = pad.caps;
294             }
295
296             pad = get_pad("audio");
297             if (pad != null) {
298                 clipfile.audio_caps = pad.caps;
299             }
300
301             Gst.Format format = Gst.Format.TIME;
302             int64 length;
303             if (!pipeline.query_duration(ref format, out length) ||
304                     format != Gst.Format.TIME) {
305                 do_error("Can't fetch length");
306                 return;
307             }
308             clipfile.length = length;
309
310             clipfile_online(true);
311             pipeline.set_state(Gst.State.NULL);
312         } else if (new_state == Gst.State.NULL) {
313             ready(this);
314         }
315     }
316 }
317
318 public class ThumbnailFetcher : Fetcher {
319     ThumbnailSink thumbnail_sink;
320     Gst.Element colorspace;
321     int64 seek_position;
322     bool done_seek;
323     bool have_thumbnail;
324
325     public ThumbnailFetcher(ClipFile f, int64 time) throws Error {
326         clipfile = f;
327         seek_position = time;
328
329         SingleDecodeBin single_bin = new SingleDecodeBin (
330                                         Gst.Caps.from_string ("video/x-raw-rgb; video/x-raw-yuv"),
331                                         "singledecoder", f.filename);
332
333         pipeline = new Gst.Pipeline("pipeline");
334         pipeline.set_auto_flush_bus(false);
335
336         thumbnail_sink = new ThumbnailSink();
337         thumbnail_sink.have_thumbnail.connect(on_have_thumbnail);
338
339         colorspace = make_element("ffmpegcolorspace");
340
341         pipeline.add_many(single_bin, thumbnail_sink, colorspace);
342
343         single_bin.pad_added.connect(on_pad_added);
344
345         colorspace.link(thumbnail_sink);
346
347         Gst.Bus bus = pipeline.get_bus();
348
349         bus.add_signal_watch();
350         bus.message["state-changed"] += on_state_change;
351         bus.message["error"] += on_error;
352         bus.message["warning"] += on_warning;
353
354         have_thumbnail = false;
355         done_seek = false;
356         pipeline.set_state(Gst.State.PAUSED);
357     }
358
359     void on_have_thumbnail(Gdk.Pixbuf buf) {
360         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_have_thumbnail");
361         if (done_seek) {
362             have_thumbnail = true;
363             clipfile.set_thumbnail(buf);
364         }
365     }
366
367     protected override void on_pad_added(Gst.Pad pad) {
368         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added");
369         Gst.Caps c = pad.get_caps();
370
371         if (c.to_string().has_prefix("video")) {
372             pad.link(colorspace.get_static_pad("sink"));
373         }
374     }
375
376     protected override void on_state_change(Gst.Bus bus, Gst.Message message) {
377         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_change");
378         if (message.src != pipeline)
379             return;
380
381         Gst.State new_state;
382         Gst.State old_state;
383         Gst.State pending_state;
384
385         message.parse_state_changed (out old_state, out new_state, out pending_state);
386         if (new_state == old_state &&
387             new_state != Gst.State.PAUSED)
388             return;
389
390         if (new_state == Gst.State.PAUSED) {
391             if (!done_seek) {
392                 done_seek = true;
393                 pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_position);
394             } else {
395                 if (have_thumbnail)
396                     pipeline.set_state(Gst.State.NULL);
397             }
398         } else if (new_state == Gst.State.NULL) {
399             ready(this);
400         }
401     }
402 }
403
404 public class Clip : Object {
405     public ClipFile clipfile;
406     public MediaType type;
407     // TODO: If a clip is being recorded, we don't want to set duration in the MediaClip file.
408     // Address when handling multiple track recording.  This is an ugly hack.
409     public bool is_recording;
410     public string name;
411     int64 _start;
412     public int64 start { 
413         get {
414             return _start;
415         }
416
417         set {
418             _start = value;
419             if (connected) {
420                 start_changed(_start);
421             }
422             moved(this);
423         }
424     }
425
426     int64 _media_start;
427     public int64 media_start { 
428         get {
429             return _media_start;
430         }
431     }
432
433     int64 _duration;
434     public int64 duration {
435         get {
436             return _duration;
437         }
438
439         set {
440             if (value < 0) {
441                 // saturating the duration
442                 value = 0;
443             }
444
445             if (!is_recording) {
446                 if (value + _media_start > clipfile.length) {
447                     // saturating the duration
448                     value = clipfile.length - media_start;
449                 }
450             }
451
452             _duration = value;
453             if (connected) {
454                 duration_changed(_duration);
455             }
456             moved(this);
457         }
458     }
459
460     bool connected;
461
462     public int64 end {
463         get { return start + duration; }
464     }
465
466     public signal void moved(Clip clip);
467     public signal void updated(Clip clip);
468     public signal void media_start_changed(int64 media_start);
469     public signal void duration_changed(int64 duration);
470     public signal void start_changed(int64 start);
471     public signal void removed(Clip clip);
472
473     public Clip(ClipFile clipfile, MediaType t, string name,
474                 int64 start, int64 media_start, int64 duration, bool is_recording) {
475         this.is_recording = is_recording;
476         this.clipfile = clipfile;
477         this.type = t;
478         this.name = name;
479         this.connected = clipfile.is_online();
480         this.set_media_start_duration(media_start, duration);
481         this.start = start;
482         clipfile.updated.connect(on_clipfile_updated);
483     }
484
485     public void gnonlin_connect() { connected = true; }
486     public void gnonlin_disconnect() { connected = false; }
487
488     void on_clipfile_updated(ClipFile f) {
489         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clipfile_updated");
490         if (f.is_online()) {
491             if (!connected) {
492                 connected = true;
493                 // TODO: Assigning to oneself has the side-effect of firing signals. 
494                 // fire signals directly.  Make certain that loading a file still works
495                 // properly in this case.
496                 set_media_start_duration(media_start, duration);
497
498                 start = start;
499             }
500         } else {
501             if (connected) {
502                 connected = false;
503             }
504         }
505         updated(this);
506     }
507
508     public bool overlap_pos(int64 start, int64 length) {
509         return start < this.start + this.duration &&
510                 this.start < start + length;
511     }
512
513     public int64 snap(Clip other, int64 pad) {
514         if (time_in_range(start, other.start, pad)) {
515             return other.start;
516         } else if (time_in_range(start, other.end, pad)) {
517             return other.end;
518         } else if (time_in_range(end, other.start, pad)) {
519             return other.start - duration;
520         } else if (time_in_range(end, other.end, pad)) {
521             return other.end - duration;
522         }
523         return start;
524     }
525
526     public bool snap_coord(out int64 s, int64 span) {
527         if (time_in_range(s, start, span)) {
528             s = start;
529             return true;
530         } else if (time_in_range(s, end, span)) {
531             s = end;
532             return true;
533         }
534         return false;
535     }
536
537     public Clip copy() {
538         return new Clip(clipfile, type, name, start, media_start, duration, false);
539     }
540
541     public bool is_trimmed() {
542         if (!clipfile.is_online()) 
543             return false;
544         return duration != clipfile.length;
545     }
546
547     public void trim(int64 delta, Gdk.WindowEdge edge) {
548         switch (edge) {
549             case Gdk.WindowEdge.WEST:
550                 if (media_start + delta < 0) {
551                     delta = -media_start;
552                 }
553
554                 if (duration - delta < 0) {
555                     delta = duration;
556                 }
557
558                 start += delta;
559                 set_media_start_duration(media_start + delta, duration - delta);
560                 break;
561             case Gdk.WindowEdge.EAST:
562                 duration += delta;
563                 break;
564         }
565     }
566
567     public void set_media_start_duration(int64 media_start, int64 duration) {
568         if (media_start < 0) {
569             media_start = 0;
570         }
571
572         if (duration < 0) {
573             duration = 0;
574         }
575
576         if (clipfile.is_online() && media_start + duration > clipfile.length) {
577             // We are saturating the value
578             media_start = clipfile.length - duration;
579         }
580
581         _media_start = media_start;
582         _duration = duration;
583
584         if (connected) {
585             media_start_changed(_media_start);
586             duration_changed(_duration);
587         }
588
589         moved(this);
590     }
591
592     public void save(FileStream f, int id) {
593         f.printf(
594             "      <clip id=\"%d\" name=\"%s\" start=\"%" + int64.FORMAT + "\" " +
595                     "media-start=\"%" + int64.FORMAT + "\" duration=\"%" + int64.FORMAT + "\"/>\n",
596                     id, name, start, media_start, duration);
597     }
598 }
599
600 public class FetcherCompletion {
601     public FetcherCompletion() {
602     }
603
604     public virtual void complete(Fetcher fetcher) {
605     }
606 }
607 }