117 lines
3.1 KiB
Python
117 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import threading
|
|
from queue import Empty, Queue
|
|
|
|
from gi.repository import Gst
|
|
|
|
from .file_model import FileItem
|
|
|
|
|
|
class Thumbnailer(threading.Thread):
|
|
queue: Queue[FileItem | None]
|
|
|
|
def __init__(self):
|
|
super().__init__(daemon=True)
|
|
self.queue = Queue(maxsize=1)
|
|
|
|
def generate_thumbnail(self, file_item: FileItem):
|
|
"""Add a file item to the thumbnail queue"""
|
|
|
|
if not file_item.full_path.is_file():
|
|
return
|
|
|
|
# Replace any pending item in the queue
|
|
try:
|
|
self.queue.get_nowait()
|
|
except Empty:
|
|
pass
|
|
self.queue.put_nowait(file_item)
|
|
|
|
def stop(self):
|
|
"""Stop the thumbnailer thread"""
|
|
|
|
# Replace any pending items in the queue
|
|
try:
|
|
self.queue.get_nowait()
|
|
except Empty:
|
|
pass
|
|
|
|
self.queue.put_nowait(None)
|
|
self.join()
|
|
|
|
def run(self) -> None:
|
|
"""Process items from the queue continuously"""
|
|
|
|
while True:
|
|
file_item = self.queue.get(block=True)
|
|
|
|
if file_item is None:
|
|
break
|
|
|
|
self._generate_thumbnail(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 name=sink"
|
|
)
|
|
|
|
pipeline = Gst.parse_launch(pipeline_str)
|
|
assert isinstance(pipeline, Gst.Pipeline)
|
|
|
|
sink = pipeline.get_by_name("sink")
|
|
assert isinstance(sink, Gst.Element)
|
|
|
|
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,
|
|
Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
|
|
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:
|
|
file_item.thumbnail = bytes(map_info.data)
|
|
file_item.has_thumbnail = True
|
|
finally:
|
|
buffer.unmap(map_info)
|
|
except Exception as err:
|
|
print("Failed:", file_item.full_path, err)
|
|
finally:
|
|
pipeline.set_state(Gst.State.NULL)
|