lazy-player/lazy_player/thumbnailer.py

135 lines
3.7 KiB
Python
Raw Permalink Normal View History

2025-03-09 18:34:57 +01:00
from __future__ import annotations
2025-03-11 11:46:33 +01:00
import multiprocessing
import os
2025-03-11 11:16:36 +01:00
import sys
2025-03-11 11:46:33 +01:00
from concurrent.futures import ThreadPoolExecutor
from queue import LifoQueue
from typing import TYPE_CHECKING, Any, cast
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-11 11:16:36 +01:00
__all__ = ["Thumbnailer", "generate_thumbnail_sync"]
2025-03-09 18:34:57 +01:00
2025-03-11 11:46:33 +01:00
MAX_WORKERS = max(1, multiprocessing.cpu_count() // 2)
2025-03-09 18:34:57 +01:00
2025-03-11 11:46:33 +01:00
class Thumbnailer(ThreadPoolExecutor):
2025-03-09 18:34:57 +01:00
def __init__(self):
2025-03-11 11:46:33 +01:00
super().__init__(
thread_name_prefix="Thumbnailer",
max_workers=MAX_WORKERS,
)
2025-03-09 18:34:57 +01:00
self._work_queue = cast(Any, LifoQueue())
2025-03-09 21:21:20 +01:00
def generate_thumbnail(self, file_item: FileItem):
"""Schedule thumbnail generation."""
2025-03-09 18:34:57 +01:00
if not file_item.full_path.is_file():
return
2025-03-11 17:56:53 +01:00
if file_item.attempted_thumbnail.value:
2025-03-11 11:16:36 +01:00
return
self.submit(generate_thumbnail_sync_nicely, file_item)
2025-03-11 17:56:53 +01:00
file_item.attempted_thumbnail.value = True
2025-03-09 18:34:57 +01:00
def generate_thumbnail_sync_nicely(file_item: FileItem):
2025-03-11 12:43:34 +01:00
os.nice(10)
return generate_thumbnail_sync(file_item)
2025-03-11 11:16:36 +01:00
def generate_thumbnail_sync(file_item: FileItem):
"""Generate thumbnail for a single file"""
2025-03-09 18:34:57 +01:00
2025-03-11 11:46:33 +01:00
# print("[thumbnailer] Generate:", file_item.full_path.name)
2025-03-11 11:16:36 +01:00
pipeline_str = (
"uridecodebin name=uridecodebin force-sw-decoders=true ! "
2025-03-11 11:16:36 +01:00
"videoconvert ! "
"jpegenc quality=85 ! "
"appsink sync=false name=sink"
)
2025-03-09 18:34:57 +01:00
pipeline: Gst.Pipeline | None = None
try:
pipeline = cast(Gst.Pipeline, Gst.parse_launch(pipeline_str))
2025-03-09 19:27:05 +01:00
sink = pipeline.get_by_name("sink")
assert isinstance(sink, Gst.Element)
2025-03-09 19:27:05 +01:00
uridecodebin = pipeline.get_by_name("uridecodebin")
assert isinstance(uridecodebin, Gst.Element)
2025-03-09 18:34:57 +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-11 11:16:36 +01:00
# Set pipeline to PAUSED to get duration
pipeline.set_state(Gst.State.PAUSED)
2025-03-11 11:46:33 +01:00
pipeline.get_state(Gst.CLOCK_TIME_NONE)
2025-03-09 19:27:05 +01:00
2025-03-11 11:46:33 +01:00
# Obtain total duration
2025-03-11 11:16:36 +01:00
success, duration = pipeline.query_duration(Gst.Format.TIME)
if not success:
2025-03-11 11:46:33 +01:00
raise RuntimeError("Failed to query duration")
2025-03-09 19:27:05 +01:00
2025-03-13 23:42:07 +01:00
def get_sample(seek_pos: int) -> bytes:
pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, seek_pos)
2025-03-09 18:34:57 +01:00
2025-03-13 23:42:07 +01:00
# Start playing to capture frame
pipeline.set_state(Gst.State.PLAYING)
2025-03-09 18:34:57 +01:00
2025-03-13 23:42:07 +01:00
sample = sink.emit("pull-sample")
if not sample:
raise RuntimeError("Failed to pull sample")
2025-03-09 18:34:57 +01:00
2025-03-13 23:42:07 +01:00
# Extract image data
buffer = sample.get_buffer()
if not buffer:
raise RuntimeError("Failed to get buffer")
2025-03-11 11:16:36 +01:00
2025-03-13 23:42:07 +01:00
success, map_info = buffer.map(Gst.MapFlags.READ)
if not success:
raise RuntimeError("Failed to map buffer")
try:
return bytes(map_info.data)
finally:
buffer.unmap(map_info)
candidates: list[bytes] = []
for i in range(3):
candidates.append(get_sample(duration // 3 - Gst.SECOND + i * Gst.SECOND))
2025-03-09 18:34:57 +01:00
2025-03-13 23:42:07 +01:00
thumbnail = max(candidates, key=len)
2025-03-11 10:38:42 +01:00
2025-03-11 11:16:36 +01:00
def set_thumbnail():
2025-03-11 17:56:53 +01:00
file_item.thumbnail.value = thumbnail
2025-03-11 10:38:42 +01:00
2025-03-11 11:16:36 +01:00
GLib.idle_add(set_thumbnail)
2025-03-11 10:38:42 +01:00
2025-03-11 11:16:36 +01:00
except Exception as err:
2025-03-11 11:46:33 +01:00
print("[thumbnailer] Error:", file_item.full_path.name, file=sys.stderr)
print("[thumbnailer]", err, file=sys.stderr)
2025-03-11 10:38:42 +01:00
2025-03-11 11:16:36 +01:00
finally:
if pipeline:
pipeline.set_state(Gst.State.NULL)