示例#1
0
    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
示例#2
0
    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)
示例#3
0
    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)
示例#4
0
    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()
示例#5
0
    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
示例#6
0
    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)
示例#7
0
    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()
示例#8
0
    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)
示例#9
0
    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)
示例#10
0
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)
示例#11
0
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)
示例#12
0
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
示例#13
0
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)
示例#14
0
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)
示例#15
0
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)
示例#16
0
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
示例#17
0
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)
示例#18
0
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)