Use deeper queue for thumbnails
This commit is contained in:
parent
6bd0bc62b9
commit
97af0de6c2
3 changed files with 77 additions and 80 deletions
lazy_player
|
@ -8,8 +8,6 @@ from typing import Optional, overload
|
||||||
|
|
||||||
from gi.repository import Gio, GObject
|
from gi.repository import Gio, GObject
|
||||||
|
|
||||||
from .thumbnailer import Thumbnailer
|
|
||||||
|
|
||||||
|
|
||||||
class FileType(Enum):
|
class FileType(Enum):
|
||||||
DIRECTORY = auto()
|
DIRECTORY = auto()
|
||||||
|
@ -80,13 +78,6 @@ class FileItem(GObject.Object):
|
||||||
self._has_thumbnail = value
|
self._has_thumbnail = value
|
||||||
self.notify("has-thumbnail")
|
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
|
@overload
|
||||||
def _load_attribute(self, name: str, dfl: str) -> str: ...
|
def _load_attribute(self, name: str, dfl: str) -> str: ...
|
||||||
|
|
||||||
|
|
|
@ -471,7 +471,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||||
|
|
||||||
# Update thumbnail if available
|
# Update thumbnail if available
|
||||||
if file_item := self.selection:
|
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():
|
if file_item.thumbnail and not self.thumbnail_image.get_paintable():
|
||||||
gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
|
gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from queue import Empty, Queue
|
from queue import Empty, Full, LifoQueue
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from gi.repository import GLib, Gst
|
from gi.repository import GLib, Gst
|
||||||
|
@ -18,13 +19,15 @@ DEFAULT_SEEK_FLAGS = (
|
||||||
| Gst.SeekFlags.TRICKMODE_NO_AUDIO
|
| Gst.SeekFlags.TRICKMODE_NO_AUDIO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = ["Thumbnailer", "generate_thumbnail_sync"]
|
||||||
|
|
||||||
|
|
||||||
class Thumbnailer(threading.Thread):
|
class Thumbnailer(threading.Thread):
|
||||||
queue: Queue[FileItem | None]
|
queue: LifoQueue[FileItem | None]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
self.queue = Queue(maxsize=1)
|
self.queue = LifoQueue(maxsize=20)
|
||||||
|
|
||||||
def generate_thumbnail(self, file_item: FileItem):
|
def generate_thumbnail(self, file_item: FileItem):
|
||||||
"""Add a file item to the thumbnail queue"""
|
"""Add a file item to the thumbnail queue"""
|
||||||
|
@ -32,20 +35,23 @@ class Thumbnailer(threading.Thread):
|
||||||
if not file_item.full_path.is_file():
|
if not file_item.full_path.is_file():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Replace any pending item in the queue
|
if file_item.attempted_thumbnail:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.queue.get_nowait()
|
self.queue.put_nowait(file_item)
|
||||||
except Empty:
|
except Full:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.queue.put_nowait(file_item)
|
file_item.attempted_thumbnail = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the thumbnailer thread"""
|
"""Stop the thumbnailer thread"""
|
||||||
|
|
||||||
# Replace any pending items in the queue
|
|
||||||
try:
|
try:
|
||||||
self.queue.get_nowait()
|
# Drop all pending items
|
||||||
|
while True:
|
||||||
|
self.queue.get_nowait()
|
||||||
except Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -61,73 +67,73 @@ class Thumbnailer(threading.Thread):
|
||||||
if file_item is None:
|
if file_item is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
file_item.attempted_thumbnail = True
|
generate_thumbnail_sync(file_item)
|
||||||
self._generate_thumbnail(file_item)
|
|
||||||
|
|
||||||
def _generate_thumbnail(self, file_item: FileItem):
|
|
||||||
"""Generate thumbnail for a single file"""
|
|
||||||
|
|
||||||
pipeline_str = (
|
def generate_thumbnail_sync(file_item: FileItem):
|
||||||
"uridecodebin name=uridecodebin ! "
|
"""Generate thumbnail for a single file"""
|
||||||
"videoconvert ! "
|
|
||||||
"jpegenc quality=85 ! "
|
|
||||||
"appsink sync=false name=sink"
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = Gst.parse_launch(pipeline_str)
|
pipeline_str = (
|
||||||
assert isinstance(pipeline, Gst.Pipeline)
|
"uridecodebin name=uridecodebin ! "
|
||||||
|
"videoconvert ! "
|
||||||
|
"jpegenc quality=85 ! "
|
||||||
|
"appsink sync=false name=sink"
|
||||||
|
)
|
||||||
|
|
||||||
sink = pipeline.get_by_name("sink")
|
pipeline = Gst.parse_launch(pipeline_str)
|
||||||
assert isinstance(sink, Gst.Element)
|
assert isinstance(pipeline, Gst.Pipeline)
|
||||||
|
|
||||||
uridecodebin = pipeline.get_by_name("uridecodebin")
|
sink = pipeline.get_by_name("sink")
|
||||||
assert isinstance(uridecodebin, Gst.Element)
|
assert isinstance(sink, Gst.Element)
|
||||||
|
|
||||||
# Set file URI
|
uridecodebin = pipeline.get_by_name("uridecodebin")
|
||||||
uridecodebin.set_property("uri", Gst.filename_to_uri(str(file_item.full_path)))
|
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:
|
try:
|
||||||
# Set pipeline to PAUSED to get duration
|
thumbnail = bytes(map_info.data)
|
||||||
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)
|
|
||||||
|
|
||||||
finally:
|
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)
|
||||||
|
|
Loading…
Reference in a new issue