Skip to content

Commit

Permalink
Refactor video_player.py (Fix #270) (#274)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
CyanVoxel authored Jun 13, 2024
1 parent 37ff35f commit 65d88b9
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 65 deletions.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
67 changes: 67 additions & 0 deletions tagstudio/src/qt/resource_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

import logging
from pathlib import Path
from typing import Any

import ujson

logging.basicConfig(format="%(message)s", level=logging.INFO)


class ResourceManager:
"""A resource manager for retrieving resources."""

_map: dict = {}
_cache: dict[str, Any] = {}
_initialized: bool = False

def __init__(self) -> None:
# Load JSON resource map
if not ResourceManager._initialized:
with open(
Path(__file__).parent / "resources.json", mode="r", encoding="utf-8"
) as f:
ResourceManager._map = ujson.load(f)
logging.info(
f"[ResourceManager] {len(ResourceManager._map.items())} resources registered"
)
ResourceManager._initialized = True

def get(self, id: str) -> Any:
"""Get a resource from the ResourceManager.
This can include resources inside and outside of QResources, and will return
theme-respecting variations of resources if available.
Args:
id (str): The name of the resource.
Returns:
Any: The resource if found, else None.
"""
cached_res = ResourceManager._cache.get(id)
if cached_res:
return cached_res
else:
res: dict = ResourceManager._map.get(id)
if res.get("mode") in ["r", "rb"]:
with open(
(Path(__file__).parents[2] / "resources" / res.get("path")),
res.get("mode"),
) as f:
data = f.read()
if res.get("mode") == "rb":
data = bytes(data)
ResourceManager._cache[id] = data
return data
elif res.get("mode") in ["qt"]:
# TODO: Qt resource loading logic
pass

def __getattr__(self, __name: str) -> Any:
attr = self.get(__name)
if attr:
return attr
raise AttributeError(f"Attribute {id} not found")
18 changes: 18 additions & 0 deletions tagstudio/src/qt/resources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"play_icon": {
"path": "qt/images/play.svg",
"mode": "rb"
},
"pause_icon": {
"path": "qt/images/pause.svg",
"mode": "rb"
},
"volume_icon": {
"path": "qt/images/volume.svg",
"mode": "rb"
},
"volume_mute_icon": {
"path": "qt/images/volume_mute.svg",
"mode": "rb"
}
}
2 changes: 2 additions & 0 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from src.qt.main_window import Ui_MainWindow
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.resource_manager import ResourceManager
from src.qt.widgets.collage_icon import CollageIconRenderer
from src.qt.widgets.panel import PanelModal
from src.qt.widgets.thumb_renderer import ThumbRenderer
Expand Down Expand Up @@ -164,6 +165,7 @@ def __init__(self, core: TagStudioCore, args):
super().__init__()
self.core: TagStudioCore = core
self.lib = self.core.lib
self.rm: ResourceManager = ResourceManager()
self.args = args
self.frame_dict: dict = {}
self.nav_frames: list[NavigationState] = []
Expand Down
107 changes: 42 additions & 65 deletions tagstudio/src/qt/widgets/video_player.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

import logging
import os
import typing

# os.environ["QT_MEDIA_BACKEND"] = "ffmpeg"
from pathlib import Path
import typing

from PySide6.QtCore import (
Qt,
Expand All @@ -18,7 +20,6 @@
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene
from PySide6.QtGui import (
QInputMethodEvent,
QPen,
QColor,
QBrush,
Expand All @@ -29,10 +30,7 @@
QBitmap,
)
from PySide6.QtSvgWidgets import QSvgWidget
from PIL import Image
from src.qt.helpers.file_opener import FileOpenerHelper

from src.core.constants import VIDEO_TYPES, AUDIO_TYPES
from PIL import Image, ImageDraw
from src.core.enums import SettingItems

Expand All @@ -41,26 +39,26 @@


class VideoPlayer(QGraphicsView):
"""A simple video player for the TagStudio application."""
"""A basic video player."""

resolution = QSize(1280, 720)
hover_fix_timer = QTimer()
video_preview = None
play_pause = None
mute_button = None
content_visible = False
filepath = None

def __init__(self, driver: "QtDriver") -> None:
# Set up the base class.
super().__init__()
self.driver = driver
self.resolution = QSize(1280, 720)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(
lambda value: self.setTintTransparency(value)
)
self.hover_fix_timer = QTimer()
self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered())
self.hover_fix_timer.setSingleShot(True)
self.content_visible = False
self.filepath = None

# Set up the video player.
self.installEventFilter(self)
self.setScene(QGraphicsScene(self))
Expand All @@ -82,6 +80,7 @@ def __init__(self, driver: "QtDriver") -> None:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scene().addItem(self.video_preview)
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)

# Set up the video tint.
self.video_tint = self.scene().addRect(
0,
Expand All @@ -91,44 +90,31 @@ def __init__(self, driver: "QtDriver") -> None:
QPen(QColor(0, 0, 0, 0)),
QBrush(QColor(0, 0, 0, 0)),
)
# self.video_tint.setParentItem(self.video_preview)
# self.album_art = QGraphicsPixmapItem(self.video_preview)
# self.scene().addItem(self.album_art)
# self.album_art.setPixmap(
# QPixmap("./tagstudio/resources/qt/images/thumb_file_default_512.png")
# )
# self.album_art.setOpacity(0.0)

# Set up the buttons.
self.play_pause = QSvgWidget("./tagstudio/resources/pause.svg")
self.play_pause = QSvgWidget()
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.play_pause.setMouseTracking(True)
self.play_pause.installEventFilter(self)
self.scene().addWidget(self.play_pause)
self.play_pause.resize(100, 100)
self.play_pause.resize(72, 72)
self.play_pause.move(
int(self.width() / 2 - self.play_pause.size().width() / 2),
int(self.height() / 2 - self.play_pause.size().height() / 2),
)
self.play_pause.hide()

self.mute_button = QSvgWidget("./tagstudio/resources/volume_muted.svg")
self.mute_button = QSvgWidget()
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.mute_button.setMouseTracking(True)
self.mute_button.installEventFilter(self)
self.scene().addWidget(self.mute_button)
self.mute_button.resize(40, 40)
self.mute_button.resize(32, 32)
self.mute_button.move(
int(self.width() - self.mute_button.size().width() / 2),
int(self.height() - self.mute_button.size().height() / 2),
)
self.mute_button.hide()
# self.fullscreen_button = QSvgWidget('./tagstudio/resources/pause.svg', self)
# self.fullscreen_button.setMouseTracking(True)
# self.fullscreen_button.installEventFilter(self)
# self.scene().addWidget(self.fullscreen_button)
# self.fullscreen_button.resize(40, 40)
# self.fullscreen_button.move(self.fullscreen_button.size().width()/2, self.height() - self.fullscreen_button.size().height()/2)
# self.fullscreen_button.hide()

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

def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None:
# logging.info(media_status)
if media_status == QMediaPlayer.MediaStatus.EndOfMedia:
# 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.
# Switches current video to with video at filepath.
# Reason for this is because Pyside6 can't handle setting a new source and freezes.
# Even if I stop the player before switching, it breaks.
# On the plus side, this adds infinite looping for the video preview.
self.player.stop()
self.player.setSource(QUrl().fromLocalFile(self.filepath))
# logging.info(f'Set source to {self.filepath}.')
# self.video_preview.setSize(self.resolution)
self.player.setPosition(0)
# logging.info(f'Set muted to true.')
if self.autoplay.isChecked():
# logging.info(self.driver.settings.value("autoplay_videos", True, bool))
self.player.play()
else:
# logging.info("Paused")
self.player.pause()
self.opener.set_filepath(self.filepath)
self.keepControlsInPlace()
self.updateControls()

def updateControls(self) -> None:
if self.player.audioOutput().isMuted():
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
self.mute_button.load(self.driver.rm.volume_mute_icon)
else:
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
self.mute_button.load(self.driver.rm.volume_icon)

if self.player.isPlaying():
self.play_pause.load("./tagstudio/resources/pause.svg")
self.play_pause.load(self.driver.rm.pause_icon)
else:
self.play_pause.load("./tagstudio/resources/play.svg")
self.play_pause.load(self.driver.rm.play_icon)

def wheelEvent(self, event: QWheelEvent) -> None:
return
Expand Down Expand Up @@ -229,8 +210,10 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
return super().eventFilter(obj, event)

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

def underMouse(self) -> bool:
# logging.info("under mouse")
self.animation.setStartValue(self.video_tint.brush().color().alpha())
self.animation.setEndValue(100)
self.animation.setDuration(500)
self.animation.setDuration(250)
self.animation.start()
self.play_pause.show()
self.mute_button.show()
# self.fullscreen_button.show()
self.keepControlsInPlace()
self.updateControls()
# rcontent = self.contentsRect()
# self.setSceneRect(0, 0, rcontent.width(), rcontent.height())

return super().underMouse()

def releaseMouse(self) -> None:
# logging.info("release mouse")
self.animation.setStartValue(self.video_tint.brush().color().alpha())
self.animation.setEndValue(0)
self.animation.setDuration(500)
self.animation.start()
self.play_pause.hide()
self.mute_button.hide()
# self.fullscreen_button.hide()

return super().releaseMouse()

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

def pauseToggle(self) -> None:
if self.player.isPlaying():
self.player.pause()
self.play_pause.load("./tagstudio/resources/play.svg")
self.play_pause.load(self.driver.rm.play_icon)
else:
self.player.play()
self.play_pause.load("./tagstudio/resources/pause.svg")
self.play_pause.load(self.driver.rm.pause_icon)

def muteToggle(self) -> None:
if self.player.audioOutput().isMuted():
self.player.audioOutput().setMuted(False)
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
self.mute_button.load(self.driver.rm.volume_icon)
else:
self.player.audioOutput().setMuted(True)
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
self.mute_button.load(self.driver.rm.volume_mute_icon)

def play(self, filepath: str, resolution: QSize) -> None:
# Sets the filepath and sends the current player position to the very end, so that the new video can be played.
# self.player.audioOutput().setMuted(True)
# Sets the filepath and sends the current player position to the very end,
# so that the new video can be played.
logging.info(f"Playing {filepath}")
self.resolution = resolution
self.filepath = filepath
Expand All @@ -297,7 +276,6 @@ def play(self, filepath: str, resolution: QSize) -> None:
self.player.play()
else:
self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)
# logging.info(f"Successfully stopped.")

def stop(self) -> None:
self.filepath = None
Expand All @@ -310,10 +288,10 @@ def resizeVideo(self, new_size: QSize) -> None:
0, 0, self.video_preview.size().width(), self.video_preview.size().height()
)

rcontent = self.contentsRect()
contents = self.contentsRect()
self.centerOn(self.video_preview)
self.roundCorners()
self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
self.setSceneRect(0, 0, contents.width(), contents.height())
self.keepControlsInPlace()

def roundCorners(self) -> None:
Expand Down Expand Up @@ -346,7 +324,6 @@ def keepControlsInPlace(self) -> None:
int(self.width() - self.mute_button.size().width() - 10),
int(self.height() - self.mute_button.size().height() - 10),
)
# self.fullscreen_button.move(-self.fullscreen_button.size().width()-10, self.height() - self.fullscreen_button.size().height()-10)

def resizeEvent(self, event: QResizeEvent) -> None:
# Keeps the video preview in the center of the screen.
Expand All @@ -358,7 +335,6 @@ def resizeEvent(self, event: QResizeEvent) -> None:
)
)
return
# return super().resizeEvent(event)\


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

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

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

0 comments on commit 65d88b9

Please sign in to comment.