Commit 7473ce51 authored by Swayamjeet Swain's avatar Swayamjeet Swain 💬 Committed by Alexandru Băluț

previewer: Allow generating thumbnails for assets

Fixes #2332
parent c4e657da
Pipeline #103133 failed with stages
in 26 seconds
......@@ -42,7 +42,7 @@ from pitivi.dialogs.clipmediaprops import ClipMediaPropsDialog
from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
from pitivi.mediafilespreviewer import PreviewWidget
from pitivi.settings import GlobalSettings
from pitivi.timeline.previewers import ThumbnailCache
from pitivi.timeline.previewers import AssetPreviewer
from pitivi.utils.loggable import Loggable
from pitivi.utils.misc import disconnectAllByFunc
from pitivi.utils.misc import path_from_uri
......@@ -168,9 +168,13 @@ class FileChooserExtraWidget(Gtk.Grid, Loggable):
self.app.settings.proxyingStrategy = ProxyingStrategy.AUTOMATIC
class AssetThumbnail(Loggable):
class AssetThumbnail(GObject.Object, Loggable):
"""Provider of decorated thumbnails for an asset."""
__gsignals__ = {
"thumb-updated": (GObject.SignalFlags.RUN_LAST, None, ()),
}
EMBLEMS = {}
PROXIED = "asset-proxied"
NO_PROXY = "no-proxy"
......@@ -187,10 +191,15 @@ class AssetThumbnail(Loggable):
os.path.join(get_pixmap_dir(), "%s.svg" % status), 64, 64)
def __init__(self, asset, proxy_manager):
GObject.Object.__init__(self)
Loggable.__init__(self)
self.__asset = asset
self.src_small, self.src_large = self.__get_thumbnails()
self.proxy_manager = proxy_manager
self.previewer = None
self.refresh()
def refresh(self):
self.src_small, self.src_large = self.__get_thumbnails()
self.decorate()
def __get_thumbnails(self):
......@@ -228,10 +237,14 @@ class AssetThumbnail(Loggable):
small_thumb, large_thumb = self.__get_icons("image-x-generic")
else:
# Build or reuse a ThumbnailCache.
thumb_cache = ThumbnailCache.get(self.__asset)
small_thumb = thumb_cache.get_preview_thumbnail()
previewer = AssetPreviewer(self.__asset, 90)
small_thumb = previewer.thumb_cache.get_preview_thumbnail()
if not small_thumb:
small_thumb, large_thumb = self.__get_icons("video-x-generic")
# Only try once to generate the thumbnail.
if not self.previewer:
self.previewer = previewer
previewer.connect("done", self.__done_cb)
else:
width = small_thumb.props.width
height = small_thumb.props.height
......@@ -248,6 +261,11 @@ class AssetThumbnail(Loggable):
small_thumb, large_thumb = self.__get_icons("audio-x-generic")
return small_thumb, large_thumb
def __done_cb(self, unused_asset_previewer):
"""Handles the done signal of our AssetPreviewer."""
self.refresh()
self.emit("thumb-updated")
@staticmethod
def get_asset_thumbnails_path(real_uri):
"""Gets normal & large thumbnail path for the asset in the XDG cache.
......@@ -794,8 +812,6 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
dialog.show()
def _addAsset(self, asset):
info = asset.get_info()
if self.app.proxy_manager.is_proxy_asset(asset) and \
not asset.props.proxy_target:
self.info("%s is a proxy asset but has no target, "
......@@ -823,8 +839,28 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
name,
thumbs_decorator))
thumbs_decorator.connect("thumb-updated", self.__thumb_updated_cb, asset)
del self._pending_assets[:]
def __thumb_updated_cb(self, asset_thumbnail, asset):
"""Handles the thumb-updated signal of the AssetThumbnails in the model."""
tree_iter = None
for row in self.storemodel:
if asset == row[COL_ASSET]:
tree_iter = row.iter
break
if not tree_iter:
return
self.storemodel.set_value(tree_iter,
COL_ICON_64,
asset_thumbnail.small_thumb)
self.storemodel.set_value(tree_iter,
COL_ICON_128,
asset_thumbnail.large_thumb)
# medialibrary callbacks
def _assetLoadingProgressCb(self, project, progress, estimated_time):
......@@ -838,10 +874,10 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
if not asset.ready:
proxying_files.append(asset)
if row[COL_THUMB_DECORATOR].state != AssetThumbnail.IN_PROGRESS:
thumbs_decorator = AssetThumbnail(asset, self.app.proxy_manager)
row[COL_ICON_64] = thumbs_decorator.small_thumb
row[COL_ICON_128] = thumbs_decorator.large_thumb
row[COL_THUMB_DECORATOR] = thumbs_decorator
asset_previewer = row[COL_THUMB_DECORATOR]
asset_previewer.refresh()
row[COL_ICON_64] = asset_previewer.small_thumb
row[COL_ICON_128] = asset_previewer.large_thumb
if progress == 0:
self._startImporting(project)
......
......@@ -45,6 +45,7 @@ from pitivi.preset import VideoPresetManager
from pitivi.render import Encoders
from pitivi.settings import get_dir
from pitivi.settings import xdg_cache_home
from pitivi.timeline.previewers import Previewer
from pitivi.timeline.previewers import ThumbnailCache
from pitivi.undo.project import AssetAddedIntention
from pitivi.undo.project import AssetProxiedIntention
......@@ -489,26 +490,27 @@ class ProjectManager(GObject.Object, Loggable):
"Could not close project - this could be because there were unsaved changes and the user cancelled when prompted about them")
return False
self.current_project.finalize()
with Previewer.manager.paused(interrupt=True):
self.current_project.finalize()
project = self.current_project
self.current_project = None
project.create_thumb()
self.emit("project-closed", project)
# We should never choke on silly stuff like disconnecting signals
# that were already disconnected. It blocks the UI for nothing.
# This can easily happen when a project load/creation failed.
try:
project.disconnect_by_function(self._projectChangedCb)
except Exception:
self.debug(
"Tried disconnecting signals, but they were not connected")
try:
project.pipeline.disconnect_by_function(self._projectPipelineDiedCb)
except Exception:
self.fixme("Handle better the errors and not get to this point")
self._cleanBackup(project.uri)
self.exitcode = project.release()
project = self.current_project
self.current_project = None
project.create_thumb()
self.emit("project-closed", project)
# We should never choke on silly stuff like disconnecting signals
# that were already disconnected. It blocks the UI for nothing.
# This can easily happen when a project load/creation failed.
try:
project.disconnect_by_function(self._projectChangedCb)
except Exception:
self.debug(
"Tried disconnecting signals, but they were not connected")
try:
project.pipeline.disconnect_by_function(self._projectPipelineDiedCb)
except Exception:
self.fixme("Handle better the errors and not get to this point")
self._cleanBackup(project.uri)
self.exitcode = project.release()
return True
......
......@@ -378,7 +378,7 @@ class PreviewGeneratorManager(Loggable):
self._start_previewer(self._previewers[track_type].pop())
class Previewer(Gtk.Layout):
class Previewer(GObject.Object):
"""Base class for previewers.
Attributes:
......@@ -389,8 +389,7 @@ class Previewer(Gtk.Layout):
manager = PreviewGeneratorManager()
def __init__(self, track_type, max_cpu_usage):
Gtk.Layout.__init__(self)
GObject.Object.__init__(self)
self.track_type = track_type
self._max_cpu_usage = max_cpu_usage
......@@ -430,13 +429,14 @@ class Previewer(Gtk.Layout):
return max(THUMB_PERIOD, quantized)
class ImagePreviewer(Previewer, Zoomable, Loggable):
class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
"""An image previewer widget, drawing thumbnails."""
# 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):
Gtk.Layout.__init__(self)
Previewer.__init__(self, GES.TrackType.VIDEO, max_cpu_usage)
Zoomable.__init__(self)
Loggable.__init__(self)
......@@ -546,31 +546,27 @@ class ImagePreviewer(Previewer, Zoomable, Loggable):
Zoomable.__del__(self)
class VideoPreviewer(Previewer, Zoomable, Loggable):
"""A video previewer widget, drawing thumbnails.
class AssetPreviewer(Previewer, Loggable):
"""Previewer for creating thumbnails for a video asset.
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):
def __init__(self, asset, 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.asset = asset
self.uri = quote_uri(asset.props.id)
self.__start_id = 0
self.__preroll_timeout_id = 0
self._thumb_cb_id = 0
self.__thumb_cb_id = 0
# The thumbs to be generated.
self.queue = []
......@@ -578,14 +574,12 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
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.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
......@@ -594,17 +588,22 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
# 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 _update_thumbnails(self):
"""Updates the queue of thumbnails to be produced.
def pause_generation(self):
if self.pipeline:
self.pipeline.set_state(Gst.State.READY)
Subclasses can also update the managed UI, if any.
The contract is that if the method sets a queue,
it also calls become_controlled().
"""
position = int(self.asset.get_duration() / 2)
if position in self.thumb_cache:
return
if position not in self.failures and position != self.position:
self.queue = [position]
self.become_controlled()
def _setup_pipeline(self):
"""Creates the pipeline.
......@@ -617,7 +616,6 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
# bringing the pipeline back to PAUSED.
self.pipeline.set_state(Gst.State.PAUSED)
return
pipeline = Gst.parse_launch(
"uridecodebin uri={uri} name=decode ! "
"videoconvert ! "
......@@ -650,7 +648,7 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
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:
if self.__thumb_cb_id:
# A thumb has already been scheduled.
return
......@@ -670,9 +668,9 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
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)
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:
......@@ -703,7 +701,7 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
def _create_next_thumb_cb(self):
"""Creates a missing thumbnail."""
self._thumb_cb_id = None
self.__thumb_cb_id = 0
try:
self.position = self.queue.pop(0)
......@@ -725,59 +723,14 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
# and then the next thumbnail generation operation will be scheduled.
return False
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
return
thumbs = {}
queue = []
interval = self.thumb_interval(self.thumb_width)
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 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 _set_pixbuf(self, pixbuf, position):
"""Updates the managed UI when a new pixbuf becomes available.
def zoomChanged(self):
self._update_thumbnails()
Args:
pixbuf (GdkPixbuf.Pixbuf): The pixbuf produced by self.pipeline.
position (int): The position for which the thumb has been created,
in nanoseconds.
"""
def __bus_message_cb(self, unused_bus, message):
if message.src == self.pipeline and \
......@@ -800,7 +753,9 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
struct_name = struct.get_name()
if struct_name == "preroll-pixbuf":
pixbuf = struct.get_value("pixbuf")
self._set_pixbuf(pixbuf)
self.thumb_cache[self.position] = pixbuf
self._set_pixbuf(pixbuf, self.position)
self.position = -1
elif message.src == self.pipeline and \
message.type == Gst.MessageType.ASYNC_DONE:
if self.position >= 0:
......@@ -820,38 +775,12 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
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.
......@@ -863,10 +792,10 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
GLib.source_remove(self.__preroll_timeout_id)
self.__preroll_timeout_id = None
if self._thumb_cb_id:
if self.__thumb_cb_id:
# Cancel the thumbnailing.
GLib.source_remove(self._thumb_cb_id)
self._thumb_cb_id = None
GLib.source_remove(self.__thumb_cb_id)
self.__thumb_cb_id = 0
if self.pipeline:
self.pipeline.get_bus().remove_signal_watch()
......@@ -874,14 +803,121 @@ class VideoPreviewer(Previewer, Zoomable, Loggable):
self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
self.pipeline = None
self._ensure_proxy_thumbnails_cache()
self.emit("done")
def pause_generation(self):
if self.pipeline:
self.pipeline.set_state(Gst.State.READY)
# pylint: disable=too-many-ancestors
class VideoPreviewer(Gtk.Layout, AssetPreviewer, Zoomable):
"""A video previewer widget, drawing thumbnails.
Attributes:
ges_elem (GES.VideoSource): The previewed element.
thumbs (dict): Maps (quantized) times to the managed Thumbnail widgets.
"""
# 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):
Gtk.Layout.__init__(self)
Zoomable.__init__(self)
AssetPreviewer.__init__(self, get_proxy_target(ges_elem), max_cpu_usage)
self.ges_elem = ges_elem
self.thumbs = {}
self._ensure_proxy_thumbnails_cache()
# 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.connect("notify::height-request", self._height_changed_cb)
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 _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
return
thumbs = {}
queue = []
interval = self.thumb_interval(self.thumb_width)
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 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 _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 _set_pixbuf(self, pixbuf, position):
"""Sets the pixbuf for the thumbnail at the expected position."""
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)
def release(self):
"""Stops preview generation and cleans the object."""
self.stop_generation()
Zoomable.__del__(self)
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 zoomChanged(self):
self._update_thumbnails()
class Thumbnail(Gtk.Image):
"""Simple widget representing a Thumbnail."""
......@@ -1053,12 +1089,13 @@ def get_wavefile_location_for_uri(uri):
return os.path.join(cache_dir, filename)
class AudioPreviewer(Previewer, Zoomable, Loggable):
class AudioPreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
"""Audio previewer using the results from the "level" GStreamer element."""
__gsignals__ = PREVIEW_GENERATOR_SIGNALS
def __init__(self, ges_elem, max_cpu_usage):
Gtk.Layout.__init__(self)
Previewer.__init__(self, GES.TrackType.AUDIO, max_cpu_usage)
Zoomable.__init__(self)
Loggable.__init__(self)
......
......@@ -446,26 +446,24 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
def setProject(self, project):
"""Connects to the GES.Timeline holding the project."""
# Avoid starting/closing preview generation like crazy while tearing down project
with Previewer.manager.paused(interrupt=True):
if self.ges_timeline is not None:
self.disconnect_by_func(self._button_press_event_cb)
self.disconnect_by_func(self._button_release_event_cb)
self.disconnect_by_func(self._motion_notify_event_cb)
self.ges_timeline.disconnect_by_func(self._durationChangedCb)
self.ges_timeline.disconnect_by_func(self._layer_added_cb)
self.ges_timeline.disconnect_by_func(self._layer_removed_cb)
self.ges_timeline.disconnect_by_func(self.__snapping_started_cb)
self.ges_timeline.disconnect_by_func(self.__snapping_ended_cb)
for ges_layer in self.ges_timeline.get_layers():
self._remove_layer(ges_layer)
self.ges_timeline.ui = None
self.ges_timeline = None
if self._project:
self._project.pipeline.disconnect_by_func(self._positionCb)
if self.ges_timeline is not None:
self.disconnect_by_func(self._button_press_event_cb)
self.disconnect_by_func(self._button_release_event_cb)
self.disconnect_by_func(self._motion_notify_event_cb)
self.ges_timeline.disconnect_by_func(self._durationChangedCb)
self.ges_timeline.disconnect_by_func(self._layer_added_cb)
self.ges_timeline.disconnect_by_func(self._layer_removed_cb)
self.ges_timeline.disconnect_by_func(self.__snapping_started_cb)
self.ges_timeline.disconnect_by_func(self.__snapping_ended_cb)
for ges_layer in self.ges_timeline.get_layers():
self._remove_layer(ges_layer)
self.ges_timeline.ui = None
self.ges_timeline = None
if self._project:
self._project.pipeline.disconnect_by_func(self._positionCb)
self._project = project
if self._project:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment