lazy-player/lazy_player/thumbnailer.py

134 lines
3.5 KiB
Python
Raw Normal View History

2025-03-09 18:34:57 +01:00
from __future__ import annotations
2025-03-09 21:21:20 +01:00
import threading
from queue import Empty, Queue
2025-03-11 10:42:08 +01:00
from typing import TYPE_CHECKING
2025-03-09 18:34:57 +01:00
2025-03-11 10:38:42 +01:00
from gi.repository import GLib, Gst
2025-03-09 18:34:57 +01:00
2025-03-11 10:42:08 +01:00
if TYPE_CHECKING:
from .file_model import FileItem
2025-03-09 18:34:57 +01:00
2025-03-11 10:59:27 +01:00
DEFAULT_SEEK_FLAGS = (
Gst.SeekFlags.FLUSH
| Gst.SeekFlags.KEY_UNIT
| Gst.SeekFlags.SNAP_NEAREST
| Gst.SeekFlags.TRICKMODE
| Gst.SeekFlags.TRICKMODE_KEY_UNITS
| Gst.SeekFlags.TRICKMODE_NO_AUDIO
)
2025-03-11 10:05:41 +01:00
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
class Thumbnailer(threading.Thread):
queue: Queue[FileItem | None]
2025-03-09 18:34:57 +01:00
def __init__(self):
2025-03-09 21:21:20 +01:00
super().__init__(daemon=True)
self.queue = Queue(maxsize=1)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
def generate_thumbnail(self, file_item: FileItem):
2025-03-09 18:34:57 +01:00
"""Add a file item to the thumbnail queue"""
if not file_item.full_path.is_file():
return
2025-03-09 21:21:20 +01:00
# Replace any pending item in the queue
try:
self.queue.get_nowait()
except Empty:
pass
2025-03-11 10:38:42 +01:00
2025-03-09 21:21:20 +01:00
self.queue.put_nowait(file_item)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
def stop(self):
"""Stop the thumbnailer thread"""
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
# Replace any pending items in the queue
try:
self.queue.get_nowait()
except Empty:
pass
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
self.queue.put_nowait(None)
self.join()
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
def run(self) -> None:
"""Process items from the queue continuously"""
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
while True:
file_item = self.queue.get(block=True)
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
if file_item is None:
break
2025-03-09 18:34:57 +01:00
2025-03-11 10:38:42 +01:00
file_item.attempted_thumbnail = True
2025-03-09 21:21:20 +01:00
self._generate_thumbnail(file_item)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
def _generate_thumbnail(self, file_item: FileItem):
"""Generate thumbnail for a single file"""
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
pipeline_str = (
"uridecodebin name=uridecodebin ! "
"videoconvert ! "
"jpegenc quality=85 ! "
2025-03-11 10:59:27 +01:00
"appsink sync=false name=sink"
2025-03-09 18:34:57 +01:00
)
2025-03-09 21:21:20 +01:00
pipeline = Gst.parse_launch(pipeline_str)
assert isinstance(pipeline, Gst.Pipeline)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
sink = pipeline.get_by_name("sink")
assert isinstance(sink, Gst.Element)
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
uridecodebin = pipeline.get_by_name("uridecodebin")
assert isinstance(uridecodebin, Gst.Element)
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
# Set file URI
uridecodebin.set_property("uri", Gst.filename_to_uri(str(file_item.full_path)))
2025-03-09 18:34:57 +01:00
2025-03-09 19:27:05 +01:00
try:
2025-03-09 21:21:20 +01:00
# Set pipeline to PAUSED to get duration
pipeline.set_state(Gst.State.PAUSED)
pipeline.get_state(Gst.SECOND)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
# Seek to 1/3 of duration
success, duration = pipeline.query_duration(Gst.Format.TIME)
if not success:
return
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
seek_pos = duration // 3
2025-03-11 10:05:41 +01:00
pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, seek_pos)
2025-03-09 19:27:05 +01:00
2025-03-09 21:21:20 +01:00
# Start playing to capture frame
pipeline.set_state(Gst.State.PLAYING)
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
sample = sink.emit("pull-sample")
if not sample:
return
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
# Extract image data
buffer = sample.get_buffer()
if not buffer:
return
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
success, map_info = buffer.map(Gst.MapFlags.READ)
if not success:
return
2025-03-09 18:34:57 +01:00
2025-03-09 21:21:20 +01:00
try:
2025-03-11 10:38:42 +01:00
thumbnail = bytes(map_info.data)
2025-03-09 21:21:20 +01:00
finally:
buffer.unmap(map_info)
2025-03-11 10:38:42 +01:00
def set_thumbnail():
file_item.thumbnail = thumbnail
file_item.has_thumbnail = True
GLib.idle_add(set_thumbnail)
2025-03-09 21:21:20 +01:00
except Exception as err:
print("Failed:", file_item.full_path, err)
2025-03-11 10:38:42 +01:00
2025-03-09 21:21:20 +01:00
finally:
pipeline.set_state(Gst.State.NULL)