diff --git a/bin/oxtimeline b/bin/oxtimeline index 78c317c..a791148 100755 --- a/bin/oxtimeline +++ b/bin/oxtimeline @@ -13,8 +13,9 @@ root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') if os.path.exists(os.path.join(root, 'oxtimeline')): sys.path.insert(0, root) - +import ox import oxtimeline +from oxtimeline import audio, video if __name__ == '__main__': parser = OptionParser() @@ -22,6 +23,7 @@ if __name__ == '__main__': parser.add_option('-y', '--height', dest='height', help='timeline height, defaults to 64px', default=64, type="int") parser.add_option('-o', '--prefix', dest='prefix', help='prefix for timeline tiles') parser.add_option('-i', '--input', dest='input', help='video input') + parser.add_option('-a', '--audio', action="store_true", dest="audio", default=False) (opts, args) = parser.parse_args() if None in (opts.prefix, opts.input): @@ -29,11 +31,13 @@ if __name__ == '__main__': sys.exit() opts.input = os.path.abspath(opts.input) - timeline = oxgst.Timeline(opts.input) - timeline.extract(opts.prefix, opts.width, opts.height) + info = ox.avinfo(opts.input) + if not info['video'] or opts.audio: + audio.Timeline(opts.input, opts.prefix, opts.width, opts.height) + else: + video.Timeline(opts.input, opts.prefix, opts.width, opts.height) oxtimeline.createTimelineMultiline(opts.prefix) oxtimeline.makeTiles(opts.prefix, 16, 3600) oxtimeline.makeTimelineOverview(opts.prefix, 1920, height=16) oxtimeline.makeTimelineOverview(opts.prefix, 1920, height=64) - diff --git a/oxtimeline/__init__.py b/oxtimeline/__init__.py index 5b7bb77..eec1b1f 100644 --- a/oxtimeline/__init__.py +++ b/oxtimeline/__init__.py @@ -1,6 +1,154 @@ -from timeline import * -import video -import info -from video import Video -from info import Info +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +# GPL 2008-2010 +import gobject +gobject.threads_init() + +from glob import glob +import math +import os +import time + +import pygst +pygst.require("0.10") +import gst +import Image + + +import video +import audio + +def loadTimeline(timeline_prefix, height=64): + files = sorted(glob('%s.%s.*.png' % (timeline_prefix, height))) + f = Image.open(files[0]) + width = f.size[0] + f = Image.open(files[-1]) + duration = f.size[0] + (len(files)-1)*width + timeline = Image.new("RGBA", (duration, height)) + pos = 0 + for f in files: + part = Image.open(f) + timeline.paste(part, (pos, 0, pos + part.size[0], height)) + pos += part.size[0] + return timeline + +def createTimelineMultiline(timeline_prefix, width=600, height=16): + lineWidth = width + timlelineHeight = height + + timeline = loadTimeline(timeline_prefix) + duration = timeline.size[0] + + width = duration/25 #one pixel per second + timeline = timeline.resize((width, timlelineHeight), Image.ANTIALIAS).convert('RGBA') + + lineHeight = timlelineHeight + 2 * 4 + + lines = int(math.ceil(width / lineWidth) + 1) + size = (lineWidth, lineHeight * lines) + + timelineColor = (64, 64, 64) + i = Image.new("RGBA", size) + + #padd end with nothing to fit to grid + t = Image.new("RGBA", (lineWidth * lines, timlelineHeight)) + t.paste(timeline, (0, 0)) + + for currentLine in range(0, lines): + offset = currentLine * lineHeight + 4 + toffset = currentLine * lineWidth + try: + tbox = t.crop((toffset, 0, toffset + lineWidth, timlelineHeight)) + box = ((0, offset , tbox.size[0], offset + tbox.size[1])) + i.paste(tbox, box) + except: + broken = True + width = lineWidth + if currentLine == lines -1: + width = duration - (lines - 1) * lineWidth + box = ((0, offset , width, offset + timlelineHeight)) + i.paste(timelineColor, box) + timeline_file = '%s.overview.png' % (timeline_prefix) + i.save(timeline_file, 'PNG') + + timeline8_file = '%s.overview.8.png' % (timeline_prefix) + if lines < 8: + i.save(timeline8_file, 'PNG') + else: + i.crop((0,0,lineWidth, 8 * lineHeight)).save(timeline8_file, 'PNG') + + +def makeTimelineByFramesPerPixel(timeline_prefix, frames_per_pixel, inpoint=0, outpoint=0, height=16): + pos = 0 + input_scale = 25 + + timeline_file = '%s.pixels.%s.png' % (timeline_prefix, width) + if outpoint > 0: + timeline_file = '%s.pixels.%s.%d-%d.png' % (timeline_prefix, width, inpoint, outpoint) + + timeline = loadTimeline(timeline_prefix) + duration = timeline.size[0] + + + width = duration / frames_per_pixel + + if inpoint<=0: + inpoint = 0 + else: + inpoint = inpoint * input_scale + if outpoint<=0: + outpoint = pos + else: + outpoint = outpoint * input_scale + + timeline = timeline.crop((inpoint, 0, outpoint, timeline.size[1])).resize((width, height), Image.ANTIALIAS) + timeline.save(timeline_file) + +def makeTimelineOverview(timeline_prefix, width, inpoint=0, outpoint=0, duration=-1, height=16): + input_scale = 25 + + timeline_file = '%s.%s.png' % (timeline_prefix, height) + if outpoint > 0: + timeline_file = '%s.overview.%s.%d-%d.png' % (timeline_prefix, height, inpoint, outpoint) + + timeline = loadTimeline(timeline_prefix) + duration = timeline.size[0] + + if inpoint<=0: + inpoint = 0 + else: + inpoint = inpoint * input_scale + if outpoint<=0: + outpoint = duration + else: + outpoint = outpoint * input_scale + + timeline = timeline.crop((inpoint, 0, outpoint, timeline.size[1])).resize((width, height), Image.ANTIALIAS) + timeline.save(timeline_file) + +def makeTiles(timeline_prefix, height=16, width=3600): + files = glob('%s.64.*.png' % timeline_prefix) + fps = 25 + part_step = 60 + output_width = width + width = len(files) * part_step + timeline = Image.new("RGBA", (width, height)) + + pos = 0 + for f in sorted(files): + part = Image.open(f) + part_width = int(part.size[0] / fps) + part = part.resize((part_width, height), Image.ANTIALIAS) + timeline.paste(part, (pos, 0, pos+part_width, height)) + pos += part_width + + timeline = timeline.crop((0, 0, pos, height)) + + pos = 0 + i = 0 + while pos < timeline.size[0]: + end = min(pos+output_width, timeline.size[0]) + timeline.crop((pos, 0, end, timeline.size[1])).save('%s.%s.%04d.png' % (timeline_prefix, timeline.size[1], i)) + pos += output_width + i += 1 diff --git a/oxtimeline/audio.py b/oxtimeline/audio.py new file mode 100644 index 0000000..6f19bd2 --- /dev/null +++ b/oxtimeline/audio.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +# GPL 2008-2010 +from __future__ import division +import Image +import time +import math +from struct import unpack + +import numpy as np +import gobject +import gst + + +class Audio(gst.Pipeline): + _data = [] + position = 0 + + def __init__(self, uri, samplerate=22050, channels=1): + gst.Pipeline.__init__(self) + self.duration = -1 + self.framerate = 25 + self.samplerate = samplerate + self.channels = channels + + self.uri = uri + + self.src = gst.element_factory_make("filesrc") + self.src.props.location = self.uri + self.sbin = gst.element_factory_make("decodebin2") + + self.queue = gst.element_factory_make("queue") + self.rate = gst.element_factory_make("audioresample") + self.conv = gst.element_factory_make("audioconvert") + + self.sink = gst.element_factory_make("fakesink") + self.sink.props.signal_handoffs = True + self.sink.connect('handoff', self._data_callback) + + self.add(self.src, self.sbin, self.conv, self.queue, self.rate, self.sink) + + + self.src.link(self.sbin) + self.sbin.connect('pad-added', self._sbinPadAddedCb) + + self.set_state(gst.STATE_PAUSED) + self.get_state() + self.getDuration() + self.frames = int((float(self.duration) / gst.SECOND) * float(self.framerate)) + + def _sbinPadAddedCb(self, unused_sbin, pad): + caps = pad.get_caps() + if 'audio' in str(caps): + pad.link(self.queue.get_pad("sink")) + self.queue.link(self.conv) + self.conv.link(self.rate) + self.rate.link(self.sink, + gst.Caps("audio/x-raw-int,rate=%s,channels=%s,width=16,depth=16" % + (self.samplerate, self.channels))) + + def _data_callback(self, sink, buff, pad): + timestamp = buff.timestamp + samples = buff.size // 2 + fmt = "<" + str(samples) + "h" + data = unpack(fmt, buff.data) + data = self._data + list(data) + + samples_per_pixel = self.samplerate / self.framerate + while len(data) > samples_per_pixel: + pixel = data[:samples_per_pixel] + pixel = np.asarray(pixel) + data = data[samples_per_pixel:] + + p = np.sum(np.abs(pixel)) / samples_per_pixel + p = p / 256 + height = int((p * self.tile_height) / 256) * 2 + if p: p += 20 + p = (p, p, p, 255) + tile = int(math.floor(float(self.position) / self.input_tile_width)) + tilePos = self.position - (tile * self.input_tile_width) + + crop = (self.tile_height-height) / 2 + + for i in range(crop, self.tile_height-crop): + self.tiles[tile].putpixel((tilePos, i), p) + + self.position += 1 + + self._data = data + + if self.mainloop and timestamp >= self.duration: + self.done() + + def getDuration(self): + if self.duration < 0: + pads = self.sink.sink_pads() + q = gst.query_new_duration(gst.FORMAT_TIME) + for pad in pads: + if pad.get_peer() and pad.get_peer().query(q): + format, self.duration = q.parse_duration() + return self.duration + +class Timeline(Audio): + def __init__(self, uri, prefix, width, height): + Audio.__init__(self, uri) + + bus = self.get_bus() + bus.add_signal_watch() + self.watch_id = bus.connect("message", self.onBusMessage) + + self.mainloop = gobject.MainLoop() + + self.tile_width = width + self.tile_height = height + self.prefix = prefix + self.timeline_fps = 25 + self.input_tile_width = int(math.ceil((float(self.framerate)/self.timeline_fps) * width)) + ntiles = int(math.ceil(float(self.frames)/self.input_tile_width)) + self.tiles = [] + for i in range(ntiles): + tile = Image.new("RGBA", (self.input_tile_width, height), (0, 0, 0, 0)) + self.tiles.append(tile) + + self.set_state(gst.STATE_PLAYING) + self.mainloop.run() + + for i in range(ntiles): + tile = self.tiles[i] + if tile.size[0] != self.tile_width: + tile = tile.resize((self.tile_width, self.tile_height), Image.ANTIALIAS) + if i < (ntiles-1): + frames = self.input_tile_width + else: + frames = self.frames-((ntiles-1)*self.input_tile_width) + tile_width = int(math.ceil(self.timeline_fps*frames)/float(self.framerate)) + if -2 < self.tile_width - tile_width < 2: + tile_width = self.tile_width + tile = tile.crop((0, 0, tile_width, self.tile_height)) + filename = "%s.%s.%04d.png" % (self.prefix, self.tile_height, i) + tile.save(filename) + + def done(self): + self.mainloop.quit() + + def onBusMessage(self, bus, message): + if message.src == self and message.type == gst.MESSAGE_EOS: + self.done() + diff --git a/oxtimeline/imagesink.py b/oxtimeline/imagesink.py index 0a676d7..a07afd3 100644 --- a/oxtimeline/imagesink.py +++ b/oxtimeline/imagesink.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 # GPL 2008 - import gobject import pygst pygst.require("0.10") import gst + import Image + class ImageSink(gst.BaseSink): #def log(self, msg): # print msg diff --git a/oxtimeline/singledecodebin.py b/oxtimeline/singledecodebin.py deleted file mode 100644 index d046f2d..0000000 --- a/oxtimeline/singledecodebin.py +++ /dev/null @@ -1,462 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Mode: Python; -*- -# vi:si:et:sw=4:sts=4:ts=4 -# -# pitivi/elements/singledecodebin.py -# -# Copyright (c) 2005, Edward Hervey -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. - -""" -Single-stream queue-less decodebin -""" - -import gobject -import gst - - -def pad_compatible_stream(pad, stream): - """ - Checks whether the given pad is compatible with the given stream. - - @param pad: The pad - @type pad: C{gst.Pad} - @param stream: The stream to match against. - @type stream: L{MultimediaStream} - @return: Whether the pad is compatible with the given stream - @rtype: C{bool} - """ - if stream == None: - # yes, None is the magical stream that takes everything - return True - # compatible caps - if stream.caps: - return not stream.caps.intersect(pad.get_caps()).is_empty() - raise Exception("Can't figure out compatibility since the stream doesn't have any caps") - -class CachedFactoryList(object): - def __init__(self, factoryFilter=None): - self._factoryFilter = factoryFilter - self._factories = None - self._registry = gst.registry_get_default() - self._registry.connect("feature-added", self._registryFeatureAddedCb) - - def get(self): - if self._factories is None: - self._buildFactories() - - return self._factories - - def _buildFactories(self): - # build the cache - #log.debug("utils", "Getting factories list") - factories = self._registry.get_feature_list(gst.ElementFactory) - if self._factoryFilter is not None: - #log.debug("utils", "filtering") - factories = filter(self._factoryFilter, factories) - - #log.debug("utils", "Sorting by rank") - factories.sort(key=lambda factory: factory.get_rank(), reverse=True) - self._factories = factories - #log.debug("utils", "Cached factories is now %r", self._factories) - - def _registryFeatureAddedCb(self, registry, feature): - # invalidate the cache - #log.warning("utils", "New feature added, invalidating cached factories") - self._factories = None - -def is_raw(caps): - """ returns True if the caps are RAW """ - rep = caps.to_string() - valid = ["video/x-raw", "audio/x-raw", "text/plain", "text/x-pango-markup"] - for val in valid: - if rep.startswith(val): - return True - return False - -def factoryFilter(factory): - if factory.get_rank() < 64 : - return False - - klass = factory.get_klass() - for cat in ("Demuxer", "Decoder", "Parse"): - if cat in klass: - return True - - return False - -_factoryCache = CachedFactoryList(factoryFilter) - -class SingleDecodeBin(gst.Bin): - """ - A variant of decodebin. - - * Only outputs one stream - * Doesn't contain any internal queue - """ - - QUEUE_SIZE = 1 * gst.SECOND - - __gsttemplates__ = ( - gst.PadTemplate ("sinkpadtemplate", - gst.PAD_SINK, - gst.PAD_ALWAYS, - gst.caps_new_any()), - gst.PadTemplate ("srcpadtemplate", - gst.PAD_SRC, - gst.PAD_SOMETIMES, - gst.caps_new_any()) - ) - - def __init__(self, caps=None, uri=None, stream=None, *args, **kwargs): - gst.Bin.__init__(self, *args, **kwargs) - if not caps: - caps = gst.caps_new_any() - self.caps = caps - self.stream = stream - self.typefind = gst.element_factory_make("typefind", "internal-typefind") - self.add(self.typefind) - - self.uri = uri - if self.uri and gst.uri_is_valid(self.uri): - self.urisrc = gst.element_make_from_uri(gst.URI_SRC, uri, "urisrc") - self.log("created urisrc %s / %r" % (self.urisrc.get_name(), - self.urisrc)) - self.add(self.urisrc) - # Set the blocksize to 512kbytes, this will only matter for push-based sources - if hasattr(self.urisrc.props, "blocksize"): - self.urisrc.props.blocksize = 524288 - self.urisrc.link_pads_full("src", self.typefind, "sink", - gst.PAD_LINK_CHECK_NOTHING) - else: - self._sinkpad = gst.GhostPad("sink", self.typefind.get_pad("sink")) - self._sinkpad.set_active(True) - self.add_pad(self._sinkpad) - - self.typefind.connect("have_type", self._typefindHaveTypeCb) - - self._srcpad = None - - self._dynamics = [] - - self._validelements = [] #added elements - - self.debug("stream:%r" % self.stream) - - self.pending_newsegment = False - self.eventProbeId = None - - ## internal methods - - def _controlDynamicElement(self, element): - self.log("element:%s" % element.get_name()) - self._dynamics.append(element) - element.connect("pad-added", self._dynamicPadAddedCb) - element.connect("no-more-pads", self._dynamicNoMorePadsCb) - - def _findCompatibleFactory(self, caps): - """ - Returns a list of factories (sorted by rank) which can take caps as - input. Returns empty list if none are compatible - """ - self.debug("caps:%s" % caps.to_string()) - res = [] - for factory in _factoryCache.get(): - for template in factory.get_static_pad_templates(): - if template.direction == gst.PAD_SINK: - intersect = caps.intersect(template.static_caps.get()) - if not intersect.is_empty(): - res.append(factory) - break - self.debug("returning %r" % res) - return res - - def _closeLink(self, element): - """ - Inspects element and tries to connect something on the srcpads. - If there are dynamic pads, it sets up a signal handler to - continue autoplugging when they become available. - """ - to_connect = [] - dynamic = False - templates = element.get_pad_template_list() - for template in templates: - if not template.direction == gst.PAD_SRC: - continue - if template.presence == gst.PAD_ALWAYS: - pad = element.get_pad(template.name_template) - to_connect.append(pad) - elif template.presence == gst.PAD_SOMETIMES: - pad = element.get_pad(template.name_template) - if pad: - to_connect.append(pad) - else: - dynamic = True - else: - self.log("Template %s is a request pad, ignoring" % pad.name_template) - - if dynamic: - self.debug("%s is a dynamic element" % element.get_name()) - self._controlDynamicElement(element) - - for pad in to_connect: - self._closePadLink(element, pad, pad.get_caps()) - - def _isDemuxer(self, element): - if not 'Demux' in element.get_factory().get_klass(): - return False - - potential_src_pads = 0 - for template in element.get_pad_template_list(): - if template.direction != gst.PAD_SRC: - continue - - if template.presence == gst.PAD_REQUEST or \ - "%" in template.name_template: - potential_src_pads += 2 - break - else: - potential_src_pads += 1 - - return potential_src_pads > 1 - - def _plugDecodingQueue(self, pad): - queue = gst.element_factory_make("queue") - queue.props.max_size_time = self.QUEUE_SIZE - queue.props.max_size_buffers = 3 - self.add(queue) - queue.sync_state_with_parent() - pad.link_full(queue.get_pad("sink"), gst.PAD_LINK_CHECK_NOTHING) - pad = queue.get_pad("src") - - return pad - - def _tryToLink1(self, source, pad, factories): - """ - Tries to link one of the factories' element to the given pad. - - Returns the element that was successfully linked to the pad. - """ - self.debug("source:%s, pad:%s , factories:%r" % (source.get_name(), - pad.get_name(), - factories)) - - if self._isDemuxer(source): - pad = self._plugDecodingQueue(pad) - - result = None - for factory in factories: - element = factory.create() - if not element: - self.warning("weren't able to create element from %r" % factory) - continue - - sinkpad = element.get_pad("sink") - if not sinkpad: - continue - - self.add(element) - element.set_state(gst.STATE_READY) - try: - pad.link(sinkpad) - except: - element.set_state(gst.STATE_NULL) - self.remove(element) - continue - - self._closeLink(element) - element.set_state(gst.STATE_PAUSED) - - result = element - break - - return result - - def _closePadLink(self, element, pad, caps): - """ - Finds the list of elements that could connect to the pad. - If the pad has the desired caps, it will create a ghostpad. - If no compatible elements could be found, the search will stop. - """ - self.debug("element:%s, pad:%s, caps:%s" % (element.get_name(), - pad.get_name(), - caps.to_string())) - if caps.is_empty(): - self.log("unknown type") - return - if caps.is_any(): - self.log("type is not know yet, waiting") - return - - self.debug("stream %r" % (self.stream)) - if caps.intersect(self.caps) and (self.stream is None or - (self.stream.pad_name == get_pad_id(pad))): - # This is the desired caps - if not self._srcpad: - self._wrapUp(element, pad) - elif is_raw(caps) and pad_compatible_stream(pad, self.stream): - self.log ("not the target stream, but compatible") - if not self._srcpad: - self._wrapUp(element, pad) - elif is_raw(caps): - self.log("We hit a raw caps which isn't the wanted one") - # FIXME : recursively remove everything until demux/typefind - - else: - # Find something - if len(caps) > 1: - self.log("many possible types, delaying") - return - facts = self._findCompatibleFactory(caps) - if not facts: - self.log("unknown type") - return - self._tryToLink1(element, pad, facts) - - def _wrapUp(self, element, pad): - """ - Ghost the given pad of element. - Remove non-used elements. - """ - - if self._srcpad: - return - self._markValidElements(element) - gobject.idle_add(self._removeUnusedElements, self.typefind) - if pad.props.caps is not None: - caps = pad.props.caps - else: - caps = pad.get_caps() - - self._srcpad = gst.GhostPad("src", pad) - self._srcpad.set_active(True) - - if caps.is_fixed(): - self._exposePad(target=pad) - else: - self._blockPad(target=pad) - - def _exposePad(self, target): - self.log("ghosting pad %s" % target.get_name()) - self.add_pad(self._srcpad) - self.post_message(gst.message_new_state_dirty(self)) - - def _blockPad(self, target): - # don't pass target as an argument to set_blocked_async. Avoids - # triggering a bug in gst-python where pad_block_destroy_data calls - # CPython without acquiring the GIL - self._target = target - self._eventProbeId = target.add_event_probe(self._padEventCb) - self._srcpad.set_blocked_async(True, self._padBlockedCb) - - def _unblockPad(self, target): - target.remove_event_probe(self._eventProbeId) - self._eventProbeId = None - self._srcpad.set_blocked_async(False, self._padBlockedCb) - - def _padBlockedCb(self, ghost, blocked): - if not blocked: - if self.pending_newsegment is not None: - self._srcpad.push_event(self.pending_newsegment) - self.pending_newsegment = None - return - - self._exposePad(target=self._target) - self._unblockPad(target=self._target) - - def _padEventCb(self, pad, event): - if event.type == gst.EVENT_TAG: - self.debug("dropping TAG event") - return False - - if event.type != gst.EVENT_NEWSEGMENT: - self.warning("first event: %s is not a NEWSEGMENT, bailing out" % - event) - self._exposePad(target=pad) - self._unblockPad(target=pad) - return True - - self.debug("stored pending newsegment") - self.pending_newsegment = event - return False - - def _markValidElements(self, element): - """ - Mark this element and upstreams as valid - """ - self.log("element:%s" % element.get_name()) - if element == self.typefind: - return - self._validelements.append(element) - # find upstream element - pad = list(element.sink_pads())[0] - parent = pad.get_peer().get_parent() - self._markValidElements(parent) - - def _removeUnusedElements(self, element): - """ - Remove unused elements connected to srcpad(s) of element - """ - self.log("element:%r" % element) - for pad in list(element.src_pads()): - if pad.is_linked(): - peer = pad.get_peer().get_parent() - if isinstance(peer, gst.Element): - self._removeUnusedElements(peer) - if not peer in self._validelements: - self.log("removing %s" % peer.get_name()) - pad.unlink(pad.get_peer()) - peer.set_state(gst.STATE_NULL) - self.remove(peer) - - def _cleanUp(self): - self.log("") - if self._srcpad: - self.remove_pad(self._srcpad) - self._srcpad = None - self._target = None - for element in self._validelements: - element.set_state(gst.STATE_NULL) - self.remove(element) - self._validelements = [] - - ## Overrides - - def do_change_state(self, transition): - self.debug("transition:%r" % transition) - res = gst.Bin.do_change_state(self, transition) - if transition == gst.STATE_CHANGE_PAUSED_TO_READY: - self._cleanUp() - return res - - ## Signal callbacks - - def _typefindHaveTypeCb(self, typefind, probability, caps): - self.debug("probability:%d, caps:%s" % (probability, caps.to_string())) - self._closePadLink(typefind, typefind.get_pad("src"), caps) - - ## Dynamic element Callbacks - - def _dynamicPadAddedCb(self, element, pad): - self.log("element:%s, pad:%s" % (element.get_name(), pad.get_name())) - if not self._srcpad: - self._closePadLink(element, pad, pad.get_caps()) - - def _dynamicNoMorePadsCb(self, element): - self.log("element:%s" % element.get_name()) - -gobject.type_register(SingleDecodeBin) diff --git a/oxtimeline/timeline.py b/oxtimeline/timeline.py deleted file mode 100644 index a0b855c..0000000 --- a/oxtimeline/timeline.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# vi:si:et:sw=4:sts=4:ts=4 -# GPL 2008 -import gobject -gobject.threads_init() - -from glob import glob -import math -import os -import time - -import pygst -pygst.require("0.10") -import gst -import Image - -from singledecodebin import SingleDecodeBin -from imagesink import ImageSink -from video import Video - - -class Timeline(Video): - lastPos=0 - def __init__(self, uri): - Video.__init__(self, uri, '25/1') - - bus = self.get_bus() - bus.add_signal_watch() - self.watch_id = bus.connect("message", self.onBusMessage) - - self.mainloop = gobject.MainLoop() - - def extract(self, prefix, width, height): - self.tile_width = width - self.tile_height = height - self.prefix = prefix - self.timeline_fps = 25 - self.input_tile_width = int(math.ceil((float(self.framerate)/self.timeline_fps) * width)) - ntiles = int(math.ceil(float(self.frames)/self.input_tile_width)) - self.tiles = [] - for i in range(ntiles): - tile = Image.new("RGB", (self.input_tile_width, height)) - self.tiles.append(tile) - - self.set_state(gst.STATE_PLAYING) - self.mainloop.run() - - for i in range(ntiles): - tile = self.tiles[i] - if tile.size[0] != self.tile_width: - tile = tile.resize((self.tile_width, self.tile_height), Image.ANTIALIAS) - if i < (ntiles-1): - frames = self.input_tile_width - else: - frames = self.frames-((ntiles-1)*self.input_tile_width) - tile_width = int(math.ceil(self.timeline_fps*frames)/float(self.framerate)) - if -2 < self.tile_width - tile_width < 2: - tile_width = self.tile_width - tile = tile.crop((0, 0, tile_width, self.tile_height)) - filename = "%s.%s.%04d.png" % (self.prefix, self.tile_height, i) - tile.save(filename) - - def done(self): - self.mainloop.quit() - - def _frameCb(self, unused_thsink, frame, timestamp): - self.log("image:%s, timestamp:%s" % (frame, gst.TIME_ARGS(timestamp))) - - if not self._ready: - # we know we're prerolled when we get the initial thumbnail - self._ready = True - else: - _framePos = int(math.ceil((float(timestamp) / (gst.SECOND) * float(self.framerate)))) - frame = frame.resize((1, self.tile_height), Image.ANTIALIAS) - for framePos in range(self.lastPos, _framePos): - tile = int(math.floor(float(framePos) / self.input_tile_width)) - tilePos = framePos - (tile * self.input_tile_width) - for i in range(self.tile_height): - self.tiles[tile].putpixel((tilePos, i), frame.getpixel((0, i))) - - self.lastPos = _framePos - if self.mainloop and timestamp >= self.duration: - self.done() - - def onBusMessage(self, bus, message): - if message.src == self and message.type == gst.MESSAGE_EOS: - self.done() - -def loadTimeline(timeline_prefix, height=64): - files = sorted(glob('%s.%s.*.png' % (timeline_prefix, height))) - f = Image.open(files[0]) - width = f.size[0] - f = Image.open(files[-1]) - duration = f.size[0] + (len(files)-1)*width - timeline = Image.new("RGB", (duration, height)) - pos = 0 - for f in files: - part = Image.open(f) - timeline.paste(part, (pos, 0, pos + part.size[0], height)) - pos += part.size[0] - return timeline - -def createTimelineMultiline(timeline_prefix, width=600, height=16): - lineWidth = width - timlelineHeight = height - - timeline = loadTimeline(timeline_prefix) - duration = timeline.size[0] - - width = duration/25 #one pixel per second - timeline = timeline.resize((width, timlelineHeight), Image.ANTIALIAS).convert('RGBA') - - lineHeight = timlelineHeight + 2 * 4 - - lines = int(math.ceil(width / lineWidth) + 1) - size = (lineWidth, lineHeight * lines) - - timelineColor = (64, 64, 64) - i = Image.new("RGBA", size) - - #padd end with nothing to fit to grid - t = Image.new("RGBA", (lineWidth * lines, timlelineHeight)) - t.paste(timeline, (0, 0)) - - for currentLine in range(0, lines): - offset = currentLine * lineHeight + 4 - toffset = currentLine * lineWidth - try: - tbox = t.crop((toffset, 0, toffset + lineWidth, timlelineHeight)) - box = ((0, offset , tbox.size[0], offset + tbox.size[1])) - i.paste(tbox, box) - except: - broken = True - width = lineWidth - if currentLine == lines -1: - width = duration - (lines - 1) * lineWidth - box = ((0, offset , width, offset + timlelineHeight)) - i.paste(timelineColor, box) - timeline_file = '%s.overview.png' % (timeline_prefix) - i.save(timeline_file, 'PNG') - - timeline8_file = '%s.overview.8.png' % (timeline_prefix) - if lines < 8: - i.save(timeline8_file, 'PNG') - else: - i.crop((0,0,lineWidth, 8 * lineHeight)).save(timeline8_file, 'PNG') - - -def makeTimelineByFramesPerPixel(timeline_prefix, frames_per_pixel, inpoint=0, outpoint=0, height=16): - pos = 0 - input_scale = 25 - - timeline_file = '%s.pixels.%s.png' % (timeline_prefix, width) - if outpoint > 0: - timeline_file = '%s.pixels.%s.%d-%d.png' % (timeline_prefix, width, inpoint, outpoint) - - timeline = loadTimeline(timeline_prefix) - duration = timeline.size[0] - - - width = duration / frames_per_pixel - - if inpoint<=0: - inpoint = 0 - else: - inpoint = inpoint * input_scale - if outpoint<=0: - outpoint = pos - else: - outpoint = outpoint * input_scale - - timeline = timeline.crop((inpoint, 0, outpoint, timeline.size[1])).resize((width, height), Image.ANTIALIAS) - timeline.save(timeline_file) - -def makeTimelineOverview(timeline_prefix, width, inpoint=0, outpoint=0, duration=-1, height=16): - input_scale = 25 - - timeline_file = '%s.%s.png' % (timeline_prefix, height) - if outpoint > 0: - timeline_file = '%s.overview.%s.%d-%d.png' % (timeline_prefix, height, inpoint, outpoint) - - timeline = loadTimeline(timeline_prefix) - duration = timeline.size[0] - - if inpoint<=0: - inpoint = 0 - else: - inpoint = inpoint * input_scale - if outpoint<=0: - outpoint = duration - else: - outpoint = outpoint * input_scale - - timeline = timeline.crop((inpoint, 0, outpoint, timeline.size[1])).resize((width, height), Image.ANTIALIAS) - timeline.save(timeline_file) - -def makeTiles(timeline_prefix, height=16, width=3600): - files = glob('%s.64.*.png' % timeline_prefix) - fps = 25 - part_step = 60 - output_width = width - width = len(files) * part_step - timeline = Image.new("RGB", (width, height)) - - pos = 0 - for f in sorted(files): - part = Image.open(f) - part_width = int(part.size[0] / fps) - part = part.resize((part_width, height), Image.ANTIALIAS) - timeline.paste(part, (pos, 0, pos+part_width, height)) - pos += part_width - - timeline = timeline.crop((0, 0, pos, height)) - - pos = 0 - i = 0 - while pos < timeline.size[0]: - end = min(pos+output_width, timeline.size[0]) - timeline.crop((pos, 0, end, timeline.size[1])).save('%s.%s.%04d.png' % (timeline_prefix, timeline.size[1], i)) - pos += output_width - i += 1 - diff --git a/oxtimeline/video.py b/oxtimeline/video.py index 86e2575..6865193 100644 --- a/oxtimeline/video.py +++ b/oxtimeline/video.py @@ -1,45 +1,30 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 -# GPL 2008 -import gobject -gobject.threads_init() - -import pygst -pygst.require("0.10") -import gst +# GPL 2008-2010 +from __future__ import division import Image import time +import math + +import gobject +import gst -from singledecodebin import SingleDecodeBin from imagesink import ImageSink + class Video(gst.Pipeline): - #def log(self, msg): - # print msg + duration = -1 - def __init__(self, uri, framerate='25/1'): + def __init__(self, uri, height=64, framerate=gst.Fraction(25, 1)): gst.Pipeline.__init__(self) + self.height = height self.framerate = framerate - self.duration = -1 - # queue of timestamps - self._queue = [] - # queue callbacks - self.callback = {} - # extracted frames - self._frames = {} - # true only if we are prerolled - self._ready = False - - uri = 'file://' + uri - self.log("uri : %s" % uri) - - self.uri = uri - - self.sbin = SingleDecodeBin(caps=gst.Caps("video/x-raw-rgb;video/x-raw-yuv"), - uri=self.uri) + self.src = gst.element_factory_make("filesrc") + self.src.props.location = uri + self.sbin = gst.element_factory_make("decodebin2") + self.csp = gst.element_factory_make("ffmpegcolorspace") self.scale = gst.element_factory_make("videoscale") self.rate = gst.element_factory_make("videorate") @@ -48,52 +33,39 @@ class Video(gst.Pipeline): self.sink = ImageSink() self.sink.connect('frame', self._frameCb) - self.add(self.sbin, self.csp, self.queue, self.scale, self.rate, self.sink) + self.add(self.src, self.sbin, self.csp, self.queue, self.scale, self.rate, self.sink) + self.src.link(self.sbin) self.sbin.connect('pad-added', self._sbinPadAddedCb) self.set_state(gst.STATE_PAUSED) self.get_state() - self.width = self.sink.width - self.height = self.sink.height - self.framerate = self.sink.framerate self.getDuration() self.frames = int((float(self.duration) / gst.SECOND) * float(self.framerate)) def _sbinPadAddedCb(self, unused_sbin, pad): - self.log("pad : %s" % pad) - pad.link(self.queue.get_pad("sink")) caps = pad.get_caps() - width = caps[0]["width"] - height = caps[0]["height"] - - if width % 4: - width += 4 - width % 4 - if height % 4: - height += 4 - height % 4 - - self.queue.link(self.scale) - self.scale.link(self.rate) - self.rate.link(self.csp, gst.Caps("video/x-raw-rgb;video/x-raw-yuv,framerate=%s,width=%s,height=%s" % (self.framerate, width, height))) - self.csp.link(self.sink) + if 'video' in str(caps): + pad.link(self.queue.get_pad("sink")) + caps = pad.get_caps() + self.queue.link(self.scale) + self.scale.link(self.rate) + height = self.height + if 'width' in caps[0].keys() and isinstance(caps[0]['width'], int): + width = int(caps[0]['width']/caps[0]['height'] * height) + else: + width = int(4/3 * height) + if width % 4: + width += 4 - width % 4 + if height % 4: + height += 4 - height % 4 + + self.rate.link(self.csp, gst.Caps("video/x-raw-rgb;video/x-raw-yuv,framerate=%s/%s,width=%s,height=%s" % (self.framerate.num, self.framerate.denom, width, height))) + self.csp.link(self.sink) def _frameCb(self, unused_thsink, frame, timestamp): - self.log("image:%s, timestamp:%s" % (frame, gst.TIME_ARGS(timestamp))) - - if not self._ready: - # we know we're prerolled when we get the initial thumbnail - self._ready = True - elif timestamp in self.callback and self.callback[timestamp]: - self.callback[timestamp](frame, timestamp) - del self.callback[timestamp] - - if timestamp in self._queue: - self._queue.remove(timestamp) - - if self._queue: - # still some more thumbnails to process - gobject.idle_add(self._getFrame, self._queue.pop(0)) + pass def getDuration(self): if self.duration < 0: @@ -104,59 +76,69 @@ class Video(gst.Pipeline): format, self.duration = q.parse_duration() return self.duration - def getFrame(self, timestamp, callback): - """ - Queue a frame request for the given timestamp, - callback is called once frame is extracted. - returns False if timestamp > duration - """ - - self.log("timestamp %s" % gst.TIME_ARGS(timestamp)) - if self.duration < 0: - self.getDuration() - - if timestamp > self.duration: - self.log("timestamp %s > duration %s" % (timestamp, self.duration)) - return False +class Timeline(Video): + _ready = False + lastPos=0 + timeline_fps = 25 - self.callback[timestamp] = callback + def __init__(self, uri, prefix, width, height): + Video.__init__(self, uri, height, gst.Fraction(self.timeline_fps, 1)) - if self._queue or not self._ready: - self.log('ready') - self._queue.append(timestamp) - else: + bus = self.get_bus() + bus.add_signal_watch() + self.watch_id = bus.connect("message", self.onBusMessage) - self._queue.append(timestamp) - self._getFrame(timestamp) - return True - - def _getFrame(self, timestamp): - if not self._ready: - return - self.log("timestamp : %s" % gst.TIME_ARGS(timestamp)) - self.seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, - gst.SEEK_TYPE_SET, timestamp, - gst.SEEK_TYPE_NONE, -1) - return False - - def frame(self, timestamp): self.mainloop = gobject.MainLoop() - self._frames[timestamp] = None - def callback(frame, timestamp): - self._frames[timestamp] = frame - self.mainloop.quit() - self._quit = False - def quit(): - if self._quit: - self.mainloop.quit() - return False - else: - self._quit = True - return True - gobject.timeout_add(1000, quit) - if self.getFrame(timestamp, callback): - self.mainloop.run() - frame = self._frames[timestamp] - del self._frames[timestamp] - return frame + + self.tile_width = width + self.tile_height = height + self.prefix = prefix + self.input_tile_width = int(math.ceil((float(self.framerate)/self.timeline_fps) * width)) + ntiles = int(math.ceil(float(self.frames)/self.input_tile_width)) + self.tiles = [] + for i in range(ntiles): + tile = Image.new("RGB", (self.input_tile_width, height)) + self.tiles.append(tile) + + self.set_state(gst.STATE_PLAYING) + self.mainloop.run() + + for i in range(ntiles): + tile = self.tiles[i] + if tile.size[0] != self.tile_width: + tile = tile.resize((self.tile_width, self.tile_height), Image.ANTIALIAS) + if i < (ntiles-1): + frames = self.input_tile_width + else: + frames = self.frames-((ntiles-1)*self.input_tile_width) + tile_width = int(math.ceil(self.timeline_fps*frames)/float(self.framerate)) + if -2 < self.tile_width - tile_width < 2: + tile_width = self.tile_width + tile = tile.crop((0, 0, tile_width, self.tile_height)) + filename = "%s.%s.%04d.png" % (self.prefix, self.tile_height, i) + tile.save(filename) + + def done(self): + self.mainloop.quit() + + def _frameCb(self, unused_thsink, frame, timestamp): + if not self._ready: + # we know we're prerolled when we get the initial thumbnail + self._ready = True + else: + _framePos = int(math.ceil((float(timestamp) / (gst.SECOND) * float(self.framerate)))) + frame = frame.resize((1, self.tile_height), Image.ANTIALIAS) + for framePos in range(self.lastPos, _framePos): + tile = int(math.floor(float(framePos) / self.input_tile_width)) + tilePos = framePos - (tile * self.input_tile_width) + for i in range(self.tile_height): + self.tiles[tile].putpixel((tilePos, i), frame.getpixel((0, i))) + + self.lastPos = _framePos + if self.mainloop and timestamp >= self.duration: + self.done() + + def onBusMessage(self, bus, message): + if message.src == self and message.type == gst.MESSAGE_EOS: + self.done()