Use deeper queue for thumbnails

This commit is contained in:
Jan Hamal Dvořák 2025-03-11 11:16:36 +01:00
parent 6bd0bc62b9
commit 97af0de6c2
3 changed files with 77 additions and 80 deletions

View file

@ -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: ...

View file

@ -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))

View file

@ -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)