Skip to content

Refactor video_player.py (Fix #270) #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)