def __init__(self, pipeline): Loggable.__init__(self) self.pipeline = pipeline self.bus = self.pipeline.get_bus() self.cpu_usage_tracker = CPUUsageTracker() self.rate = 1.0 self.done = False self.ready = False self.lastPos = 0 self._bus_cb_id = None
def __init__(self, ges_elem, max_cpu_usage): Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) self.__preroll_timeout_id = 0 # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT self.__image_pixbuf = None if isinstance(ges_elem, GES.ImageSource): self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) self.thumbs = {} self.thumb_cache = ThumbnailCache.get(self.uri) self._ensure_proxy_thumbnails_cache() self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpoint_changed_cb) self.pipeline = None self.gdkpixbufsink = None self.becomeControlled() self.connect("notify::height-request", self._height_changed_cb)
def __init__(self, ges_elem, max_cpu_usage): Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage) Zoomable.__init__(self) Loggable.__init__(self) self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) self.__start_id = 0 self.__preroll_timeout_id = 0 self._thumb_cb_id = 0 # The thumbs to be generated. self.queue = [] # The position for which a thumbnail is currently being generated. self.position = -1 # The positions for which we failed to get a pixbuf. self.failures = set() self._thumb_cb_id = None self.thumbs = {} self.thumb_height = THUMB_HEIGHT self.thumb_width = 0 self.__image_pixbuf = None if not isinstance(ges_elem, GES.ImageSource): self.thumb_cache = ThumbnailCache.get(self.uri) self._ensure_proxy_thumbnails_cache() self.thumb_width, unused_height = self.thumb_cache.image_size self.pipeline = None self.gdkpixbufsink = None self.cpu_usage_tracker = CPUUsageTracker() # Initial delay before generating the next thumbnail, in millis. self.interval = 500 # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpoint_changed_cb) self.ges_elem.connect("notify::duration", self._duration_changed_cb) self.become_controlled() self.connect("notify::height-request", self._height_changed_cb)
def __init__(self, bElement, timeline): """ @param bElement : the backend GES.TrackElement @param track : the track to which the bElement belongs @param timeline : the containing graphic timeline. """ Clutter.ScrollActor.__init__(self) PreviewGenerator.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = timeline self.bElement = bElement # Guard against malformed URIs self.uri = quote_uri(bElement.props.uri) self.duration = bElement.props.duration # Variables related to thumbnailing self.wishlist = [] self._thumb_cb_id = None self._allAnimated = False self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX self.thumb_width = None # will be set by self._setupPipeline() # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = get_cache_for_uri(self.uri) self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.timeline.connect("scrolled", self._scrollCb) self.bElement.connect("notify::duration", self._durationChangedCb) self.bElement.connect("notify::in-point", self._inpointChangedCb) self.bElement.connect("notify::start", self._startChangedCb) self.pipeline = None self.becomeControlled()
def __init__(self, bElement, timeline): """ @param bElement : the backend GES.TrackElement @param track : the track to which the bElement belongs @param timeline : the containing graphic timeline. """ Zoomable.__init__(self) Clutter.ScrollActor.__init__(self) Loggable.__init__(self) PreviewGenerator.__init__(self, GES.TrackType.VIDEO) # Variables related to the timeline objects self.timeline = timeline self.bElement = bElement self.uri = quote_uri(bElement.props.uri) # Guard against malformed URIs self.duration = bElement.props.duration # Variables related to thumbnailing self.wishlist = [] self._callback_id = None self._thumb_cb_id = None self._allAnimated = False self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = long(0.5 * Gst.SECOND) self.thumb_margin = BORDER_WIDTH self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin self.thumb_width = None # will be set by self._setupPipeline() # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = get_cache_for_uri(self.uri) self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.timeline.connect("scrolled", self._scrollCb) self.bElement.connect("notify::duration", self._durationChangedCb) self.bElement.connect("notify::in-point", self._inpointChangedCb) self.bElement.connect("notify::start", self._startChangedCb) self.pipeline = None self.becomeControlled()
def __init__(self, ges_elem): """ @param ges_elem : the backend GES.TrackElement @param track : the track to which the ges_elem belongs """ Previewer.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = getThumbnailCache(self.uri) self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpointChangedCb) self.pipeline = None self.gdkpixbufsink = None self.__last_rectangle = Gdk.Rectangle() self.becomeControlled() self.connect("notify::height-request", self._heightChangedCb)
def __init__(self, bElement): """ @param bElement : the backend GES.TrackElement @param track : the track to which the bElement belongs """ super(VideoPreviewer, self).__init__() PreviewGenerator.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = bElement.get_parent().get_timeline().ui self.bElement = bElement # Guard against malformed URIs self.uri = quote_uri(bElement.props.uri) # Variables related to thumbnailing self.wishlist = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = get_cache_for_uri(self.uri) self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.bElement.connect("notify::in-point", self._inpointChangedCb) self.pipeline = None self.__last_rectangle = Gdk.Rectangle() self.becomeControlled() self.connect("notify::height-request", self._heightChangedCb)
class VideoPreviewer(Previewer, Zoomable, Loggable): """A video previewer widget, drawing thumbnails. Attributes: ges_elem (GES.TrackElement): The previewed element. """ # We could define them in Previewer, but for some reason they are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, ges_elem): Previewer.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT self.__image_pixbuf = None if isinstance(ges_elem, GES.ImageSource): self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = ThumbnailCache.get(self.uri) self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpointChangedCb) self.pipeline = None self.gdkpixbufsink = None self.__last_rectangle = Gdk.Rectangle() self.becomeControlled() self.connect("notify::height-request", self._heightChangedCb) # Internal API def _setupPipeline(self): """Creates the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format( uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") sinkpad = self.gdkpixbufsink.get_static_pad("sink") self.pipeline.set_state(Gst.State.PAUSED) # Wait for the pipeline to be prerolled so we can check the width # that the thumbnails will have and set the aspect ratio accordingly # as well as getting the framerate of the video: change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE) if Gst.StateChangeReturn.SUCCESS == change_return[0]: neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] else: # the pipeline couldn't be prerolled so we can't determine the # correct values. Set sane defaults (this should never happen) self.warning("Couldn't preroll the pipeline") # assume 16:9 aspect ratio self.thumb_width = 16 * self.thumb_height / 9 decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplugSelectCb) # pop all messages from the bus so we won't be flooded with messages # from the prerolling phase while self.pipeline.get_bus().pop(): continue # add a message handler that listens for the created pixbufs self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.__bus_message_handler) def _checkCPU(self): """Adjusts when the next thumbnail is generated. Checks the CPU usage and adjusts the waiting time at which the next thumbnail will be generated +/- 10%. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < THUMBNAILS_CPU_USAGE: self.interval *= 0.9 self.log( 'Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"', self.interval, filename_from_uri(self.uri)) else: self.interval *= 1.1 self.log( 'Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"', self.interval, filename_from_uri(self.uri)) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb, priority=GLib.PRIORITY_LOW) def _startThumbnailingWhenIdle(self): self.debug('Waiting for UI to become idle for: %s', filename_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return # self.props.width_request = self.nsToPixel(self.ges_elem.get_asset().get_filesource_asset().props.duration) # self.props.width = self.nsToPixel(self.ges_elem.get_asset().get_filesource_asset().props.duration) self.debug('Now generating thumbnails for: %s', filename_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.ges_elem.props.duration self.queue = list(range(0, duration, self.thumb_period)) self._checkCPU() if self.ges_elem.props.in_point != 0: adj = self.get_hadjustment() adj.props.page_size = 1.0 adj.props.value = Zoomable.nsToPixel(self.ges_elem.props.in_point) # self._addVisibleThumbnails() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = random.randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"' % filename_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True else: return False # Stop the timer def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't # smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _remove_all_children(self): for child in self.get_children(): self.remove(child) def _addVisibleThumbnails(self, rect): """Gets the thumbnails for the currently visible clip portion.""" if self.thumb_width is None: return False self.thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left = self.pixelToNs(rect.x) + self.ges_elem.props.in_point element_right = element_left + self.pixelToNs(rect.width) element_left = quantize(element_left, thumb_duration) for current_time in range(element_left, element_right, thumb_duration): thumb = Thumbnail(self.thumb_width, self.thumb_height) x = Zoomable.nsToPixel(current_time) - self.nsToPixel( self.ges_elem.props.in_point) y = (self.props.height_request - self.thumb_height) / 2 self.put(thumb, x, y) self.thumbs[current_time] = thumb if self.__image_pixbuf: thumb.set_from_pixbuf(self.__image_pixbuf) thumb.set_visible(True) elif current_time in self.thumb_cache: pixbuf = self.thumb_cache[current_time] thumb.set_from_pixbuf(pixbuf) thumb.set_visible(True) else: self.wishlist.append(current_time) return True def _get_wish(self): """Returns a wish that is also in the queue, if any.""" while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _setThumbnail(self, time, pixbuf): # Q: Is "time" guaranteed to be nanosecond precise? # A: Not always. # => __tim says: "that's how it should be" # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test # => Daniel: It is *not* nanosecond precise when we remove the videorate # element from the pipeline # => thiblahute: not the case with mpegts if time in self.thumbs: thumb = self.thumbs[time] else: sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, time) time = sorted_times[index] thumb = self.thumbs[time] thumb.set_from_pixbuf(pixbuf) if time in self.queue: self.queue.remove(time) self.thumb_cache[time] = pixbuf self.queue_draw() # Interface (Zoomable) def zoomChanged(self): self._remove_all_children() # Callbacks def __bus_message_handler(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._setThumbnail(stream_time, pixbuf) elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS # pylint: disable=no-self-use def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _heightChangedCb(self, unused_widget, unused_value): self._remove_all_children() def _inpointChangedCb(self, unused_b_element, unused_value): self.get_hadjustment().set_value( Zoomable.nsToPixel(self.ges_elem.props.in_point)) def setSelected(self, selected): if selected: opacity = 0.5 else: opacity = 1.0 for thumb in self.get_children(): thumb.props.opacity = opacity def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self.emit("done") def cleanup(self): """Stops preview generation and cleans the object.""" self.stopGeneration() Zoomable.__del__(self) # pylint: disable=arguments-differ def do_draw(self, context): res, rect = Gdk.cairo_get_clip_rectangle(context) assert res if self.__last_rectangle.x != rect.x or \ self.__last_rectangle.y != rect.y or \ self.__last_rectangle.width != rect.width or \ self.__last_rectangle.height != rect.height: if self._addVisibleThumbnails(rect): self.__last_rectangle = rect else: self.__last_rectangle = Gdk.Rectangle() Gtk.Layout.do_draw(self, context)
class VideoPreviewer(Previewer, Zoomable, Loggable): """A video previewer widget, drawing thumbnails. Attributes: ges_elem (GES.TrackElement): The previewed element. thumbs (dict): Maps (quantized) times to Thumbnail objects. thumb_cache (ThumbnailCache): The pixmaps persistent cache. """ # We could define them in Previewer, but for some reason they are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, ges_elem, max_cpu_usage): Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) self.__preroll_timeout_id = 0 # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT self.__image_pixbuf = None if isinstance(ges_elem, GES.ImageSource): self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) self.thumbs = {} self.thumb_cache = ThumbnailCache.get(self.uri) self._ensure_proxy_thumbnails_cache() self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpoint_changed_cb) self.pipeline = None self.gdkpixbufsink = None self.becomeControlled() self.connect("notify::height-request", self._height_changed_cb) # Internal API def _setupPipeline(self): """Creates the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplug_select_cb) self.__preroll_timeout_id = GLib.timeout_add_seconds(MAX_BRINGING_TO_PAUSED_DURATION, self.__preroll_timed_out_cb) self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.__bus_message_cb) self.pipeline.set_state(Gst.State.PAUSED) def _checkCPU(self): """Adjusts when the next thumbnail is generated. Checks the CPU usage and adjusts the waiting time at which the next thumbnail will be generated +/- 10%. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < self._max_cpu_usage: self.interval *= 0.9 self.log( 'Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"', self.interval, path_from_uri(self.uri)) else: self.interval *= 1.1 self.log( 'Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"', self.interval, path_from_uri(self.uri)) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb, priority=GLib.PRIORITY_LOW) def _startThumbnailingWhenIdle(self): self.debug( 'Waiting for UI to become idle for: %s', path_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return self.debug( 'Now generating thumbnails for: %s', path_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.ges_elem.props.duration self.queue = list(range(0, duration, self.thumb_period)) self._checkCPU() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = random.randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"', path_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource self._thumb_cb_id = None return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True # Stop the timer return False def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't # smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _update_thumbnails(self): """Updates the thumbnails for the currently visible clip portion.""" if self.thumb_width is None: return False thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left = quantize(self.ges_elem.props.in_point, thumb_duration) element_right = self.ges_elem.props.in_point + self.ges_elem.props.duration for position in range(element_left, element_right, thumb_duration): x = Zoomable.nsToPixel(position) - self.nsToPixel(self.ges_elem.props.in_point) y = (self.props.height_request - self.thumb_height) / 2 try: thumb = self.thumbs.pop(position) self.move(thumb, x, y) except KeyError: thumb = Thumbnail(self.thumb_width, self.thumb_height) self.put(thumb, x, y) thumbs[position] = thumb if self.__image_pixbuf: # The thumbnail is fixed, probably it's an image clip. thumb.set_from_pixbuf(self.__image_pixbuf) thumb.set_visible(True) elif position in self.thumb_cache: pixbuf = self.thumb_cache[position] thumb.set_from_pixbuf(pixbuf) thumb.set_visible(True) else: self.wishlist.append(position) for thumb in self.thumbs.values(): self.remove(thumb) self.thumbs = thumbs return True def _get_wish(self): """Returns a wish that is also in the queue, if any.""" while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _set_pixbuf(self, position, pixbuf): """Sets the pixbuf for the thumbnail at the specified position.""" if position in self.thumbs: thumb = self.thumbs[position] else: # The pixbufs we get from gdkpixbufsink are not always # exactly the ones requested, the reported position can differ. # Try to find the closest thumbnail for the specified position. sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, position) position = sorted_times[index] thumb = self.thumbs[position] thumb.set_from_pixbuf(pixbuf) if position in self.queue: self.queue.remove(position) self.thumb_cache[position] = pixbuf self.queue_draw() # Interface (Zoomable) def zoomChanged(self): self._update_thumbnails() # Callbacks def __bus_message_cb(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink and \ self.__preroll_timeout_id == 0: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._set_pixbuf(stream_time, pixbuf) elif message.src == self.pipeline and message.type == Gst.MessageType.STATE_CHANGED: if message.parse_state_changed()[1] == Gst.State.PAUSED: if self.__preroll_timeout_id: GLib.source_remove(self.__preroll_timeout_id) self.__preroll_timeout_id = 0 sinkpad = self.gdkpixbufsink.get_static_pad("sink") neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] self._update_thumbnails() elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS def __preroll_timed_out_cb(self): self.stopGeneration() # pylint: disable=no-self-use def _autoplug_select_cb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _height_changed_cb(self, unused_widget, unused_param_spec): self._update_thumbnails() def _inpoint_changed_cb(self, unused_ges_timeline_element, unused_param_spec): self._update_thumbnails() def setSelected(self, selected): if selected: opacity = 0.5 else: opacity = 1.0 for thumb in self.get_children(): thumb.props.opacity = opacity def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def _ensure_proxy_thumbnails_cache(self): """Ensures that both the target asset and the proxy assets have caches.""" uri = quote_uri(self.ges_elem.props.uri) if self.uri != uri: self.thumb_cache.copy(uri) def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.get_bus().remove_signal_watch() self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self._ensure_proxy_thumbnails_cache() self.emit("done") def release(self): """Stops preview generation and cleans the object.""" self.stopGeneration() Zoomable.__del__(self)
class PipelineCpuAdapter(Loggable): """ This pipeline manager will modulate the rate of the provided pipeline. It is the responsibility of the caller to set the sync of the sink to True, disable QOS and provide a pipeline with a rate of 1.0. Doing otherwise would be cheating. Cheating is bad. """ def __init__(self, pipeline): Loggable.__init__(self) self.pipeline = pipeline self.bus = self.pipeline.get_bus() self.cpu_usage_tracker = CPUUsageTracker() self.rate = 1.0 self.done = False self.ready = False self.lastPos = 0 self._bus_cb_id = None def start(self): GLib.timeout_add(200, self._modulateRate) self._bus_cb_id = self.bus.connect("message", self._messageCb) self.done = False def stop(self): if self._bus_cb_id is not None: self.bus.disconnect(self._bus_cb_id) self._bus_cb_id = None self.pipeline = None self.done = True def _modulateRate(self): """ Adapt the rate of audio playback (analysis) depending on CPU usage. """ if self.done: return False usage_percent = self.cpu_usage_tracker.usage() self.cpu_usage_tracker.reset() if usage_percent >= WAVEFORMS_CPU_USAGE: if self.rate < 0.1: if not self.ready: self.ready = True self.pipeline.set_state(Gst.State.READY) res, self.lastPos = self.pipeline.query_position(Gst.Format.TIME) return True if self.rate > 0.0: self.rate *= 0.9 self.log('Pipeline rate slowed down (-10%%) to %.3f' % self.rate) else: self.rate *= 1.1 self.log('Pipeline rate sped up (+10%%) to %.3f' % self.rate) if not self.ready: res, position = self.pipeline.query_position(Gst.Format.TIME) else: if self.rate > 0.5: # This to avoid going back and forth from READY to PAUSED self.pipeline.set_state(Gst.State.PAUSED) # The message handler will unset ready and seek correctly. return True self.pipeline.set_state(Gst.State.PAUSED) self.pipeline.seek(self.rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, position, Gst.SeekType.NONE, -1) self.pipeline.set_state(Gst.State.PLAYING) self.ready = False # Keep the glib timer running: return True def _messageCb(self, unused_bus, message): if not self.ready: return if message.type == Gst.MessageType.STATE_CHANGED: prev, new, pending = message.parse_state_changed() if message.src == self.pipeline: if prev == Gst.State.READY and new == Gst.State.PAUSED: self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, self.lastPos, Gst.SeekType.NONE, -1) self.ready = False
class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable): def __init__(self, bElement, timeline): """ @param bElement : the backend GES.TrackElement @param track : the track to which the bElement belongs @param timeline : the containing graphic timeline. """ Zoomable.__init__(self) Clutter.ScrollActor.__init__(self) Loggable.__init__(self) PreviewGenerator.__init__(self, GES.TrackType.VIDEO) # Variables related to the timeline objects self.timeline = timeline self.bElement = bElement self.uri = quote_uri(bElement.props.uri) # Guard against malformed URIs self.duration = bElement.props.duration # Variables related to thumbnailing self.wishlist = [] self._callback_id = None self._thumb_cb_id = None self._allAnimated = False self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = long(0.5 * Gst.SECOND) self.thumb_margin = BORDER_WIDTH self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin self.thumb_width = None # will be set by self._setupPipeline() # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = get_cache_for_uri(self.uri) self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.timeline.connect("scrolled", self._scrollCb) self.bElement.connect("notify::duration", self._durationChangedCb) self.bElement.connect("notify::in-point", self._inpointChangedCb) self.bElement.connect("notify::start", self._startChangedCb) self.pipeline = None self.becomeControlled() # Internal API def _update(self, unused_msg_source=None): if self._callback_id: GLib.source_remove(self._callback_id) if self.thumb_width: self._addVisibleThumbnails() if self.wishlist: self.becomeControlled() def _setupPipeline(self): """ Create the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") sinkpad = self.gdkpixbufsink.get_static_pad("sink") self.pipeline.set_state(Gst.State.PAUSED) # Wait for the pipeline to be prerolled so we can check the width # that the thumbnails will have and set the aspect ratio accordingly # as well as getting the framerate of the video: change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE) if Gst.StateChangeReturn.SUCCESS == change_return[0]: neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] else: # the pipeline couldn't be prerolled so we can't determine the # correct values. Set sane defaults (this should never happen) self.warning("Couldn't preroll the pipeline") # assume 16:9 aspect ratio self.thumb_width = 16 * self.thumb_height / 9 decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplugSelectCb) # pop all messages from the bus so we won't be flooded with messages # from the prerolling phase while self.pipeline.get_bus().pop(): continue # add a message handler that listens for the created pixbufs self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.bus_message_handler) def _checkCPU(self): """ Check the CPU usage and adjust the time interval (+10 or -10%) at which the next thumbnail will be generated. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < THUMBNAILS_CPU_USAGE: self.interval *= 0.9 self.log('Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri))) else: self.interval *= 1.1 self.log('Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri))) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb) def _startThumbnailingWhenIdle(self): self.debug('Waiting for UI to become idle for: %s', filename_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return self.debug('Now generating thumbnails for: %s', filename_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.duration else: self.duration = duration self.queue = range(0, duration, self.thumb_period) self._checkCPU() self._addVisibleThumbnails() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"' % filename_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True else: return False # Stop the timer def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _addVisibleThumbnails(self): """ Get the thumbnails to be displayed in the currently visible clip portion """ self.remove_all_children() old_thumbs = self.thumbs self.thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left, element_right = self._get_visible_range() element_left = quantize(element_left, thumb_duration) for current_time in range(element_left, element_right, thumb_duration): thumb = Thumbnail(self.thumb_width, self.thumb_height) thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin) self.add_child(thumb) self.thumbs[current_time] = thumb if current_time in self.thumb_cache: gdkpixbuf = self.thumb_cache[current_time] if self._allAnimated or current_time not in old_thumbs: self.thumbs[current_time].set_from_gdkpixbuf_animated(gdkpixbuf) else: self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf) else: self.wishlist.append(current_time) self._allAnimated = False def _get_wish(self): """ Returns a wish that is also in the queue, or None if no such wish exists """ while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _setThumbnail(self, time, pixbuf): # Q: Is "time" guaranteed to be nanosecond precise? # A: Not always. # => __tim says: "that's how it should be" # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test # => Daniel: It is *not* nanosecond precise when we remove the videorate # element from the pipeline # => thiblahute: not the case with mpegts original_time = time if time in self.thumbs: thumb = self.thumbs[time] else: sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, time) time = sorted_times[index] thumb = self.thumbs[time] if thumb.has_pixel_data: # If this happens, it means the precision of the thumbnail # generator is not good enough for the current thumbnail # interval. # We could consider shifting the thumbnails, but seems like # too much trouble for something which does not happen in # practice. My last words.. self.fixme("Thumbnail is already set for time: %s, %s", format_ns(time), format_ns(original_time)) return thumb.set_from_gdkpixbuf_animated(pixbuf) if time in self.queue: self.queue.remove(time) self.thumb_cache[time] = pixbuf # Interface (Zoomable) def zoomChanged(self): self.remove_all_children() self._allAnimated = True self._update() def _get_visible_range(self): # Shortcut/convenience variables: start = self.bElement.props.start in_point = self.bElement.props.in_point duration = self.bElement.props.duration timeline_left, timeline_right = self._get_visible_timeline_range() element_left = timeline_left - start + in_point element_left = max(element_left, in_point) element_right = timeline_right - start + in_point element_right = min(element_right, in_point + duration) return (element_left, element_right) # TODO: move to Timeline or to utils def _get_visible_timeline_range(self): # determine the visible left edge of the timeline # TODO: isn't there some easier way to get the scroll point of the ScrollActor? # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x) timeline_left = self.timeline.get_scroll_point().x # determine the width of the pipeline # by intersecting the timeline's and the stage's allocation timeline_allocation = self.timeline.props.allocation stage_allocation = self.timeline.get_stage().props.allocation timeline_rect = Clutter.Rect() timeline_rect.init(timeline_allocation.x1, timeline_allocation.y1, timeline_allocation.x2 - timeline_allocation.x1, timeline_allocation.y2 - timeline_allocation.y1) stage_rect = Clutter.Rect() stage_rect.init(stage_allocation.x1, stage_allocation.y1, stage_allocation.x2 - stage_allocation.x1, stage_allocation.y2 - stage_allocation.y1) has_intersection, intersection = timeline_rect.intersection(stage_rect) if not has_intersection: return (0, 0) timeline_width = intersection.size.width # determine the visible right edge of the timeline timeline_right = timeline_left + timeline_width # convert to nanoseconds time_left = Zoomable.pixelToNs(timeline_left) time_right = Zoomable.pixelToNs(timeline_right) return (time_left, time_right) # Callbacks def bus_message_handler(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._setThumbnail(stream_time, pixbuf) elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _scrollCb(self, unused): self._update() def _startChangedCb(self, unused_bElement, unused_value): self._update() def _inpointChangedCb(self, unused_bElement, unused_value): position = Clutter.Point() position.x = Zoomable.nsToPixel(self.bElement.props.in_point) self.scroll_to_point(position) self._update() def _durationChangedCb(self, unused_bElement, unused_value): new_duration = max(self.duration, self.bElement.props.duration) if new_duration > self.duration: self.duration = new_duration self._update() def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None PreviewGenerator.emit(self, "done") def cleanup(self): self.stopGeneration() Zoomable.__del__(self)
class VideoPreviewer(Previewer, Zoomable, Loggable): """A video previewer widget, drawing thumbnails. Attributes: ges_elem (GES.TrackElement): The previewed element. """ # We could define them in Previewer, but for some reason they are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, ges_elem): Previewer.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT self.__image_pixbuf = None if isinstance(ges_elem, GES.ImageSource): self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = getThumbnailCache(self.uri) self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpointChangedCb) self.pipeline = None self.gdkpixbufsink = None self.__last_rectangle = Gdk.Rectangle() self.becomeControlled() self.connect("notify::height-request", self._heightChangedCb) # Internal API def _setupPipeline(self): """Creates the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") sinkpad = self.gdkpixbufsink.get_static_pad("sink") self.pipeline.set_state(Gst.State.PAUSED) # Wait for the pipeline to be prerolled so we can check the width # that the thumbnails will have and set the aspect ratio accordingly # as well as getting the framerate of the video: change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE) if Gst.StateChangeReturn.SUCCESS == change_return[0]: neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] else: # the pipeline couldn't be prerolled so we can't determine the # correct values. Set sane defaults (this should never happen) self.warning("Couldn't preroll the pipeline") # assume 16:9 aspect ratio self.thumb_width = 16 * self.thumb_height / 9 decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplugSelectCb) # pop all messages from the bus so we won't be flooded with messages # from the prerolling phase while self.pipeline.get_bus().pop(): continue # add a message handler that listens for the created pixbufs self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.__bus_message_handler) def _checkCPU(self): """Adjusts when the next thumbnail is generated. Checks the CPU usage and adjusts the waiting time at which the next thumbnail will be generated +/- 10%. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < THUMBNAILS_CPU_USAGE: self.interval *= 0.9 self.log( 'Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"', self.interval, filename_from_uri(self.uri)) else: self.interval *= 1.1 self.log( 'Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"', self.interval, filename_from_uri(self.uri)) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb, priority=GLib.PRIORITY_LOW) def _startThumbnailingWhenIdle(self): self.debug( 'Waiting for UI to become idle for: %s', filename_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return # self.props.width_request = self.nsToPixel(self.ges_elem.get_asset().get_filesource_asset().props.duration) # self.props.width = self.nsToPixel(self.ges_elem.get_asset().get_filesource_asset().props.duration) self.debug( 'Now generating thumbnails for: %s', filename_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.ges_elem.props.duration self.queue = list(range(0, duration, self.thumb_period)) self._checkCPU() if self.ges_elem.props.in_point != 0: adj = self.get_hadjustment() adj.props.page_size = 1.0 adj.props.value = Zoomable.nsToPixel(self.ges_elem.props.in_point) # self._addVisibleThumbnails() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = random.randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"' % filename_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True else: return False # Stop the timer def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't # smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _remove_all_children(self): for child in self.get_children(): self.remove(child) def _addVisibleThumbnails(self, rect): """Gets the thumbnails for the currently visible clip portion.""" if self.thumb_width is None: return False self.thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left = self.pixelToNs(rect.x) + self.ges_elem.props.in_point element_right = element_left + self.pixelToNs(rect.width) element_left = quantize(element_left, thumb_duration) for current_time in range(element_left, element_right, thumb_duration): thumb = Thumbnail(self.thumb_width, self.thumb_height) x = Zoomable.nsToPixel(current_time) - self.nsToPixel(self.ges_elem.props.in_point) y = (self.props.height_request - self.thumb_height) / 2 self.put(thumb, x, y) self.thumbs[current_time] = thumb if self.__image_pixbuf: thumb.set_from_pixbuf(self.__image_pixbuf) thumb.set_visible(True) elif current_time in self.thumb_cache: pixbuf = self.thumb_cache[current_time] thumb.set_from_pixbuf(pixbuf) thumb.set_visible(True) else: self.wishlist.append(current_time) return True def _get_wish(self): """Returns a wish that is also in the queue, if any.""" while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _setThumbnail(self, time, pixbuf): # Q: Is "time" guaranteed to be nanosecond precise? # A: Not always. # => __tim says: "that's how it should be" # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test # => Daniel: It is *not* nanosecond precise when we remove the videorate # element from the pipeline # => thiblahute: not the case with mpegts if time in self.thumbs: thumb = self.thumbs[time] else: sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, time) time = sorted_times[index] thumb = self.thumbs[time] thumb.set_from_pixbuf(pixbuf) if time in self.queue: self.queue.remove(time) self.thumb_cache[time] = pixbuf self.queue_draw() # Interface (Zoomable) def zoomChanged(self): self._remove_all_children() # Callbacks def __bus_message_handler(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._setThumbnail(stream_time, pixbuf) elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS # pylint: disable=no-self-use def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _heightChangedCb(self, unused_widget, unused_value): self._remove_all_children() def _inpointChangedCb(self, unused_b_element, unused_value): self.get_hadjustment().set_value(Zoomable.nsToPixel( self.ges_elem.props.in_point)) def setSelected(self, selected): if selected: opacity = 0.5 else: opacity = 1.0 for thumb in self.get_children(): thumb.props.opacity = opacity def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self.emit("done") def cleanup(self): """Stops preview generation and cleans the object.""" self.stopGeneration() Zoomable.__del__(self) # pylint: disable=arguments-differ def do_draw(self, context): res, rect = Gdk.cairo_get_clip_rectangle(context) assert res if self.__last_rectangle.x != rect.x or \ self.__last_rectangle.y != rect.y or \ self.__last_rectangle.width != rect.width or \ self.__last_rectangle.height != rect.height: if self._addVisibleThumbnails(rect): self.__last_rectangle = rect else: self.__last_rectangle = Gdk.Rectangle() Gtk.Layout.do_draw(self, context)
class VideoPreviewer(Previewer, Zoomable, Loggable): """A video previewer widget, drawing thumbnails. Attributes: ges_elem (GES.TrackElement): The previewed element. thumbs (dict): Maps (quantized) times to Thumbnail widgets. thumb_cache (ThumbnailCache): The pixmaps persistent cache. """ # We could define them in Previewer, but for some reason they are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, ges_elem, max_cpu_usage): Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage) Zoomable.__init__(self) Loggable.__init__(self) self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) self.__start_id = 0 self.__preroll_timeout_id = 0 self._thumb_cb_id = 0 # The thumbs to be generated. self.queue = [] # The position for which a thumbnail is currently being generated. self.position = -1 # The positions for which we failed to get a pixbuf. self.failures = set() self._thumb_cb_id = None self.thumbs = {} self.thumb_height = THUMB_HEIGHT self.thumb_width = 0 self.__image_pixbuf = None if not isinstance(ges_elem, GES.ImageSource): self.thumb_cache = ThumbnailCache.get(self.uri) self._ensure_proxy_thumbnails_cache() self.thumb_width, unused_height = self.thumb_cache.image_size self.pipeline = None self.gdkpixbufsink = None self.cpu_usage_tracker = CPUUsageTracker() # Initial delay before generating the next thumbnail, in millis. self.interval = 500 # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpoint_changed_cb) self.ges_elem.connect("notify::duration", self._duration_changed_cb) self.become_controlled() self.connect("notify::height-request", self._height_changed_cb) def pause_generation(self): if self.pipeline: self.pipeline.set_state(Gst.State.READY) def _setup_pipeline(self): """Creates the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ if self.pipeline: # Generation was just PAUSED... keep going # bringing the pipeline back to PAUSED. self.pipeline.set_state(Gst.State.PAUSED) return pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate={thumbs_per_second}/1 ! " "gdkpixbufsink name=gdkpixbufsink".format( uri=self.uri, height=self.thumb_height, thumbs_per_second=int(Gst.SECOND / THUMB_PERIOD))) # Get the gdkpixbufsink which contains the the sinkpad. self.gdkpixbufsink = pipeline.get_by_name("gdkpixbufsink") decode = pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplug_select_cb) self.__preroll_timeout_id = GLib.timeout_add_seconds( MAX_BRINGING_TO_PAUSED_DURATION, self.__preroll_timed_out_cb) pipeline.get_bus().add_signal_watch() pipeline.get_bus().connect("message", self.__bus_message_cb) pipeline.set_state(Gst.State.PAUSED) self.pipeline = pipeline def _schedule_next_thumb_generation(self): """Schedules the generation of the next thumbnail, or stop. Checks the CPU usage and adjusts the waiting time at which the next thumbnail will be generated +/- 10%. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ if self._thumb_cb_id is not None: # A thumb has already been scheduled. return if not self.queue: # Nothing left to do. self.debug("Thumbnails generation complete") self.stop_generation() return usage_percent = self.cpu_usage_tracker.usage() if usage_percent < self._max_cpu_usage: self.interval *= 0.9 self.log("Thumbnailing sped up to a %.1f ms interval for `%s`", self.interval, path_from_uri(self.uri)) else: self.interval *= 1.1 self.log("Thumbnailing slowed down to a %.1f ms interval for `%s`", self.interval, path_from_uri(self.uri)) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb_cb, priority=GLib.PRIORITY_LOW) def _start_thumbnailing_cb(self): if not self.__start_id: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return False self.__start_id = None if isinstance(self.ges_elem, GES.ImageSource): self.debug("Generating thumbnail for image: %s", path_from_uri(self.uri)) self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) self.thumb_width = self.__image_pixbuf.props.width self._update_thumbnails() self.emit("done") else: if not self.thumb_width: self.debug("Finding thumb width") self._setup_pipeline() return False # Update the thumbnails with what we already have, if anything. self._update_thumbnails() if self.queue: self.debug("Generating thumbnails for video: %s, %s", path_from_uri(self.uri), self.queue) # When the pipeline status is set to PAUSED, # the first thumbnail generation will be scheduled. self._setup_pipeline() else: self.emit("done") # Stop calling me, I started already. return False def _create_next_thumb_cb(self): """Creates a missing thumbnail.""" self._thumb_cb_id = None try: self.position = self.queue.pop(0) except IndexError: # The queue is empty. Can happen if _update_thumbnails # has been called in the meanwhile. self.stop_generation() return False self.log("Creating thumb at %s", self.position) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, self.position, Gst.SeekType.NONE, -1) # Stop calling me. # The seek operation will generate an ASYNC_DONE message on the bus, # and then the next thumbnail generation operation will be scheduled. return False @property def thumb_interval(self): """Gets the interval for which a thumbnail is displayed. Returns: int: a duration in nanos, multiple of THUMB_PERIOD. """ interval = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # Make sure the thumb interval is a multiple of THUMB_PERIOD. quantized = quantize(interval, THUMB_PERIOD) # Make sure the quantized thumb interval fits # the thumb and the margin. if quantized < interval: quantized += THUMB_PERIOD # Make sure we don't show thumbs more often than THUMB_PERIOD. return max(THUMB_PERIOD, quantized) def _update_thumbnails(self): """Updates the thumbnail widgets for the clip at the current zoom.""" if not self.thumb_width: # The thumb_width will be available when pipeline has been started # or the __image_pixbuf is ready. return thumbs = {} queue = [] interval = self.thumb_interval element_left = quantize(self.ges_elem.props.in_point, interval) element_right = self.ges_elem.props.in_point + self.ges_elem.props.duration y = (self.props.height_request - self.thumb_height) / 2 for position in range(element_left, element_right, interval): x = Zoomable.nsToPixel(position) - self.nsToPixel( self.ges_elem.props.in_point) try: thumb = self.thumbs.pop(position) self.move(thumb, x, y) except KeyError: thumb = Thumbnail(self.thumb_width, self.thumb_height) self.put(thumb, x, y) thumbs[position] = thumb if isinstance(self.ges_elem, GES.ImageSource): thumb.set_from_pixbuf(self.__image_pixbuf) thumb.set_visible(True) elif position in self.thumb_cache: pixbuf = self.thumb_cache[position] thumb.set_from_pixbuf(pixbuf) thumb.set_visible(True) else: if position not in self.failures and position != self.position: queue.append(position) for thumb in self.thumbs.values(): self.remove(thumb) self.thumbs = thumbs self.queue = queue if queue: self.become_controlled() def _set_pixbuf(self, pixbuf): """Sets the pixbuf for the thumbnail at the expected position.""" position = self.position self.position = -1 try: thumb = self.thumbs[position] except KeyError: # Can happen because we don't stop the pipeline before # updating the thumbnails in _update_thumbnails. return thumb.set_from_pixbuf(pixbuf) self.thumb_cache[position] = pixbuf self.queue_draw() def zoomChanged(self): self._update_thumbnails() def __bus_message_cb(self, unused_bus, message): if message.src == self.pipeline and \ message.type == Gst.MessageType.STATE_CHANGED: if message.parse_state_changed()[1] == Gst.State.PAUSED: # The pipeline is ready to be used. if self.__preroll_timeout_id: GLib.source_remove(self.__preroll_timeout_id) self.__preroll_timeout_id = 0 sinkpad = self.gdkpixbufsink.get_static_pad("sink") neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] self._update_thumbnails() elif message.src == self.gdkpixbufsink and \ message.type == Gst.MessageType.ELEMENT and \ self.__preroll_timeout_id == 0: # We got a thumbnail pixbuf. struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": pixbuf = struct.get_value("pixbuf") self._set_pixbuf(pixbuf) elif message.src == self.pipeline and \ message.type == Gst.MessageType.ASYNC_DONE: if self.position >= 0: self.warning("Thumbnail generation failed at %s", self.position) self.failures.add(self.position) self.position = -1 self._schedule_next_thumb_generation() return Gst.BusSyncReply.PASS def __preroll_timed_out_cb(self): self.stop_generation() # pylint: disable=no-self-use def _autoplug_select_cb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _height_changed_cb(self, unused_widget, unused_param_spec): self._update_thumbnails() def _inpoint_changed_cb(self, unused_ges_timeline_element, unused_param_spec): """Handles the changing of the in-point of the clip.""" self._update_thumbnails() def _duration_changed_cb(self, unused_ges_timeline_element, unused_param_spec): """Handles the changing of the duration of the clip.""" self._update_thumbnails() def set_selected(self, selected): if selected: opacity = 0.5 else: opacity = 1.0 for thumb in self.get_children(): thumb.props.opacity = opacity def start_generation(self): self.debug("Waiting for UI to become idle for: %s", path_from_uri(self.uri)) self.__start_id = GLib.idle_add(self._start_thumbnailing_cb, priority=GLib.PRIORITY_LOW) def _ensure_proxy_thumbnails_cache(self): """Ensures that both the target asset and the proxy assets have caches.""" uri = quote_uri(self.ges_elem.props.uri) if self.uri != uri: self.thumb_cache.copy(uri) def stop_generation(self): if self.__start_id: # Cancel the starting. GLib.source_remove(self.__start_id) self.__start_id = None if self.__preroll_timeout_id: # Stop waiting for the pipeline to be ready. GLib.source_remove(self.__preroll_timeout_id) self.__preroll_timeout_id = None if self._thumb_cb_id: # Cancel the thumbnailing. GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.get_bus().remove_signal_watch() self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self._ensure_proxy_thumbnails_cache() self.emit("done") def release(self): """Stops preview generation and cleans the object.""" self.stop_generation() Zoomable.__del__(self)
class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable): # We could define them in PreviewGenerator, but then for some reason they # are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, bElement, timeline): """ @param bElement : the backend GES.TrackElement @param track : the track to which the bElement belongs @param timeline : the containing graphic timeline. """ Clutter.ScrollActor.__init__(self) PreviewGenerator.__init__(self, GES.TrackType.VIDEO) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = timeline self.bElement = bElement self.uri = quote_uri(bElement.props.uri) # Guard against malformed URIs self.duration = bElement.props.duration # Variables related to thumbnailing self.wishlist = [] self._thumb_cb_id = None self._allAnimated = False self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX self.thumb_width = None # will be set by self._setupPipeline() # Maps (quantized) times to Thumbnail objects self.thumbs = {} self.thumb_cache = get_cache_for_uri(self.uri) self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.timeline.connect("scrolled", self._scrollCb) self.bElement.connect("notify::duration", self._durationChangedCb) self.bElement.connect("notify::in-point", self._inpointChangedCb) self.bElement.connect("notify::start", self._startChangedCb) self.pipeline = None self.becomeControlled() # Internal API def _update(self, unused_msg_source=None): if self.thumb_width: self._addVisibleThumbnails() if self.wishlist: self.becomeControlled() def _setupPipeline(self): """ Create the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") sinkpad = self.gdkpixbufsink.get_static_pad("sink") self.pipeline.set_state(Gst.State.PAUSED) # Wait for the pipeline to be prerolled so we can check the width # that the thumbnails will have and set the aspect ratio accordingly # as well as getting the framerate of the video: change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE) if Gst.StateChangeReturn.SUCCESS == change_return[0]: neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] else: # the pipeline couldn't be prerolled so we can't determine the # correct values. Set sane defaults (this should never happen) self.warning("Couldn't preroll the pipeline") # assume 16:9 aspect ratio self.thumb_width = 16 * self.thumb_height / 9 decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplugSelectCb) # pop all messages from the bus so we won't be flooded with messages # from the prerolling phase while self.pipeline.get_bus().pop(): continue # add a message handler that listens for the created pixbufs self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.bus_message_handler) def _checkCPU(self): """ Check the CPU usage and adjust the time interval (+10 or -10%) at which the next thumbnail will be generated. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < THUMBNAILS_CPU_USAGE: self.interval *= 0.9 self.log('Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri))) else: self.interval *= 1.1 self.log('Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"' % (self.interval, filename_from_uri(self.uri))) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb) def _startThumbnailingWhenIdle(self): self.debug('Waiting for UI to become idle for: %s', filename_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return self.debug('Now generating thumbnails for: %s', filename_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.duration else: self.duration = duration self.queue = list(range(0, duration, self.thumb_period)) self._checkCPU() self._addVisibleThumbnails() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"' % filename_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True else: return False # Stop the timer def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _addVisibleThumbnails(self): """ Get the thumbnails to be displayed in the currently visible clip portion """ self.remove_all_children() old_thumbs = self.thumbs self.thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left, element_right = self._get_visible_range() element_left = quantize(element_left, thumb_duration) for current_time in range(element_left, element_right, thumb_duration): thumb = Thumbnail(self.thumb_width, self.thumb_height) thumb.set_position(Zoomable.nsToPixel(current_time), THUMB_MARGIN_PX) self.add_child(thumb) self.thumbs[current_time] = thumb if current_time in self.thumb_cache: gdkpixbuf = self.thumb_cache[current_time] if self._allAnimated or current_time not in old_thumbs: self.thumbs[current_time].set_from_gdkpixbuf_animated(gdkpixbuf) else: self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf) else: self.wishlist.append(current_time) self._allAnimated = False def _get_wish(self): """ Returns a wish that is also in the queue, or None if no such wish exists """ while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _setThumbnail(self, time, pixbuf): # Q: Is "time" guaranteed to be nanosecond precise? # A: Not always. # => __tim says: "that's how it should be" # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test # => Daniel: It is *not* nanosecond precise when we remove the videorate # element from the pipeline # => thiblahute: not the case with mpegts original_time = time if time in self.thumbs: thumb = self.thumbs[time] else: sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, time) time = sorted_times[index] thumb = self.thumbs[time] if thumb.has_pixel_data: # If this happens, it means the precision of the thumbnail # generator is not good enough for the current thumbnail # interval. # We could consider shifting the thumbnails, but seems like # too much trouble for something which does not happen in # practice. My last words.. self.fixme("Thumbnail is already set for time: %s, %s", format_ns(time), format_ns(original_time)) return thumb.set_from_gdkpixbuf_animated(pixbuf) if time in self.queue: self.queue.remove(time) self.thumb_cache[time] = pixbuf # Interface (Zoomable) def zoomChanged(self): self.remove_all_children() self._allAnimated = True self._update() def _get_visible_range(self): # Shortcut/convenience variables: start = self.bElement.props.start in_point = self.bElement.props.in_point duration = self.bElement.props.duration timeline_left, timeline_right = self._get_visible_timeline_range() element_left = timeline_left - start + in_point element_left = max(element_left, in_point) element_right = timeline_right - start + in_point element_right = min(element_right, in_point + duration) return (element_left, element_right) # TODO: move to Timeline or to utils def _get_visible_timeline_range(self): # determine the visible left edge of the timeline # TODO: isn't there some easier way to get the scroll point of the ScrollActor? # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x) timeline_left = self.timeline.get_scroll_point().x # determine the width of the pipeline # by intersecting the timeline's and the stage's allocation timeline_allocation = self.timeline.props.allocation stage_allocation = self.timeline.get_stage().props.allocation timeline_rect = Clutter.Rect() timeline_rect.init(timeline_allocation.x1, timeline_allocation.y1, timeline_allocation.x2 - timeline_allocation.x1, timeline_allocation.y2 - timeline_allocation.y1) stage_rect = Clutter.Rect() stage_rect.init(stage_allocation.x1, stage_allocation.y1, stage_allocation.x2 - stage_allocation.x1, stage_allocation.y2 - stage_allocation.y1) has_intersection, intersection = timeline_rect.intersection(stage_rect) if not has_intersection: return (0, 0) timeline_width = intersection.size.width # determine the visible right edge of the timeline timeline_right = timeline_left + timeline_width # convert to nanoseconds time_left = Zoomable.pixelToNs(timeline_left) time_right = Zoomable.pixelToNs(timeline_right) return (time_left, time_right) # Callbacks def bus_message_handler(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._setThumbnail(stream_time, pixbuf) elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS def _autoplugSelectCb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _scrollCb(self, unused): self._update() def _startChangedCb(self, unused_bElement, unused_value): self._update() def _inpointChangedCb(self, unused_bElement, unused_value): position = Clutter.Point() position.x = Zoomable.nsToPixel(self.bElement.props.in_point) self.scroll_to_point(position) self._update() def _durationChangedCb(self, unused_bElement, unused_value): new_duration = max(self.duration, self.bElement.props.duration) if new_duration > self.duration: self.duration = new_duration self._update() def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self.emit("done") def cleanup(self): self.stopGeneration() Zoomable.__del__(self)
class VideoPreviewer(Previewer, Zoomable, Loggable): """A video previewer widget, drawing thumbnails. Attributes: ges_elem (GES.TrackElement): The previewed element. thumbs (dict): Maps (quantized) times to Thumbnail objects. thumb_cache (ThumbnailCache): The pixmaps persistent cache. """ # We could define them in Previewer, but for some reason they are ignored. __gsignals__ = PREVIEW_GENERATOR_SIGNALS def __init__(self, ges_elem, max_cpu_usage): Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage) Zoomable.__init__(self) Loggable.__init__(self) # Variables related to the timeline objects self.timeline = ges_elem.get_parent().get_timeline().ui self.ges_elem = ges_elem # Guard against malformed URIs self.uri = quote_uri(get_proxy_target(ges_elem).props.id) self.__preroll_timeout_id = 0 # Variables related to thumbnailing self.wishlist = [] self.queue = [] self._thumb_cb_id = None self._running = False # We should have one thumbnail per thumb_period. # TODO: get this from the user settings self.thumb_period = int(0.5 * Gst.SECOND) self.thumb_height = THUMB_HEIGHT self.__image_pixbuf = None if isinstance(ges_elem, GES.ImageSource): self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( Gst.uri_get_location(self.uri), -1, self.thumb_height, True) self.thumbs = {} self.thumb_cache = ThumbnailCache.get(self.uri) self._ensure_proxy_thumbnails_cache() self.thumb_width, unused_height = self.thumb_cache.getImagesSize() self.cpu_usage_tracker = CPUUsageTracker() self.interval = 500 # Every 0.5 second, reevaluate the situation # Connect signals and fire things up self.ges_elem.connect("notify::in-point", self._inpoint_changed_cb) self.pipeline = None self.gdkpixbufsink = None self.becomeControlled() self.connect("notify::height-request", self._height_changed_cb) # Internal API def _setupPipeline(self): """Creates the pipeline. It has the form "playbin ! thumbnailsink" where thumbnailsink is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink" """ # TODO: don't hardcode framerate self.pipeline = Gst.parse_launch( "uridecodebin uri={uri} name=decode ! " "videoconvert ! " "videorate ! " "videoscale method=lanczos ! " "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height}," "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! " "gdkpixbufsink name=gdkpixbufsink".format( uri=self.uri, height=self.thumb_height)) # get the gdkpixbufsink and the sinkpad self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink") decode = self.pipeline.get_by_name("decode") decode.connect("autoplug-select", self._autoplug_select_cb) self.__preroll_timeout_id = GLib.timeout_add_seconds( MAX_BRINGING_TO_PAUSED_DURATION, self.__preroll_timed_out_cb) self.pipeline.get_bus().add_signal_watch() self.pipeline.get_bus().connect("message", self.__bus_message_cb) self.pipeline.set_state(Gst.State.PAUSED) def _checkCPU(self): """Adjusts when the next thumbnail is generated. Checks the CPU usage and adjusts the waiting time at which the next thumbnail will be generated +/- 10%. Even then, it will only happen when the gobject loop is idle to avoid blocking the UI. """ usage_percent = self.cpu_usage_tracker.usage() if usage_percent < self._max_cpu_usage: self.interval *= 0.9 self.log( 'Thumbnailing sped up (+10%%) to a %.1f ms interval for "%s"', self.interval, path_from_uri(self.uri)) else: self.interval *= 1.1 self.log( 'Thumbnailing slowed down (-10%%) to a %.1f ms interval for "%s"', self.interval, path_from_uri(self.uri)) self.cpu_usage_tracker.reset() self._thumb_cb_id = GLib.timeout_add(self.interval, self._create_next_thumb, priority=GLib.PRIORITY_LOW) def _startThumbnailingWhenIdle(self): self.debug('Waiting for UI to become idle for: %s', path_from_uri(self.uri)) GLib.idle_add(self._startThumbnailing, priority=GLib.PRIORITY_LOW) def _startThumbnailing(self): if not self.pipeline: # Can happen if stopGeneration is called because the clip has been # removed from the timeline after the PreviewGeneratorManager # started this job. return self.debug('Now generating thumbnails for: %s', path_from_uri(self.uri)) query_success, duration = self.pipeline.query_duration(Gst.Format.TIME) if not query_success or duration == -1: self.debug("Could not determine duration of: %s", self.uri) duration = self.ges_elem.props.duration self.queue = list(range(0, duration, self.thumb_period)) self._checkCPU() # Save periodically to avoid the common situation where the user exits # the app before a long clip has been fully thumbnailed. # Spread timeouts between 30-80 secs to avoid concurrent disk writes. random_time = random.randrange(30, 80) GLib.timeout_add_seconds(random_time, self._autosave) # Remove the GSource return False def _create_next_thumb(self): if not self.wishlist or not self.queue: # nothing left to do self.debug("Thumbnails generation complete") self.stopGeneration() self.thumb_cache.commit() return else: self.debug("Missing %d thumbs", len(self.wishlist)) wish = self._get_wish() if wish: time = wish self.queue.remove(wish) else: time = self.queue.pop(0) self.log('Creating thumb for "%s"', path_from_uri(self.uri)) # append the time to the end of the queue so that if this seek fails # another try will be started later self.queue.append(time) self.pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, Gst.SeekType.SET, time, Gst.SeekType.NONE, -1) # Remove the GSource self._thumb_cb_id = None return False def _autosave(self): if self.wishlist: self.log("Periodic thumbnail autosave") self.thumb_cache.commit() return True # Stop the timer return False def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't # smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period) def _update_thumbnails(self): """Updates the thumbnails for the currently visible clip portion.""" if self.thumb_width is None: return False thumbs = {} self.wishlist = [] thumb_duration = self._get_thumb_duration() element_left = quantize(self.ges_elem.props.in_point, thumb_duration) element_right = self.ges_elem.props.in_point + self.ges_elem.props.duration for position in range(element_left, element_right, thumb_duration): x = Zoomable.nsToPixel(position) - self.nsToPixel( self.ges_elem.props.in_point) y = (self.props.height_request - self.thumb_height) / 2 try: thumb = self.thumbs.pop(position) self.move(thumb, x, y) except KeyError: thumb = Thumbnail(self.thumb_width, self.thumb_height) self.put(thumb, x, y) thumbs[position] = thumb if self.__image_pixbuf: # The thumbnail is fixed, probably it's an image clip. thumb.set_from_pixbuf(self.__image_pixbuf) thumb.set_visible(True) elif position in self.thumb_cache: pixbuf = self.thumb_cache[position] thumb.set_from_pixbuf(pixbuf) thumb.set_visible(True) else: self.wishlist.append(position) for thumb in self.thumbs.values(): self.remove(thumb) self.thumbs = thumbs return True def _get_wish(self): """Returns a wish that is also in the queue, if any.""" while True: if not self.wishlist: return None wish = self.wishlist.pop(0) if wish in self.queue: return wish def _set_pixbuf(self, position, pixbuf): """Sets the pixbuf for the thumbnail at the specified position.""" if position in self.thumbs: thumb = self.thumbs[position] else: # The pixbufs we get from gdkpixbufsink are not always # exactly the ones requested, the reported position can differ. # Try to find the closest thumbnail for the specified position. sorted_times = sorted(self.thumbs.keys()) index = binary_search(sorted_times, position) position = sorted_times[index] thumb = self.thumbs[position] thumb.set_from_pixbuf(pixbuf) if position in self.queue: self.queue.remove(position) self.thumb_cache[position] = pixbuf self.queue_draw() # Interface (Zoomable) def zoomChanged(self): self._update_thumbnails() # Callbacks def __bus_message_cb(self, unused_bus, message): if message.type == Gst.MessageType.ELEMENT and \ message.src == self.gdkpixbufsink and \ self.__preroll_timeout_id == 0: struct = message.get_structure() struct_name = struct.get_name() if struct_name == "preroll-pixbuf": stream_time = struct.get_value("stream-time") pixbuf = struct.get_value("pixbuf") self._set_pixbuf(stream_time, pixbuf) elif message.src == self.pipeline and message.type == Gst.MessageType.STATE_CHANGED: if message.parse_state_changed()[1] == Gst.State.PAUSED: if self.__preroll_timeout_id: GLib.source_remove(self.__preroll_timeout_id) self.__preroll_timeout_id = 0 sinkpad = self.gdkpixbufsink.get_static_pad("sink") neg_caps = sinkpad.get_current_caps()[0] self.thumb_width = neg_caps["width"] self._update_thumbnails() elif message.type == Gst.MessageType.ASYNC_DONE and \ message.src == self.pipeline: self._checkCPU() return Gst.BusSyncReply.PASS def __preroll_timed_out_cb(self): self.stopGeneration() # pylint: disable=no-self-use def _autoplug_select_cb(self, unused_decode, unused_pad, unused_caps, factory): # Don't plug audio decoders / parsers. if "Audio" in factory.get_klass(): return True return False def _height_changed_cb(self, unused_widget, unused_param_spec): self._update_thumbnails() def _inpoint_changed_cb(self, unused_ges_timeline_element, unused_param_spec): self._update_thumbnails() def setSelected(self, selected): if selected: opacity = 0.5 else: opacity = 1.0 for thumb in self.get_children(): thumb.props.opacity = opacity def startGeneration(self): self._setupPipeline() self._startThumbnailingWhenIdle() def _ensure_proxy_thumbnails_cache(self): """Ensures that both the target asset and the proxy assets have caches.""" uri = quote_uri(self.ges_elem.props.uri) if self.uri != uri: self.thumb_cache.copy(uri) def stopGeneration(self): if self._thumb_cb_id: GLib.source_remove(self._thumb_cb_id) self._thumb_cb_id = None if self.pipeline: self.pipeline.get_bus().remove_signal_watch() self.pipeline.set_state(Gst.State.NULL) self.pipeline.get_state(Gst.CLOCK_TIME_NONE) self.pipeline = None self._ensure_proxy_thumbnails_cache() self.emit("done") def release(self): """Stops preview generation and cleans the object.""" self.stopGeneration() Zoomable.__del__(self)