From 97af0de6c24ba4bfcf4678adf2b41230169ff3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hamal=20Dvo=C5=99=C3=A1k?= <mordae@anilinux.org> Date: Tue, 11 Mar 2025 11:16:36 +0100 Subject: [PATCH] Use deeper queue for thumbnails --- lazy_player/file_model.py | 9 --- lazy_player/main_window.py | 2 +- lazy_player/thumbnailer.py | 146 +++++++++++++++++++------------------ 3 files changed, 77 insertions(+), 80 deletions(-) diff --git a/lazy_player/file_model.py b/lazy_player/file_model.py index 7daeb16..3054a2c 100644 --- a/lazy_player/file_model.py +++ b/lazy_player/file_model.py @@ -8,8 +8,6 @@ from typing import Optional, overload from gi.repository import Gio, GObject -from .thumbnailer import Thumbnailer - class FileType(Enum): DIRECTORY = auto() @@ -80,13 +78,6 @@ class FileItem(GObject.Object): self._has_thumbnail = value self.notify("has-thumbnail") - def ensure_thumbnail(self, thumbnailer: Thumbnailer): - if self.thumbnail or self.attempted_thumbnail: - return - - if not self.attempted_thumbnail: - thumbnailer.generate_thumbnail(self) - @overload def _load_attribute(self, name: str, dfl: str) -> str: ... diff --git a/lazy_player/main_window.py b/lazy_player/main_window.py index c00161f..fb5ecf7 100644 --- a/lazy_player/main_window.py +++ b/lazy_player/main_window.py @@ -471,7 +471,7 @@ class MainWindow(Gtk.ApplicationWindow): # Update thumbnail if available if file_item := self.selection: - file_item.ensure_thumbnail(self.thumbnailer) + self.thumbnailer.generate_thumbnail(file_item) if file_item.thumbnail and not self.thumbnail_image.get_paintable(): gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail)) diff --git a/lazy_player/thumbnailer.py b/lazy_player/thumbnailer.py index fc222dc..866a549 100644 --- a/lazy_player/thumbnailer.py +++ b/lazy_player/thumbnailer.py @@ -1,7 +1,8 @@ from __future__ import annotations +import sys import threading -from queue import Empty, Queue +from queue import Empty, Full, LifoQueue from typing import TYPE_CHECKING from gi.repository import GLib, Gst @@ -18,13 +19,15 @@ DEFAULT_SEEK_FLAGS = ( | Gst.SeekFlags.TRICKMODE_NO_AUDIO ) +__all__ = ["Thumbnailer", "generate_thumbnail_sync"] + class Thumbnailer(threading.Thread): - queue: Queue[FileItem | None] + queue: LifoQueue[FileItem | None] def __init__(self): super().__init__(daemon=True) - self.queue = Queue(maxsize=1) + self.queue = LifoQueue(maxsize=20) def generate_thumbnail(self, file_item: FileItem): """Add a file item to the thumbnail queue""" @@ -32,20 +35,23 @@ class Thumbnailer(threading.Thread): if not file_item.full_path.is_file(): return - # Replace any pending item in the queue + if file_item.attempted_thumbnail: + return + try: - self.queue.get_nowait() - except Empty: + self.queue.put_nowait(file_item) + except Full: pass - self.queue.put_nowait(file_item) + file_item.attempted_thumbnail = True def stop(self): """Stop the thumbnailer thread""" - # Replace any pending items in the queue try: - self.queue.get_nowait() + # Drop all pending items + while True: + self.queue.get_nowait() except Empty: pass @@ -61,73 +67,73 @@ class Thumbnailer(threading.Thread): if file_item is None: break - file_item.attempted_thumbnail = True - self._generate_thumbnail(file_item) + generate_thumbnail_sync(file_item) - def _generate_thumbnail(self, file_item: FileItem): - """Generate thumbnail for a single file""" - pipeline_str = ( - "uridecodebin name=uridecodebin ! " - "videoconvert ! " - "jpegenc quality=85 ! " - "appsink sync=false name=sink" - ) +def generate_thumbnail_sync(file_item: FileItem): + """Generate thumbnail for a single file""" - pipeline = Gst.parse_launch(pipeline_str) - assert isinstance(pipeline, Gst.Pipeline) + pipeline_str = ( + "uridecodebin name=uridecodebin ! " + "videoconvert ! " + "jpegenc quality=85 ! " + "appsink sync=false name=sink" + ) - sink = pipeline.get_by_name("sink") - assert isinstance(sink, Gst.Element) + pipeline = Gst.parse_launch(pipeline_str) + assert isinstance(pipeline, Gst.Pipeline) - uridecodebin = pipeline.get_by_name("uridecodebin") - assert isinstance(uridecodebin, Gst.Element) + sink = pipeline.get_by_name("sink") + assert isinstance(sink, Gst.Element) - # Set file URI - uridecodebin.set_property("uri", Gst.filename_to_uri(str(file_item.full_path))) + uridecodebin = pipeline.get_by_name("uridecodebin") + assert isinstance(uridecodebin, Gst.Element) + + # Set file URI + uridecodebin.set_property("uri", Gst.filename_to_uri(str(file_item.full_path))) + + try: + # Set pipeline to PAUSED to get duration + pipeline.set_state(Gst.State.PAUSED) + pipeline.get_state(Gst.SECOND) + + # Seek to 1/3 of duration + success, duration = pipeline.query_duration(Gst.Format.TIME) + if not success: + return + + seek_pos = duration // 3 + pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, seek_pos) + + # Start playing to capture frame + pipeline.set_state(Gst.State.PLAYING) + + sample = sink.emit("pull-sample") + if not sample: + return + + # Extract image data + buffer = sample.get_buffer() + if not buffer: + return + + success, map_info = buffer.map(Gst.MapFlags.READ) + if not success: + return try: - # Set pipeline to PAUSED to get duration - pipeline.set_state(Gst.State.PAUSED) - pipeline.get_state(Gst.SECOND) - - # Seek to 1/3 of duration - success, duration = pipeline.query_duration(Gst.Format.TIME) - if not success: - return - - seek_pos = duration // 3 - pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, seek_pos) - - # Start playing to capture frame - pipeline.set_state(Gst.State.PLAYING) - - sample = sink.emit("pull-sample") - if not sample: - return - - # Extract image data - buffer = sample.get_buffer() - if not buffer: - return - - success, map_info = buffer.map(Gst.MapFlags.READ) - if not success: - return - - try: - thumbnail = bytes(map_info.data) - finally: - buffer.unmap(map_info) - - def set_thumbnail(): - file_item.thumbnail = thumbnail - file_item.has_thumbnail = True - - GLib.idle_add(set_thumbnail) - - except Exception as err: - print("Failed:", file_item.full_path, err) - + thumbnail = bytes(map_info.data) finally: - pipeline.set_state(Gst.State.NULL) + buffer.unmap(map_info) + + def set_thumbnail(): + file_item.thumbnail = thumbnail + file_item.has_thumbnail = True + + GLib.idle_add(set_thumbnail) + + except Exception as err: + print("[thumbnailer] Error:", file_item.full_path, err, file=sys.stderr) + + finally: + pipeline.set_state(Gst.State.NULL)