Skip to content

Commit 65d88b9

Browse files
authored
Refactor video_player.py (Fix #270) (#274)
* Refactor video_player.py - Move icons files to qt/images folder, some being renamed - Reduce icon loading to single initial import - Tweak icon dimensions and animation timings - Remove unnecessary commented code - Remove unused/duplicate imports - Add license info to file * Add basic ResourceManager, use in video_player.py * Revert tagstudio.spec changes * Change tuple usage to dicts * Move ResourceManager initialization steps * Fix errant list notation
1 parent 37ff35f commit 65d88b9

File tree

8 files changed

+129
-65
lines changed

8 files changed

+129
-65
lines changed
File renamed without changes.

tagstudio/src/qt/resource_manager.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
5+
import logging
6+
from pathlib import Path
7+
from typing import Any
8+
9+
import ujson
10+
11+
logging.basicConfig(format="%(message)s", level=logging.INFO)
12+
13+
14+
class ResourceManager:
15+
"""A resource manager for retrieving resources."""
16+
17+
_map: dict = {}
18+
_cache: dict[str, Any] = {}
19+
_initialized: bool = False
20+
21+
def __init__(self) -> None:
22+
# Load JSON resource map
23+
if not ResourceManager._initialized:
24+
with open(
25+
Path(__file__).parent / "resources.json", mode="r", encoding="utf-8"
26+
) as f:
27+
ResourceManager._map = ujson.load(f)
28+
logging.info(
29+
f"[ResourceManager] {len(ResourceManager._map.items())} resources registered"
30+
)
31+
ResourceManager._initialized = True
32+
33+
def get(self, id: str) -> Any:
34+
"""Get a resource from the ResourceManager.
35+
This can include resources inside and outside of QResources, and will return
36+
theme-respecting variations of resources if available.
37+
38+
Args:
39+
id (str): The name of the resource.
40+
41+
Returns:
42+
Any: The resource if found, else None.
43+
"""
44+
cached_res = ResourceManager._cache.get(id)
45+
if cached_res:
46+
return cached_res
47+
else:
48+
res: dict = ResourceManager._map.get(id)
49+
if res.get("mode") in ["r", "rb"]:
50+
with open(
51+
(Path(__file__).parents[2] / "resources" / res.get("path")),
52+
res.get("mode"),
53+
) as f:
54+
data = f.read()
55+
if res.get("mode") == "rb":
56+
data = bytes(data)
57+
ResourceManager._cache[id] = data
58+
return data
59+
elif res.get("mode") in ["qt"]:
60+
# TODO: Qt resource loading logic
61+
pass
62+
63+
def __getattr__(self, __name: str) -> Any:
64+
attr = self.get(__name)
65+
if attr:
66+
return attr
67+
raise AttributeError(f"Attribute {id} not found")

tagstudio/src/qt/resources.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"play_icon": {
3+
"path": "qt/images/play.svg",
4+
"mode": "rb"
5+
},
6+
"pause_icon": {
7+
"path": "qt/images/pause.svg",
8+
"mode": "rb"
9+
},
10+
"volume_icon": {
11+
"path": "qt/images/volume.svg",
12+
"mode": "rb"
13+
},
14+
"volume_mute_icon": {
15+
"path": "qt/images/volume_mute.svg",
16+
"mode": "rb"
17+
}
18+
}

tagstudio/src/qt/ts_qt.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from src.qt.main_window import Ui_MainWindow
7171
from src.qt.helpers.function_iterator import FunctionIterator
7272
from src.qt.helpers.custom_runnable import CustomRunnable
73+
from src.qt.resource_manager import ResourceManager
7374
from src.qt.widgets.collage_icon import CollageIconRenderer
7475
from src.qt.widgets.panel import PanelModal
7576
from src.qt.widgets.thumb_renderer import ThumbRenderer
@@ -164,6 +165,7 @@ def __init__(self, core: TagStudioCore, args):
164165
super().__init__()
165166
self.core: TagStudioCore = core
166167
self.lib = self.core.lib
168+
self.rm: ResourceManager = ResourceManager()
167169
self.args = args
168170
self.frame_dict: dict = {}
169171
self.nav_frames: list[NavigationState] = []

tagstudio/src/qt/widgets/video_player.py

Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
# Licensed under the GPL-3.0 License.
2+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
3+
14
import logging
2-
import os
3-
import typing
45

5-
# os.environ["QT_MEDIA_BACKEND"] = "ffmpeg"
6+
from pathlib import Path
7+
import typing
68

79
from PySide6.QtCore import (
810
Qt,
@@ -18,7 +20,6 @@
1820
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
1921
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene
2022
from PySide6.QtGui import (
21-
QInputMethodEvent,
2223
QPen,
2324
QColor,
2425
QBrush,
@@ -29,10 +30,7 @@
2930
QBitmap,
3031
)
3132
from PySide6.QtSvgWidgets import QSvgWidget
32-
from PIL import Image
3333
from src.qt.helpers.file_opener import FileOpenerHelper
34-
35-
from src.core.constants import VIDEO_TYPES, AUDIO_TYPES
3634
from PIL import Image, ImageDraw
3735
from src.core.enums import SettingItems
3836

@@ -41,26 +39,26 @@
4139

4240

4341
class VideoPlayer(QGraphicsView):
44-
"""A simple video player for the TagStudio application."""
42+
"""A basic video player."""
4543

46-
resolution = QSize(1280, 720)
47-
hover_fix_timer = QTimer()
4844
video_preview = None
4945
play_pause = None
5046
mute_button = None
51-
content_visible = False
52-
filepath = None
5347

5448
def __init__(self, driver: "QtDriver") -> None:
55-
# Set up the base class.
5649
super().__init__()
5750
self.driver = driver
51+
self.resolution = QSize(1280, 720)
5852
self.animation = QVariantAnimation(self)
5953
self.animation.valueChanged.connect(
6054
lambda value: self.setTintTransparency(value)
6155
)
56+
self.hover_fix_timer = QTimer()
6257
self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered())
6358
self.hover_fix_timer.setSingleShot(True)
59+
self.content_visible = False
60+
self.filepath = None
61+
6462
# Set up the video player.
6563
self.installEventFilter(self)
6664
self.setScene(QGraphicsScene(self))
@@ -82,6 +80,7 @@ def __init__(self, driver: "QtDriver") -> None:
8280
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
8381
self.scene().addItem(self.video_preview)
8482
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
83+
8584
# Set up the video tint.
8685
self.video_tint = self.scene().addRect(
8786
0,
@@ -91,44 +90,31 @@ def __init__(self, driver: "QtDriver") -> None:
9190
QPen(QColor(0, 0, 0, 0)),
9291
QBrush(QColor(0, 0, 0, 0)),
9392
)
94-
# self.video_tint.setParentItem(self.video_preview)
95-
# self.album_art = QGraphicsPixmapItem(self.video_preview)
96-
# self.scene().addItem(self.album_art)
97-
# self.album_art.setPixmap(
98-
# QPixmap("./tagstudio/resources/qt/images/thumb_file_default_512.png")
99-
# )
100-
# self.album_art.setOpacity(0.0)
93+
10194
# Set up the buttons.
102-
self.play_pause = QSvgWidget("./tagstudio/resources/pause.svg")
95+
self.play_pause = QSvgWidget()
10396
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
10497
self.play_pause.setMouseTracking(True)
10598
self.play_pause.installEventFilter(self)
10699
self.scene().addWidget(self.play_pause)
107-
self.play_pause.resize(100, 100)
100+
self.play_pause.resize(72, 72)
108101
self.play_pause.move(
109102
int(self.width() / 2 - self.play_pause.size().width() / 2),
110103
int(self.height() / 2 - self.play_pause.size().height() / 2),
111104
)
112105
self.play_pause.hide()
113106

114-
self.mute_button = QSvgWidget("./tagstudio/resources/volume_muted.svg")
107+
self.mute_button = QSvgWidget()
115108
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
116109
self.mute_button.setMouseTracking(True)
117110
self.mute_button.installEventFilter(self)
118111
self.scene().addWidget(self.mute_button)
119-
self.mute_button.resize(40, 40)
112+
self.mute_button.resize(32, 32)
120113
self.mute_button.move(
121114
int(self.width() - self.mute_button.size().width() / 2),
122115
int(self.height() - self.mute_button.size().height() / 2),
123116
)
124117
self.mute_button.hide()
125-
# self.fullscreen_button = QSvgWidget('./tagstudio/resources/pause.svg', self)
126-
# self.fullscreen_button.setMouseTracking(True)
127-
# self.fullscreen_button.installEventFilter(self)
128-
# self.scene().addWidget(self.fullscreen_button)
129-
# self.fullscreen_button.resize(40, 40)
130-
# self.fullscreen_button.move(self.fullscreen_button.size().width()/2, self.height() - self.fullscreen_button.size().height()/2)
131-
# self.fullscreen_button.hide()
132118

133119
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
134120
self.opener = FileOpenerHelper(filepath=self.filepath)
@@ -157,37 +143,32 @@ def toggleAutoplay(self) -> None:
157143
self.driver.settings.sync()
158144

159145
def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None:
160-
# logging.info(media_status)
161146
if media_status == QMediaPlayer.MediaStatus.EndOfMedia:
162-
# Switches current video to with video at filepath. Reason for this is because Pyside6 is dumb and can't handle setting a new source and freezes.
147+
# Switches current video to with video at filepath.
148+
# Reason for this is because Pyside6 can't handle setting a new source and freezes.
163149
# Even if I stop the player before switching, it breaks.
164150
# On the plus side, this adds infinite looping for the video preview.
165151
self.player.stop()
166152
self.player.setSource(QUrl().fromLocalFile(self.filepath))
167-
# logging.info(f'Set source to {self.filepath}.')
168-
# self.video_preview.setSize(self.resolution)
169153
self.player.setPosition(0)
170-
# logging.info(f'Set muted to true.')
171154
if self.autoplay.isChecked():
172-
# logging.info(self.driver.settings.value("autoplay_videos", True, bool))
173155
self.player.play()
174156
else:
175-
# logging.info("Paused")
176157
self.player.pause()
177158
self.opener.set_filepath(self.filepath)
178159
self.keepControlsInPlace()
179160
self.updateControls()
180161

181162
def updateControls(self) -> None:
182163
if self.player.audioOutput().isMuted():
183-
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
164+
self.mute_button.load(self.driver.rm.volume_mute_icon)
184165
else:
185-
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
166+
self.mute_button.load(self.driver.rm.volume_icon)
186167

187168
if self.player.isPlaying():
188-
self.play_pause.load("./tagstudio/resources/pause.svg")
169+
self.play_pause.load(self.driver.rm.pause_icon)
189170
else:
190-
self.play_pause.load("./tagstudio/resources/play.svg")
171+
self.play_pause.load(self.driver.rm.play_icon)
191172

192173
def wheelEvent(self, event: QWheelEvent) -> None:
193174
return
@@ -229,8 +210,10 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
229210
return super().eventFilter(obj, event)
230211

231212
def checkIfStillHovered(self) -> None:
232-
# Yet again, Pyside6 is dumb. I don't know why, but the HoverLeave event is not triggered sometimes and does not hide the controls.
233-
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse is still in the video preview.
213+
# I don't know why, but the HoverLeave event is not triggered sometimes
214+
# and does not hide the controls.
215+
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse
216+
# is still in the video preview.
234217
if not self.video_preview.isUnderMouse():
235218
self.releaseMouse()
236219
else:
@@ -240,55 +223,51 @@ def setTintTransparency(self, value) -> None:
240223
self.video_tint.setBrush(QBrush(QColor(0, 0, 0, value)))
241224

242225
def underMouse(self) -> bool:
243-
# logging.info("under mouse")
244226
self.animation.setStartValue(self.video_tint.brush().color().alpha())
245227
self.animation.setEndValue(100)
246-
self.animation.setDuration(500)
228+
self.animation.setDuration(250)
247229
self.animation.start()
248230
self.play_pause.show()
249231
self.mute_button.show()
250-
# self.fullscreen_button.show()
251232
self.keepControlsInPlace()
252233
self.updateControls()
253-
# rcontent = self.contentsRect()
254-
# self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
234+
255235
return super().underMouse()
256236

257237
def releaseMouse(self) -> None:
258-
# logging.info("release mouse")
259238
self.animation.setStartValue(self.video_tint.brush().color().alpha())
260239
self.animation.setEndValue(0)
261240
self.animation.setDuration(500)
262241
self.animation.start()
263242
self.play_pause.hide()
264243
self.mute_button.hide()
265-
# self.fullscreen_button.hide()
244+
266245
return super().releaseMouse()
267246

268247
def resetControlsToDefault(self) -> None:
269248
# Resets the video controls to their default state.
270-
self.play_pause.load("./tagstudio/resources/pause.svg")
271-
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
249+
self.play_pause.load(self.driver.rm.pause_icon)
250+
self.mute_button.load(self.driver.rm.volume_mute_icon)
272251

273252
def pauseToggle(self) -> None:
274253
if self.player.isPlaying():
275254
self.player.pause()
276-
self.play_pause.load("./tagstudio/resources/play.svg")
255+
self.play_pause.load(self.driver.rm.play_icon)
277256
else:
278257
self.player.play()
279-
self.play_pause.load("./tagstudio/resources/pause.svg")
258+
self.play_pause.load(self.driver.rm.pause_icon)
280259

281260
def muteToggle(self) -> None:
282261
if self.player.audioOutput().isMuted():
283262
self.player.audioOutput().setMuted(False)
284-
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
263+
self.mute_button.load(self.driver.rm.volume_icon)
285264
else:
286265
self.player.audioOutput().setMuted(True)
287-
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
266+
self.mute_button.load(self.driver.rm.volume_mute_icon)
288267

289268
def play(self, filepath: str, resolution: QSize) -> None:
290-
# Sets the filepath and sends the current player position to the very end, so that the new video can be played.
291-
# self.player.audioOutput().setMuted(True)
269+
# Sets the filepath and sends the current player position to the very end,
270+
# so that the new video can be played.
292271
logging.info(f"Playing {filepath}")
293272
self.resolution = resolution
294273
self.filepath = filepath
@@ -297,7 +276,6 @@ def play(self, filepath: str, resolution: QSize) -> None:
297276
self.player.play()
298277
else:
299278
self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)
300-
# logging.info(f"Successfully stopped.")
301279

302280
def stop(self) -> None:
303281
self.filepath = None
@@ -310,10 +288,10 @@ def resizeVideo(self, new_size: QSize) -> None:
310288
0, 0, self.video_preview.size().width(), self.video_preview.size().height()
311289
)
312290

313-
rcontent = self.contentsRect()
291+
contents = self.contentsRect()
314292
self.centerOn(self.video_preview)
315293
self.roundCorners()
316-
self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
294+
self.setSceneRect(0, 0, contents.width(), contents.height())
317295
self.keepControlsInPlace()
318296

319297
def roundCorners(self) -> None:
@@ -346,7 +324,6 @@ def keepControlsInPlace(self) -> None:
346324
int(self.width() - self.mute_button.size().width() - 10),
347325
int(self.height() - self.mute_button.size().height() - 10),
348326
)
349-
# self.fullscreen_button.move(-self.fullscreen_button.size().width()-10, self.height() - self.fullscreen_button.size().height()-10)
350327

351328
def resizeEvent(self, event: QResizeEvent) -> None:
352329
# Keeps the video preview in the center of the screen.
@@ -358,7 +335,6 @@ def resizeEvent(self, event: QResizeEvent) -> None:
358335
)
359336
)
360337
return
361-
# return super().resizeEvent(event)\
362338

363339

364340
class VideoPreview(QGraphicsVideoItem):
@@ -367,7 +343,8 @@ def boundingRect(self):
367343

368344
def paint(self, painter, option, widget):
369345
# painter.brush().setColor(QColor(0, 0, 0, 255))
370-
# You can set any shape you want here. RoundedRect is the standard rectangle with rounded corners
346+
# You can set any shape you want here.
347+
# RoundedRect is the standard rectangle with rounded corners.
371348
# With 2nd and 3rd parameter you can tweak the curve until you get what you expect
372349

373350
super().paint(painter, option, widget)

0 commit comments

Comments
 (0)