Split-off video overlay

This commit is contained in:
Jan Hamal Dvořák 2025-03-11 19:02:15 +01:00
parent ae1e361cc9
commit 3c7953d815
4 changed files with 248 additions and 210 deletions

View file

@ -5,32 +5,36 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, cast 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 .file_model import FileItem, FileListModel, FileType
from .reactive import Watcher, update_all_computed from .reactive import Watcher, update_all_computed
from .thumbnailer import Thumbnailer from .thumbnailer import Thumbnailer
from .video_overlay import VideoOverlay
from .video_player import VideoPlayer from .video_player import VideoPlayer
class MainWindow(Gtk.ApplicationWindow, Watcher): class MainWindow(Gtk.ApplicationWindow, Watcher):
player: VideoPlayer
overlay: VideoOverlay
stack: Gtk.Stack stack: Gtk.Stack
list_view: Gtk.ListView list_view: Gtk.ListView
list_model: FileListModel list_model: FileListModel
selection_model: Gtk.SingleSelection selection_model: Gtk.SingleSelection
video_widget: Gtk.Picture
tick_callback_id: int video_picture: Gtk.Picture
overlay_label: Gtk.Label thumbnail_picture: Gtk.Picture
overlay_hide_time: float
last_position_save: float clock: Gtk.Label
grid_segments: list[list[Gtk.Box]]
grid_overlay: Gtk.Grid
grid_clock: Gtk.Label
main_clock: Gtk.Label
directory_history: list[Path] directory_history: list[Path]
selection_history: dict[str, int] selection_history: dict[str, int]
last_position_save: float
now: float
def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any): def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.thumbnailer = thumbnailer self.thumbnailer = thumbnailer
@ -39,18 +43,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
self.directory_history = [] self.directory_history = []
self.selection_history = {} self.selection_history = {}
# For overlay text timeout # Last time we've saved playback position.
self.overlay_hide_time = 0.0
self.last_position_save = 0.0 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 # Make window fullscreen and borderless
self.set_decorated(False) self.set_decorated(False)
@ -69,107 +63,29 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
if cursor: if cursor:
self.set_cursor(cursor) self.set_cursor(cursor)
# Main horizontal box to split the screen # Process frame ticks.
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add_tick_callback(self._on_tick, None)
# Create stack to hold our widgets
self.stack = Gtk.Stack()
# Create video widget and overlay # Create video widget and overlay
self.video_widget = Gtk.Picture() self.video_picture = Gtk.Picture()
self.video_widget.set_can_shrink(True) self.video_picture.set_can_shrink(True)
self.video_widget.set_keep_aspect_ratio(True) self.video_picture.set_keep_aspect_ratio(True)
self.video_widget.set_vexpand(True) self.video_picture.set_vexpand(True)
self.video_widget.set_hexpand(True) self.video_picture.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)
# Create main menu clock # Create main menu clock
self.main_clock = Gtk.Label() self.clock = Gtk.Label()
self.main_clock.set_name("digital-clock") self.clock.set_name("main-clock")
self.main_clock.set_halign(Gtk.Align.CENTER) self.clock.set_halign(Gtk.Align.CENTER)
self.main_clock.set_valign(Gtk.Align.CENTER) self.clock.set_valign(Gtk.Align.CENTER)
left_box.append(self.main_clock)
# Create image widget for thumbnail # Create image widget for thumbnail
self.thumbnail_image = Gtk.Picture() self.thumbnail_picture = Gtk.Picture()
self.thumbnail_image.set_name("thumbnail-image") self.thumbnail_picture.set_name("thumbnail-picture")
self.thumbnail_image.set_size_request(384, 216) # 16:9 aspect ratio self.thumbnail_picture.set_size_request(384, 216)
self.thumbnail_image.set_can_shrink(True) self.thumbnail_picture.set_can_shrink(True)
self.thumbnail_image.set_keep_aspect_ratio(True) self.thumbnail_picture.set_keep_aspect_ratio(True)
self.thumbnail_image.set_resource(None) self.thumbnail_picture.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)
# Create list model and view # Create list model and view
self.list_model = FileListModel() self.list_model = FileListModel()
@ -178,35 +94,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
self.selection_model.connect("selection-changed", self._on_selection_changed) self.selection_model.connect("selection-changed", self._on_selection_changed)
self.list_view.set_model(self.selection_model) self.list_view.set_model(self.selection_model)
self.list_view.set_vexpand(True) self.list_view.set_vexpand(True)
self.list_view.connect("activate", self._on_activate)
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)
# Factory for list items # Factory for list items
factory = Gtk.SignalListItemFactory() factory = Gtk.SignalListItemFactory()
@ -214,25 +102,52 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
factory.connect("bind", self._bind_list_item) factory.connect("bind", self._bind_list_item)
self.list_view.set_factory(factory) self.list_view.set_factory(factory)
# Populate the list store
self._populate_file_list()
# Add list view to a scrolled window # Add list view to a scrolled window
scrolled = Gtk.ScrolledWindow() scrolled = Gtk.ScrolledWindow()
scrolled.set_child(self.list_view) 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) 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) self.set_child(self.stack)
# Enable all watch methods.
self.watch_all() self.watch_all()
@property # Populate the list store.
def now(self) -> float: self._populate_file_list()
frame_clock = self.get_frame_clock()
if frame_clock is None:
return 0
return frame_clock.get_frame_time() / 1_000_000
@property @property
def selection(self) -> FileItem | None: def selection(self) -> FileItem | None:
@ -285,6 +200,33 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
item.connect("notify::saved-duration", update_icon) item.connect("notify::saved-duration", update_icon)
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): def _restore_selection(self):
pos = self.selection_history.get(os.getcwd(), 0) pos = self.selection_history.get(os.getcwd(), 0)
self.selection_model.set_selected(pos) self.selection_model.set_selected(pos)
@ -327,14 +269,14 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
if file_item.thumbnail.value: if file_item.thumbnail.value:
gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value)) gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
texture = Gdk.Texture.new_from_bytes(gbytes) texture = Gdk.Texture.new_from_bytes(gbytes)
self.thumbnail_image.set_paintable(texture) self.thumbnail_picture.set_paintable(texture)
else: else:
self.thumbnailer.generate_thumbnail(file_item) self.thumbnailer.generate_thumbnail(file_item)
self.thumbnail_image.set_paintable(None) self.thumbnail_picture.set_paintable(None)
def _toggle_play_pause(self) -> None: def _toggle_play_pause(self) -> None:
"""Toggle between play and pause states""" """Toggle between play and pause states"""
self.video_player.toggle_play_pause() self.player.toggle_play_pause()
def _on_player_key_pressed( def _on_player_key_pressed(
self, self,
@ -348,62 +290,62 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
elif keyval == Gdk.keyval_from_name("Escape"): elif keyval == Gdk.keyval_from_name("Escape"):
self._save_position() self._save_position()
self.video_player.stop() self.player.stop()
self.stack.set_visible_child_name("menu") self.stack.set_visible_child_name("menu")
self.list_view.grab_focus() self.list_view.grab_focus()
return True return True
elif keyval == Gdk.keyval_from_name("Left"): elif keyval == Gdk.keyval_from_name("Left"):
self.video_player.seek_relative(-10) self.player.seek_relative(-10)
return True return True
elif keyval == Gdk.keyval_from_name("Right"): elif keyval == Gdk.keyval_from_name("Right"):
self.video_player.seek_relative(10) self.player.seek_relative(10)
return True return True
elif keyval == Gdk.keyval_from_name("Down"): elif keyval == Gdk.keyval_from_name("Down"):
self.video_player.seek_relative(-60) self.player.seek_relative(-60)
return True return True
elif keyval == Gdk.keyval_from_name("Up"): elif keyval == Gdk.keyval_from_name("Up"):
self.video_player.seek_relative(60) self.player.seek_relative(60)
return True return True
elif keyval == Gdk.keyval_from_name("Home"): elif keyval == Gdk.keyval_from_name("Home"):
self.video_player.seek_start() self.player.seek_start()
return True return True
elif keyval == Gdk.keyval_from_name("End"): elif keyval == Gdk.keyval_from_name("End"):
self.video_player.seek_end() self.player.seek_end()
return True return True
elif keyval == Gdk.keyval_from_name("j"): 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 has_subs:
if index: if index:
self.show_overlay_text(f"Subtitles #{index} ({lang})") self.overlay.show_message(f"Subtitles #{index} ({lang})")
else: else:
self.show_overlay_text("Subtitles turned off") self.overlay.show_message("Subtitles turned off")
file_item = self.selection file_item = self.selection
if file_item is not None: if file_item is not None:
file_item.saved_subtitle_track.value = index - 1 file_item.saved_subtitle_track.value = index - 1
else: else:
self.show_overlay_text("No subtitles available") self.overlay.show_message("No subtitles available")
return True return True
elif keyval == Gdk.keyval_from_name("a"): 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: if has_audio:
self.show_overlay_text(f"Audio #{index} ({lang})") self.overlay.show_message(f"Audio #{index} ({lang})")
file_item = self.selection file_item = self.selection
if file_item is not None: if file_item is not None:
file_item.saved_audio_track.value = index - 1 file_item.saved_audio_track.value = index - 1
else: else:
self.show_overlay_text("No audio tracks available") self.overlay.show_message("No audio tracks available")
return True return True
@ -470,7 +412,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher):
def _save_position(self) -> None: def _save_position(self) -> None:
"""Save current playback position as xattr""" """Save current playback position as xattr"""
if not self.video_player.is_playing: if not self.player.is_playing:
return return
file_item = self.selection 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: if file_item is None or file_item.file_type == FileType.DIRECTORY:
return return
position = self.video_player.get_position() position = self.player.get_position()
if position is not None: if position is not None:
file_item.saved_position.value = position file_item.saved_position.value = position
duration = self.video_player.get_duration() duration = self.player.get_duration()
if duration is not None: if duration is not None:
file_item.saved_duration.value = duration 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: 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() 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.now = frame_clock.get_frame_time() / 1_000_000
self.main_clock.set_text(current_time_str)
self.grid_clock.set_text(current_time_str)
if current_time >= self.overlay_hide_time: # Save playback position every 60 seconds.
self.overlay_label.set_visible(False) if self.now - self.last_position_save >= 60.0:
# Save position every 60 seconds
frame_time = frame_clock.get_frame_time() / 1_000_000
if frame_time - self.last_position_save >= 60.0:
self._save_position() self._save_position()
self.last_position_save = frame_time self.last_position_save = self.now
# Update thumbnail if available # Update thumbnail if available
if file_item := self.selection: if file_item := self.selection:
self.thumbnailer.generate_thumbnail(file_item) 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)) gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value))
texture = Gdk.Texture.new_from_bytes(gbytes) texture = Gdk.Texture.new_from_bytes(gbytes)
self.thumbnail_image.set_paintable(texture) self.thumbnail_picture.set_paintable(texture)
return True return True

View file

@ -20,11 +20,11 @@ listview > row {
color: #0f0; color: #0f0;
} }
#black-overlay { #overlay {
background-color: black; background-color: black;
} }
#overlay-text { #overlay-message {
color: white; color: white;
font-size: 24px; font-size: 24px;
font-family: monospace; font-family: monospace;
@ -34,19 +34,19 @@ listview > row {
margin: 32px; margin: 32px;
} }
#thumbnail-image { #thumbnail-picture {
margin: 8px; margin: 8px;
} }
#digital-clock, #main-clock,
#grid-clock { #overlay-clock {
color: white; color: white;
font-size: 48px; font-size: 48px;
font-family: monospace; font-family: monospace;
padding: 12px; padding: 12px;
} }
#grid-clock { #overlay-clock {
background-color: rgba(32, 32, 32, 0.5); background-color: rgba(32, 32, 32, 0.5);
border-radius: 8px; border-radius: 8px;
} }

View 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

View file

@ -10,6 +10,7 @@ DEFAULT_SEEK_FLAGS = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT
class VideoPlayer(GObject.Object): class VideoPlayer(GObject.Object):
picture: Gtk.Picture
pipeline: Gst.Pipeline pipeline: Gst.Pipeline
playbin: Gst.Element playbin: Gst.Element
@ -21,6 +22,8 @@ class VideoPlayer(GObject.Object):
def __init__(self, picture: Gtk.Picture): def __init__(self, picture: Gtk.Picture):
super().__init__() super().__init__()
self.picture = picture
self.is_playing = Ref(False) self.is_playing = Ref(False)
self.is_paused = Ref(True) self.is_paused = Ref(True)
@ -41,7 +44,7 @@ class VideoPlayer(GObject.Object):
# Link picture to sink # Link picture to sink
paintable = video_sink.get_property("paintable") paintable = video_sink.get_property("paintable")
picture.set_paintable(paintable) self.picture.set_paintable(paintable)
def play( def play(
self, self,