-
-
Notifications
You must be signed in to change notification settings - Fork 461
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
Changes from 3 commits
f41e7d0
cd3709a
a99476a
cadbceb
5037530
369d212
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # 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[str, tuple[str, str]] = {} # dict[<id>, tuple[<filepath>,<mode>]] | ||
| _cache: dict[str, Any] = {} | ||
|
|
||
| # Initialize _map | ||
| with open( | ||
| Path(__file__).parent / "resources.json", mode="r", encoding="utf-8" | ||
| ) as f: | ||
| json_map: dict = ujson.load(f) | ||
| for item in json_map.items(): | ||
| _map[item[0]] = (item[1]["path"], item[1]["mode"]) | ||
|
|
||
| logging.info(f"[ResourceManager] Resources Loaded: {_map}") | ||
|
|
||
| def __init__(self) -> None: | ||
| pass | ||
|
|
||
| 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, None) | ||
| if cached_res: | ||
| return cached_res | ||
| else: | ||
| path, mode = ResourceManager._map.get(id, None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will raise exception in case the resource is not present in storing the data as dictionary inside btw. the default fallback value of
|
||
| if mode in ["r", "rb"]: | ||
| with open((Path(__file__).parents[2] / "resources" / path), mode) as f: | ||
| data = f.read() | ||
|
|
||
| if mode == ["rb"]: | ||
| data = bytes(data) | ||
|
|
||
| ResourceManager._cache[id] = data | ||
| return data | ||
| elif 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") | ||
| 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" | ||
| } | ||
| } |
| 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, | ||
|
|
@@ -18,7 +20,6 @@ | |
| from PySide6.QtMultimediaWidgets import QGraphicsVideoItem | ||
| from PySide6.QtWidgets import QGraphicsView, QGraphicsScene | ||
| from PySide6.QtGui import ( | ||
| QInputMethodEvent, | ||
| QPen, | ||
| QColor, | ||
| QBrush, | ||
|
|
@@ -29,38 +30,37 @@ | |
| 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 | ||
| from src.qt.resource_manager import ResourceManager | ||
|
|
||
| if typing.TYPE_CHECKING: | ||
| from src.qt.ts_qt import QtDriver | ||
|
|
||
|
|
||
| class VideoPlayer(QGraphicsView): | ||
| """A simple video player for the TagStudio application.""" | ||
| """A basic video player.""" | ||
|
|
||
| resolution = QSize(1280, 720) | ||
| hover_fix_timer = QTimer() | ||
| rm: ResourceManager = ResourceManager() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd initialize this class inside |
||
| 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)) | ||
|
|
@@ -82,6 +82,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, | ||
|
|
@@ -91,44 +92,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) | ||
|
|
@@ -157,37 +145,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(VideoPlayer.rm.volume_mute_icon) | ||
| else: | ||
| self.mute_button.load("./tagstudio/resources/volume_unmuted.svg") | ||
| self.mute_button.load(VideoPlayer.rm.volume_icon) | ||
|
|
||
| if self.player.isPlaying(): | ||
| self.play_pause.load("./tagstudio/resources/pause.svg") | ||
| self.play_pause.load(VideoPlayer.rm.pause_icon) | ||
| else: | ||
| self.play_pause.load("./tagstudio/resources/play.svg") | ||
| self.play_pause.load(VideoPlayer.rm.play_icon) | ||
|
|
||
| def wheelEvent(self, event: QWheelEvent) -> None: | ||
| return | ||
|
|
@@ -229,8 +212,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: | ||
|
|
@@ -240,55 +225,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(VideoPlayer.rm.pause_icon) | ||
| self.mute_button.load(VideoPlayer.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(VideoPlayer.rm.play_icon) | ||
| else: | ||
| self.player.play() | ||
| self.play_pause.load("./tagstudio/resources/pause.svg") | ||
| self.play_pause.load(VideoPlayer.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(VideoPlayer.rm.volume_icon) | ||
| else: | ||
| self.player.audioOutput().setMuted(True) | ||
| self.mute_button.load("./tagstudio/resources/volume_muted.svg") | ||
| self.mute_button.load(VideoPlayer.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 | ||
|
|
@@ -297,7 +278,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 | ||
|
|
@@ -310,10 +290,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: | ||
|
|
@@ -346,7 +326,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. | ||
|
|
@@ -358,7 +337,6 @@ def resizeEvent(self, event: QResizeEvent) -> None: | |
| ) | ||
| ) | ||
| return | ||
| # return super().resizeEvent(event)\ | ||
|
|
||
|
|
||
| class VideoPreview(QGraphicsVideoItem): | ||
|
|
@@ -367,7 +345,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) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would be better to do in constructor, otherwise doing
importof this class will have "side-effect" (ie. opening and loading from json file), similar way as doingimport src.qt.resources_rchas side-effect of prefilling some QT resourceswhen using
.items(), you can work with it as two variables (key, value), so you dont have to unpack it via numeric index (item[0]item[1]) latervalueinstead of extracting specific keys in case there will be some other property in future. This will also mitigate a potential issue further on (see my other comment elsewhere)...and at that point you can do just this: