Split-off video overlay
This commit is contained in:
parent
ae1e361cc9
commit
3c7953d815
4 changed files with 248 additions and 210 deletions
|
@ -5,32 +5,36 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from gi.repository import Gdk, GLib, Gtk, Pango
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from .file_model import FileItem, FileListModel, FileType
|
||||
from .reactive import Watcher, update_all_computed
|
||||
from .thumbnailer import Thumbnailer
|
||||
from .video_overlay import VideoOverlay
|
||||
from .video_player import VideoPlayer
|
||||
|
||||
|
||||
class MainWindow(Gtk.ApplicationWindow, Watcher):
|
||||
player: VideoPlayer
|
||||
overlay: VideoOverlay
|
||||
|
||||
stack: Gtk.Stack
|
||||
|
||||
list_view: Gtk.ListView
|
||||
list_model: FileListModel
|
||||
selection_model: Gtk.SingleSelection
|
||||
video_widget: Gtk.Picture
|
||||
tick_callback_id: int
|
||||
overlay_label: Gtk.Label
|
||||
overlay_hide_time: float
|
||||
last_position_save: float
|
||||
grid_segments: list[list[Gtk.Box]]
|
||||
grid_overlay: Gtk.Grid
|
||||
grid_clock: Gtk.Label
|
||||
main_clock: Gtk.Label
|
||||
|
||||
video_picture: Gtk.Picture
|
||||
thumbnail_picture: Gtk.Picture
|
||||
|
||||
clock: Gtk.Label
|
||||
|
||||
directory_history: list[Path]
|
||||
selection_history: dict[str, int]
|
||||
|
||||
last_position_save: float
|
||||
now: float
|
||||
|
||||
def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.thumbnailer = thumbnailer
|
||||
|
@ -39,18 +43,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
self.directory_history = []
|
||||
self.selection_history = {}
|
||||
|
||||
# For overlay text timeout
|
||||
self.overlay_hide_time = 0.0
|
||||
# Last time we've saved playback position.
|
||||
self.last_position_save = 0.0
|
||||
self.overlay_label = Gtk.Label()
|
||||
self.overlay_label.set_name("overlay-text")
|
||||
self.overlay_label.set_valign(Gtk.Align.CENTER)
|
||||
self.overlay_label.set_halign(Gtk.Align.CENTER)
|
||||
self.overlay_label.set_visible(False)
|
||||
self.overlay_label.set_wrap(True)
|
||||
self.overlay_label.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
|
||||
|
||||
self.tick_callback_id = self.add_tick_callback(self._on_tick, None)
|
||||
|
||||
# Make window fullscreen and borderless
|
||||
self.set_decorated(False)
|
||||
|
@ -69,107 +63,29 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
if cursor:
|
||||
self.set_cursor(cursor)
|
||||
|
||||
# Main horizontal box to split the screen
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
# Create stack to hold our widgets
|
||||
self.stack = Gtk.Stack()
|
||||
# Process frame ticks.
|
||||
self.add_tick_callback(self._on_tick, None)
|
||||
|
||||
# Create video widget and overlay
|
||||
self.video_widget = Gtk.Picture()
|
||||
self.video_widget.set_can_shrink(True)
|
||||
self.video_widget.set_keep_aspect_ratio(True)
|
||||
self.video_widget.set_vexpand(True)
|
||||
self.video_widget.set_hexpand(True)
|
||||
|
||||
video_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
video_box.set_name("black-overlay")
|
||||
video_box.set_vexpand(True)
|
||||
video_box.set_hexpand(True)
|
||||
|
||||
# Create grid overlay with 3x3 boxes
|
||||
self.grid_overlay = Gtk.Grid()
|
||||
self.grid_overlay.set_hexpand(True)
|
||||
self.grid_overlay.set_vexpand(True)
|
||||
self.grid_overlay.set_name("grid-overlay")
|
||||
self.grid_overlay.set_column_homogeneous(True)
|
||||
self.grid_overlay.set_row_homogeneous(True)
|
||||
|
||||
# Store grid segments in a 3x3 array
|
||||
self.grid_segments = [[Gtk.Box() for _ in range(3)] for _ in range(3)]
|
||||
|
||||
# Setup 3x3 grid of boxes
|
||||
for row in range(3):
|
||||
for col in range(3):
|
||||
box = self.grid_segments[row][col]
|
||||
box.set_name("grid-box")
|
||||
box.set_hexpand(True)
|
||||
box.set_vexpand(True)
|
||||
self.grid_overlay.attach(box, col, row, 1, 1)
|
||||
|
||||
# Add clock to top-left grid box
|
||||
self.grid_clock = Gtk.Label()
|
||||
self.grid_clock.set_name("grid-clock")
|
||||
self.grid_clock.set_halign(Gtk.Align.CENTER)
|
||||
self.grid_clock.set_valign(Gtk.Align.START)
|
||||
self.grid_clock.set_hexpand(True)
|
||||
self.grid_clock.set_vexpand(True)
|
||||
self.grid_clock.set_text(datetime.now().strftime("%H:%M"))
|
||||
|
||||
# Attach to top-left grid box
|
||||
self.grid_segments[0][0].append(self.grid_clock)
|
||||
|
||||
# Create an overlay container
|
||||
overlay = Gtk.Overlay()
|
||||
overlay.set_child(self.video_widget)
|
||||
overlay.add_overlay(self.grid_overlay)
|
||||
overlay.add_overlay(self.overlay_label)
|
||||
|
||||
video_box.append(overlay)
|
||||
|
||||
# Setup video player
|
||||
self.video_player = VideoPlayer(self.video_widget)
|
||||
|
||||
# Add both main menu and overlay to stack
|
||||
self.stack.add_named(main_box, "menu")
|
||||
self.stack.add_named(video_box, "player")
|
||||
self.stack.set_visible_child_name("menu")
|
||||
|
||||
# Create a grid to handle the 1:2 ratio
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_homogeneous(True)
|
||||
grid.set_hexpand(True)
|
||||
|
||||
# Left third (1/3 width)
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
left_box.set_valign(Gtk.Align.START)
|
||||
left_box.set_halign(Gtk.Align.FILL)
|
||||
self.video_picture = Gtk.Picture()
|
||||
self.video_picture.set_can_shrink(True)
|
||||
self.video_picture.set_keep_aspect_ratio(True)
|
||||
self.video_picture.set_vexpand(True)
|
||||
self.video_picture.set_hexpand(True)
|
||||
|
||||
# Create main menu clock
|
||||
self.main_clock = Gtk.Label()
|
||||
self.main_clock.set_name("digital-clock")
|
||||
self.main_clock.set_halign(Gtk.Align.CENTER)
|
||||
self.main_clock.set_valign(Gtk.Align.CENTER)
|
||||
left_box.append(self.main_clock)
|
||||
self.clock = Gtk.Label()
|
||||
self.clock.set_name("main-clock")
|
||||
self.clock.set_halign(Gtk.Align.CENTER)
|
||||
self.clock.set_valign(Gtk.Align.CENTER)
|
||||
|
||||
# Create image widget for thumbnail
|
||||
self.thumbnail_image = Gtk.Picture()
|
||||
self.thumbnail_image.set_name("thumbnail-image")
|
||||
self.thumbnail_image.set_size_request(384, 216) # 16:9 aspect ratio
|
||||
self.thumbnail_image.set_can_shrink(True)
|
||||
self.thumbnail_image.set_keep_aspect_ratio(True)
|
||||
self.thumbnail_image.set_resource(None)
|
||||
left_box.append(self.thumbnail_image)
|
||||
|
||||
# Right two-thirds (2/3 width)
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
right_box.set_hexpand(True)
|
||||
|
||||
# Attach boxes to grid with specific column spans
|
||||
grid.attach(left_box, 0, 0, 1, 1)
|
||||
grid.attach(right_box, 1, 0, 2, 1)
|
||||
|
||||
main_box.append(grid)
|
||||
self.thumbnail_picture = Gtk.Picture()
|
||||
self.thumbnail_picture.set_name("thumbnail-picture")
|
||||
self.thumbnail_picture.set_size_request(384, 216)
|
||||
self.thumbnail_picture.set_can_shrink(True)
|
||||
self.thumbnail_picture.set_keep_aspect_ratio(True)
|
||||
self.thumbnail_picture.set_resource(None)
|
||||
|
||||
# Create list model and view
|
||||
self.list_model = FileListModel()
|
||||
|
@ -178,35 +94,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
self.selection_model.connect("selection-changed", self._on_selection_changed)
|
||||
self.list_view.set_model(self.selection_model)
|
||||
self.list_view.set_vexpand(True)
|
||||
|
||||
def on_activate(widget: Gtk.ListView, index: int):
|
||||
selected_item = self.selection_model.get_item(index)
|
||||
if selected_item:
|
||||
file_item = cast(FileItem, selected_item)
|
||||
|
||||
if file_item.file_type == FileType.DIRECTORY:
|
||||
self._navigate_to(file_item.full_path)
|
||||
return
|
||||
|
||||
position = file_item.saved_position.value
|
||||
duration = file_item.saved_duration.value
|
||||
|
||||
if (position / duration) >= 0.99:
|
||||
position = 0
|
||||
|
||||
# Start playing the video
|
||||
self.video_player.play(
|
||||
file_item.full_path,
|
||||
position,
|
||||
file_item.saved_subtitle_track.value,
|
||||
file_item.saved_audio_track.value,
|
||||
)
|
||||
self.last_position_save = self.now
|
||||
|
||||
self.stack.set_visible_child_name("player")
|
||||
self.show_overlay_text(f"Playing: {file_item.name}")
|
||||
|
||||
self.list_view.connect("activate", on_activate)
|
||||
self.list_view.connect("activate", self._on_activate)
|
||||
|
||||
# Factory for list items
|
||||
factory = Gtk.SignalListItemFactory()
|
||||
|
@ -214,25 +102,52 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
factory.connect("bind", self._bind_list_item)
|
||||
self.list_view.set_factory(factory)
|
||||
|
||||
# Populate the list store
|
||||
self._populate_file_list()
|
||||
|
||||
# Add list view to a scrolled window
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_child(self.list_view)
|
||||
|
||||
# Setup video player.
|
||||
self.player = VideoPlayer(self.video_picture)
|
||||
|
||||
# Setup video overlay using that player.
|
||||
self.overlay = VideoOverlay(self.player)
|
||||
self.overlay.set_name("overlay")
|
||||
|
||||
# Left third (1/3 width).
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
left_box.set_valign(Gtk.Align.START)
|
||||
left_box.set_halign(Gtk.Align.FILL)
|
||||
left_box.append(self.clock)
|
||||
left_box.append(self.thumbnail_picture)
|
||||
|
||||
# Right two-thirds (2/3 width).
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
right_box.set_hexpand(True)
|
||||
right_box.append(scrolled)
|
||||
|
||||
# Create a grid to handle the 1:2 ratio we want.
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_homogeneous(True)
|
||||
grid.set_hexpand(True)
|
||||
grid.attach(left_box, 0, 0, 1, 1)
|
||||
grid.attach(right_box, 1, 0, 2, 1)
|
||||
|
||||
# Add both main menu and overlay to stack.
|
||||
self.stack = Gtk.Stack()
|
||||
self.stack.add_named(grid, "menu")
|
||||
self.stack.add_named(self.overlay, "player")
|
||||
|
||||
# Start with main menu visible.
|
||||
self.stack.set_visible_child_name("menu")
|
||||
|
||||
# Stack is our root.
|
||||
self.set_child(self.stack)
|
||||
|
||||
# Enable all watch methods.
|
||||
self.watch_all()
|
||||
|
||||
@property
|
||||
def now(self) -> float:
|
||||
frame_clock = self.get_frame_clock()
|
||||
if frame_clock is None:
|
||||
return 0
|
||||
|
||||
return frame_clock.get_frame_time() / 1_000_000
|
||||
# Populate the list store.
|
||||
self._populate_file_list()
|
||||
|
||||
@property
|
||||
def selection(self) -> FileItem | None:
|
||||
|
@ -285,6 +200,33 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
item.connect("notify::saved-duration", update_icon)
|
||||
update_icon()
|
||||
|
||||
def _on_activate(self, widget: Gtk.ListView, index: int):
|
||||
selected_item = self.selection_model.get_item(index)
|
||||
if selected_item:
|
||||
file_item = cast(FileItem, selected_item)
|
||||
|
||||
if file_item.file_type == FileType.DIRECTORY:
|
||||
self._navigate_to(file_item.full_path)
|
||||
return
|
||||
|
||||
position = file_item.saved_position.value
|
||||
duration = file_item.saved_duration.value
|
||||
|
||||
if (position / duration) >= 0.99:
|
||||
position = 0
|
||||
|
||||
# Start playing the video
|
||||
self.player.play(
|
||||
file_item.full_path,
|
||||
position,
|
||||
file_item.saved_subtitle_track.value,
|
||||
file_item.saved_audio_track.value,
|
||||
)
|
||||
self.last_position_save = self.now
|
||||
|
||||
self.stack.set_visible_child_name("player")
|
||||
self.overlay.show_message(f"Playing: {file_item.name}")
|
||||
|
||||
def _restore_selection(self):
|
||||
pos = self.selection_history.get(os.getcwd(), 0)
|
||||
self.selection_model.set_selected(pos)
|
||||
|
@ -327,14 +269,14 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
if file_item.thumbnail.value:
|
||||
gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
|
||||
texture = Gdk.Texture.new_from_bytes(gbytes)
|
||||
self.thumbnail_image.set_paintable(texture)
|
||||
self.thumbnail_picture.set_paintable(texture)
|
||||
else:
|
||||
self.thumbnailer.generate_thumbnail(file_item)
|
||||
self.thumbnail_image.set_paintable(None)
|
||||
self.thumbnail_picture.set_paintable(None)
|
||||
|
||||
def _toggle_play_pause(self) -> None:
|
||||
"""Toggle between play and pause states"""
|
||||
self.video_player.toggle_play_pause()
|
||||
self.player.toggle_play_pause()
|
||||
|
||||
def _on_player_key_pressed(
|
||||
self,
|
||||
|
@ -348,62 +290,62 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
|
||||
elif keyval == Gdk.keyval_from_name("Escape"):
|
||||
self._save_position()
|
||||
self.video_player.stop()
|
||||
self.player.stop()
|
||||
self.stack.set_visible_child_name("menu")
|
||||
self.list_view.grab_focus()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Left"):
|
||||
self.video_player.seek_relative(-10)
|
||||
self.player.seek_relative(-10)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Right"):
|
||||
self.video_player.seek_relative(10)
|
||||
self.player.seek_relative(10)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Down"):
|
||||
self.video_player.seek_relative(-60)
|
||||
self.player.seek_relative(-60)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Up"):
|
||||
self.video_player.seek_relative(60)
|
||||
self.player.seek_relative(60)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Home"):
|
||||
self.video_player.seek_start()
|
||||
self.player.seek_start()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("End"):
|
||||
self.video_player.seek_end()
|
||||
self.player.seek_end()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("j"):
|
||||
has_subs, index, lang = self.video_player.cycle_subtitles()
|
||||
has_subs, index, lang = self.player.cycle_subtitles()
|
||||
|
||||
if has_subs:
|
||||
if index:
|
||||
self.show_overlay_text(f"Subtitles #{index} ({lang})")
|
||||
self.overlay.show_message(f"Subtitles #{index} ({lang})")
|
||||
else:
|
||||
self.show_overlay_text("Subtitles turned off")
|
||||
self.overlay.show_message("Subtitles turned off")
|
||||
|
||||
file_item = self.selection
|
||||
if file_item is not None:
|
||||
file_item.saved_subtitle_track.value = index - 1
|
||||
else:
|
||||
self.show_overlay_text("No subtitles available")
|
||||
self.overlay.show_message("No subtitles available")
|
||||
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("a"):
|
||||
has_audio, index, lang = self.video_player.cycle_audio()
|
||||
has_audio, index, lang = self.player.cycle_audio()
|
||||
|
||||
if has_audio:
|
||||
self.show_overlay_text(f"Audio #{index} ({lang})")
|
||||
self.overlay.show_message(f"Audio #{index} ({lang})")
|
||||
file_item = self.selection
|
||||
if file_item is not None:
|
||||
file_item.saved_audio_track.value = index - 1
|
||||
else:
|
||||
self.show_overlay_text("No audio tracks available")
|
||||
self.overlay.show_message("No audio tracks available")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -470,7 +412,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
def _save_position(self) -> None:
|
||||
"""Save current playback position as xattr"""
|
||||
|
||||
if not self.video_player.is_playing:
|
||||
if not self.player.is_playing:
|
||||
return
|
||||
|
||||
file_item = self.selection
|
||||
|
@ -478,55 +420,35 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
|
|||
if file_item is None or file_item.file_type == FileType.DIRECTORY:
|
||||
return
|
||||
|
||||
position = self.video_player.get_position()
|
||||
position = self.player.get_position()
|
||||
if position is not None:
|
||||
file_item.saved_position.value = position
|
||||
|
||||
duration = self.video_player.get_duration()
|
||||
duration = self.player.get_duration()
|
||||
if duration is not None:
|
||||
file_item.saved_duration.value = duration
|
||||
|
||||
def show_overlay_text(self, text: str, timeout_seconds: float = 1.0) -> None:
|
||||
"""Show text in a centered overlay that disappears after timeout"""
|
||||
|
||||
self.overlay_label.set_text(text)
|
||||
self.overlay_label.set_visible(True)
|
||||
|
||||
# Set absolute time when overlay should hide
|
||||
self.overlay_hide_time = self.now + timeout_seconds
|
||||
|
||||
def _watch_player_state(self):
|
||||
is_playing = self.video_player.is_playing.value
|
||||
is_paused = self.video_player.is_paused.value
|
||||
self.grid_overlay.set_visible(is_playing and is_paused)
|
||||
|
||||
def _on_tick(self, widget: Gtk.Widget, frame_clock: Gdk.FrameClock, data: Any) -> bool:
|
||||
# Update all reactive values.
|
||||
# Update all reactive values for whole application.
|
||||
update_all_computed()
|
||||
|
||||
current_time = frame_clock.get_frame_time() / 1_000_000
|
||||
self.clock.set_text(datetime.now().strftime("%H:%M"))
|
||||
|
||||
current_time_str = datetime.now().strftime("%H:%M")
|
||||
self.main_clock.set_text(current_time_str)
|
||||
self.grid_clock.set_text(current_time_str)
|
||||
self.now = frame_clock.get_frame_time() / 1_000_000
|
||||
|
||||
if current_time >= self.overlay_hide_time:
|
||||
self.overlay_label.set_visible(False)
|
||||
|
||||
# Save position every 60 seconds
|
||||
frame_time = frame_clock.get_frame_time() / 1_000_000
|
||||
if frame_time - self.last_position_save >= 60.0:
|
||||
# Save playback position every 60 seconds.
|
||||
if self.now - self.last_position_save >= 60.0:
|
||||
self._save_position()
|
||||
self.last_position_save = frame_time
|
||||
self.last_position_save = self.now
|
||||
|
||||
# Update thumbnail if available
|
||||
if file_item := self.selection:
|
||||
self.thumbnailer.generate_thumbnail(file_item)
|
||||
|
||||
if file_item.thumbnail.value and not self.thumbnail_image.get_paintable():
|
||||
if file_item.thumbnail.value and not self.thumbnail_picture.get_paintable():
|
||||
gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
|
||||
texture = Gdk.Texture.new_from_bytes(gbytes)
|
||||
self.thumbnail_image.set_paintable(texture)
|
||||
self.thumbnail_picture.set_paintable(texture)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ listview > row {
|
|||
color: #0f0;
|
||||
}
|
||||
|
||||
#black-overlay {
|
||||
#overlay {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#overlay-text {
|
||||
#overlay-message {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-family: monospace;
|
||||
|
@ -34,19 +34,19 @@ listview > row {
|
|||
margin: 32px;
|
||||
}
|
||||
|
||||
#thumbnail-image {
|
||||
#thumbnail-picture {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
#digital-clock,
|
||||
#grid-clock {
|
||||
#main-clock,
|
||||
#overlay-clock {
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
font-family: monospace;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#grid-clock {
|
||||
#overlay-clock {
|
||||
background-color: rgba(32, 32, 32, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
113
lazy_player/video_overlay.py
Normal file
113
lazy_player/video_overlay.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import Gdk, Gtk, Pango
|
||||
|
||||
from .reactive import Watcher
|
||||
from .video_player import VideoPlayer
|
||||
|
||||
|
||||
class VideoOverlay(Gtk.Overlay, Watcher):
|
||||
player: VideoPlayer
|
||||
|
||||
message: Gtk.Label
|
||||
message_expiration: float
|
||||
|
||||
grid: Gtk.Grid
|
||||
grid_expiration: float
|
||||
|
||||
clock_box: Gtk.Box
|
||||
clock: Gtk.Label
|
||||
|
||||
now: float
|
||||
|
||||
def __init__(self, player: VideoPlayer):
|
||||
super().__init__()
|
||||
|
||||
self.now = 0.0
|
||||
self.player = player
|
||||
|
||||
# Message is appears at the center of the screen,
|
||||
# above everything else. Usually to indicate change
|
||||
# of subtitle or audio track or something similar.
|
||||
self.message = Gtk.Label()
|
||||
self.message.set_name("overlay-message")
|
||||
self.message.set_valign(Gtk.Align.CENTER)
|
||||
self.message.set_halign(Gtk.Align.CENTER)
|
||||
self.message.set_visible(False)
|
||||
self.message.set_wrap(True)
|
||||
self.message.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
|
||||
|
||||
# Once specific time passes, message disappears.
|
||||
self.message_expiration = 0.0
|
||||
|
||||
# Grid overlay is between the video at the bottom and
|
||||
# the message at the top. It is only shown when user
|
||||
# interacts with the player.
|
||||
self.grid = Gtk.Grid()
|
||||
self.grid.set_hexpand(True)
|
||||
self.grid.set_vexpand(True)
|
||||
self.grid.set_column_homogeneous(True)
|
||||
self.grid.set_row_homogeneous(True)
|
||||
|
||||
# Grid visibility can also expire after a while.
|
||||
self.grid_expiration = 0.0
|
||||
|
||||
# Create grid boxes.
|
||||
self.clock_box = Gtk.Box(hexpand=True, vexpand=True)
|
||||
self.grid.attach(self.clock_box, 0, 0, 1, 1)
|
||||
|
||||
self.grid.attach(Gtk.Box(), 1, 0, 2, 1)
|
||||
self.grid.attach(Gtk.Box(), 0, 1, 3, 1)
|
||||
self.grid.attach(Gtk.Box(), 0, 2, 3, 1)
|
||||
|
||||
# Add clock to the top-left grid box.
|
||||
self.clock = Gtk.Label(hexpand=True, vexpand=True)
|
||||
self.clock.set_name("overlay-clock")
|
||||
self.clock.set_halign(Gtk.Align.CENTER)
|
||||
self.clock.set_valign(Gtk.Align.START)
|
||||
self.clock.set_text(datetime.now().strftime("%H:%M"))
|
||||
self.clock_box.append(self.clock)
|
||||
|
||||
# Add children.
|
||||
self.set_child(self.player.picture)
|
||||
self.add_overlay(self.grid)
|
||||
self.add_overlay(self.message)
|
||||
|
||||
# Consume ticks for the clock and overlay expiration.
|
||||
self.add_tick_callback(self._on_tick, None)
|
||||
|
||||
# Register all watches.
|
||||
self.watch_all()
|
||||
|
||||
def _on_tick(self, widget: Gtk.Widget, frame_clock: Gdk.FrameClock, data: Any) -> bool:
|
||||
self.clock.set_text(datetime.now().strftime("%H:%M"))
|
||||
|
||||
self.now = frame_clock.get_frame_time() / 1_000_000
|
||||
|
||||
if self.grid_expiration <= self.now:
|
||||
self.grid.hide()
|
||||
|
||||
if self.message_expiration <= self.now:
|
||||
self.message.hide()
|
||||
|
||||
return True
|
||||
|
||||
def show_message(self, text: str, timeout: float = 1.0) -> None:
|
||||
"""Show text in a centered overlay that disappears after timeout."""
|
||||
|
||||
self.message.set_text(text)
|
||||
self.message.show()
|
||||
self.message_expiration = self.now + timeout
|
||||
|
||||
def _watch_player_state(self):
|
||||
is_playing = self.player.is_playing.value
|
||||
is_paused = self.player.is_paused.value
|
||||
|
||||
if is_playing and is_paused:
|
||||
self.grid.show()
|
||||
self.grid_expiration = 1e20
|
||||
else:
|
||||
self.grid_expiration = 0
|
|
@ -10,6 +10,7 @@ DEFAULT_SEEK_FLAGS = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT
|
|||
|
||||
|
||||
class VideoPlayer(GObject.Object):
|
||||
picture: Gtk.Picture
|
||||
pipeline: Gst.Pipeline
|
||||
playbin: Gst.Element
|
||||
|
||||
|
@ -21,6 +22,8 @@ class VideoPlayer(GObject.Object):
|
|||
def __init__(self, picture: Gtk.Picture):
|
||||
super().__init__()
|
||||
|
||||
self.picture = picture
|
||||
|
||||
self.is_playing = Ref(False)
|
||||
self.is_paused = Ref(True)
|
||||
|
||||
|
@ -41,7 +44,7 @@ class VideoPlayer(GObject.Object):
|
|||
|
||||
# Link picture to sink
|
||||
paintable = video_sink.get_property("paintable")
|
||||
picture.set_paintable(paintable)
|
||||
self.picture.set_paintable(paintable)
|
||||
|
||||
def play(
|
||||
self,
|
||||
|
|
Loading…
Reference in a new issue