/* Copyright 2009 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 enum MediaType { AUDIO, VIDEO } public class Gap { public int64 start; public int64 end; public Gap(int64 start, int64 end) { this.start = start; this.end = end; } public bool is_empty() { return start >= end; } public Gap intersect(Gap g) { return new Gap(int64.max(start, g.start), int64.min(end, g.end)); } } public class ClipFile : Object { public string filename; int64 _length; public int64 length { public get { if (!online) { warning("retrieving length while clip offline"); } return _length; } public set { _length = value; } } bool online; public Gst.Caps video_caps; // or null if no video public Gst.Caps audio_caps; // or null if no audio public Gdk.Pixbuf thumbnail = null; public signal void updated(); public ClipFile(string filename, int64 length = 0) { this.filename = filename; this.length = length; online = false; } public bool is_online() { return online; } public void set_online(bool o) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "set_online"); online = o; updated(); } public void set_thumbnail(Gdk.Pixbuf b) { // TODO: Investigate this // 56x56 - 62x62 icon size does not work for some reason when // we display the thumbnail while dragging the clip. thumbnail = b.scale_simple(64, 44, Gdk.InterpType.BILINEAR); } public bool has_caps_structure(MediaType m) { if (m == MediaType.AUDIO) { if (audio_caps == null || audio_caps.get_size() < 1) return false; } else if (m == MediaType.VIDEO) { if (video_caps == null || video_caps.get_size() < 1) return false; } return true; } public bool is_of_type(MediaType t) { if (t == MediaType.VIDEO) return video_caps != null; return audio_caps != null; } bool get_caps_structure(MediaType m, out Gst.Structure s) { if (!has_caps_structure(m)) return false; if (m == MediaType.AUDIO) { s = audio_caps.get_structure(0); } else if (m == MediaType.VIDEO) { s = video_caps.get_structure(0); } return true; } public bool get_frame_rate(out Fraction rate) { Gst.Structure structure; if (!get_caps_structure(MediaType.VIDEO, out structure)) return false; return structure.get_fraction("framerate", out rate.numerator, out rate.denominator); } public bool get_dimensions(out int w, out int h) { Gst.Structure s; if (!get_caps_structure(MediaType.VIDEO, out s)) return false; return s.get_int("width", out w) && s.get_int("height", out h); } public bool get_sample_rate(out int rate) { Gst.Structure s; if (!get_caps_structure(MediaType.AUDIO, out s)) return false; return s.get_int("rate", out rate); } public bool get_video_format(out uint32 fourcc) { Gst.Structure s; if (!get_caps_structure(MediaType.VIDEO, out s)) return false; return s.get_fourcc("format", out fourcc); } public bool get_num_channels(out int channels) { Gst.Structure s; if (!get_caps_structure(MediaType.AUDIO, out s)) { return false; } return s.get_int("channels", out channels); } public bool get_num_channels_string(out string s) { int i; if (!get_num_channels(out i)) return false; if (i == 1) s = "Mono"; else if (i == 2) s = "Stereo"; else if ((i % 2) == 0) s = "Surround %d.1".printf(i - 1); else s = "%d".printf(i); return true; } } public abstract class Fetcher : Object { protected Gst.Element filesrc; protected Gst.Element decodebin; protected Gst.Pipeline pipeline; public ClipFile clipfile; public string error_string; protected abstract void on_pad_added(Gst.Pad pad); protected abstract void on_state_change(Gst.Bus bus, Gst.Message message); public signal void ready(Fetcher fetcher); protected void do_error(string error) { error_string = error; pipeline.set_state(Gst.State.NULL); } protected void on_warning(Gst.Bus bus, Gst.Message message) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_warning"); Error error; string text; message.parse_warning(out error, out text); warning("%s", text); } protected void on_error(Gst.Bus bus, Gst.Message message) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error"); Error error; string text; message.parse_error(out error, out text); do_error(text); } } public class ClipFetcher : Fetcher { public signal void clipfile_online(bool online); public ClipFetcher(string filename) throws Error { clipfile = new ClipFile(filename); clipfile_online.connect(clipfile.set_online); filesrc = make_element("filesrc"); filesrc.set("location", filename); decodebin = (Gst.Bin) make_element("decodebin"); pipeline = new Gst.Pipeline("pipeline"); pipeline.set_auto_flush_bus(false); if (pipeline == null) error("can't construct pipeline"); pipeline.add_many(filesrc, decodebin); if (!filesrc.link(decodebin)) error("can't link filesrc"); decodebin.pad_added.connect(on_pad_added); Gst.Bus bus = pipeline.get_bus(); bus.add_signal_watch(); bus.message["state-changed"] += on_state_change; bus.message["error"] += on_error; bus.message["warning"] += on_warning; error_string = null; pipeline.set_state(Gst.State.PLAYING); } public string get_filename() { return clipfile.filename; } protected override void on_pad_added(Gst.Pad pad) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added"); Gst.Pad fake_pad; Gst.Element fake_sink; try { if (pad.caps.to_string().has_prefix("video")) { fake_sink = make_element("fakesink"); pipeline.add(fake_sink); fake_pad = fake_sink.get_static_pad("sink"); if (!fake_sink.sync_state_with_parent()) { error("could not sync state with parent"); } } else { fake_sink = make_element("fakesink"); pipeline.add(fake_sink); fake_pad = fake_sink.get_static_pad("sink"); if (!fake_sink.sync_state_with_parent()) { error("could not sync state with parent"); } } pad.link(fake_pad); } catch (Error e) { } } Gst.Pad? get_pad(string prefix) { foreach(Gst.Pad pad in decodebin.pads) { string caps = pad.caps.to_string(); if (caps.has_prefix(prefix)) { return pad; } } return null; } protected override void on_state_change(Gst.Bus bus, Gst.Message message) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_change"); if (message.src != pipeline) return; Gst.State old_state; Gst.State new_state; Gst.State pending; message.parse_state_changed(out old_state, out new_state, out pending); if (new_state == old_state) return; if (new_state == Gst.State.PLAYING) { Gst.Pad? pad = get_pad("video"); if (pad != null) { clipfile.video_caps = pad.caps; } pad = get_pad("audio"); if (pad != null) { clipfile.audio_caps = pad.caps; } Gst.Format format = Gst.Format.TIME; int64 length; if (!pipeline.query_duration(ref format, out length) || format != Gst.Format.TIME) { do_error("Can't fetch length"); return; } clipfile.length = length; clipfile_online(true); pipeline.set_state(Gst.State.NULL); } else if (new_state == Gst.State.NULL) { ready(this); } } } public class ThumbnailFetcher : Fetcher { ThumbnailSink thumbnail_sink; Gst.Element colorspace; int64 seek_position; bool done_seek; bool have_thumbnail; public ThumbnailFetcher(ClipFile f, int64 time) throws Error { clipfile = f; seek_position = time; SingleDecodeBin single_bin = new SingleDecodeBin ( Gst.Caps.from_string ("video/x-raw-rgb; video/x-raw-yuv"), "singledecoder", f.filename); pipeline = new Gst.Pipeline("pipeline"); pipeline.set_auto_flush_bus(false); thumbnail_sink = new ThumbnailSink(); thumbnail_sink.have_thumbnail.connect(on_have_thumbnail); colorspace = make_element("ffmpegcolorspace"); pipeline.add_many(single_bin, thumbnail_sink, colorspace); single_bin.pad_added.connect(on_pad_added); colorspace.link(thumbnail_sink); Gst.Bus bus = pipeline.get_bus(); bus.add_signal_watch(); bus.message["state-changed"] += on_state_change; bus.message["error"] += on_error; bus.message["warning"] += on_warning; have_thumbnail = false; done_seek = false; pipeline.set_state(Gst.State.PAUSED); } void on_have_thumbnail(Gdk.Pixbuf buf) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_have_thumbnail"); if (done_seek) { have_thumbnail = true; clipfile.set_thumbnail(buf); } } protected override void on_pad_added(Gst.Pad pad) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_pad_added"); Gst.Caps c = pad.get_caps(); if (c.to_string().has_prefix("video")) { pad.link(colorspace.get_static_pad("sink")); } } protected override void on_state_change(Gst.Bus bus, Gst.Message message) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_state_change"); if (message.src != pipeline) return; Gst.State new_state; Gst.State old_state; Gst.State pending_state; message.parse_state_changed (out old_state, out new_state, out pending_state); if (new_state == old_state && new_state != Gst.State.PAUSED) return; if (new_state == Gst.State.PAUSED) { if (!done_seek) { done_seek = true; pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_position); } else { if (have_thumbnail) pipeline.set_state(Gst.State.NULL); } } else if (new_state == Gst.State.NULL) { ready(this); } } } public class Clip : Object { public ClipFile clipfile; public MediaType type; // TODO: If a clip is being recorded, we don't want to set duration in the MediaClip file. // Address when handling multiple track recording. This is an ugly hack. public bool is_recording; public string name; int64 _start; public int64 start { get { return _start; } set { _start = value; if (connected) { start_changed(_start); } moved(this); } } int64 _media_start; public int64 media_start { get { return _media_start; } } int64 _duration; public int64 duration { get { return _duration; } set { if (value < 0) { // saturating the duration value = 0; } if (!is_recording) { if (value + _media_start > clipfile.length) { // saturating the duration value = clipfile.length - media_start; } } _duration = value; if (connected) { duration_changed(_duration); } moved(this); } } bool connected; public int64 end { get { return start + duration; } } public signal void moved(Clip clip); public signal void updated(Clip clip); public signal void media_start_changed(int64 media_start); public signal void duration_changed(int64 duration); public signal void start_changed(int64 start); public signal void removed(Clip clip); public Clip(ClipFile clipfile, MediaType t, string name, int64 start, int64 media_start, int64 duration, bool is_recording) { this.is_recording = is_recording; this.clipfile = clipfile; this.type = t; this.name = name; this.connected = clipfile.is_online(); this.set_media_start_duration(media_start, duration); this.start = start; clipfile.updated.connect(on_clipfile_updated); } public void gnonlin_connect() { connected = true; } public void gnonlin_disconnect() { connected = false; } void on_clipfile_updated(ClipFile f) { emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clipfile_updated"); if (f.is_online()) { if (!connected) { connected = true; // TODO: Assigning to oneself has the side-effect of firing signals. // fire signals directly. Make certain that loading a file still works // properly in this case. set_media_start_duration(media_start, duration); start = start; } } else { if (connected) { connected = false; } } updated(this); } public bool overlap_pos(int64 start, int64 length) { return start < this.start + this.duration && this.start < start + length; } public int64 snap(Clip other, int64 pad) { if (time_in_range(start, other.start, pad)) { return other.start; } else if (time_in_range(start, other.end, pad)) { return other.end; } else if (time_in_range(end, other.start, pad)) { return other.start - duration; } else if (time_in_range(end, other.end, pad)) { return other.end - duration; } return start; } public bool snap_coord(out int64 s, int64 span) { if (time_in_range(s, start, span)) { s = start; return true; } else if (time_in_range(s, end, span)) { s = end; return true; } return false; } public Clip copy() { return new Clip(clipfile, type, name, start, media_start, duration, false); } public bool is_trimmed() { if (!clipfile.is_online()) return false; return duration != clipfile.length; } public void trim(int64 delta, Gdk.WindowEdge edge) { switch (edge) { case Gdk.WindowEdge.WEST: if (media_start + delta < 0) { delta = -media_start; } if (duration - delta < 0) { delta = duration; } start += delta; set_media_start_duration(media_start + delta, duration - delta); break; case Gdk.WindowEdge.EAST: duration += delta; break; } } public void set_media_start_duration(int64 media_start, int64 duration) { if (media_start < 0) { media_start = 0; } if (duration < 0) { duration = 0; } if (clipfile.is_online() && media_start + duration > clipfile.length) { // We are saturating the value media_start = clipfile.length - duration; } _media_start = media_start; _duration = duration; if (connected) { media_start_changed(_media_start); duration_changed(_duration); } moved(this); } public void save(FileStream f, int id) { f.printf( " \n", id, name, start, media_start, duration); } } public class FetcherCompletion { public FetcherCompletion() { } public virtual void complete(Fetcher fetcher) { } } }