winipedia-utils 0.1.63__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/concurrent/concurrent.py +245 -242
- winipedia_utils/concurrent/multiprocessing.py +130 -115
- winipedia_utils/concurrent/multithreading.py +93 -93
- winipedia_utils/consts.py +23 -23
- winipedia_utils/data/__init__.py +1 -1
- winipedia_utils/data/dataframe/__init__.py +1 -0
- winipedia_utils/data/dataframe/cleaning.py +378 -0
- winipedia_utils/data/structures/__init__.py +1 -0
- winipedia_utils/data/structures/dicts.py +16 -0
- winipedia_utils/django/__init__.py +24 -24
- winipedia_utils/django/bulk.py +538 -538
- winipedia_utils/django/command.py +334 -334
- winipedia_utils/django/database.py +289 -289
- winipedia_utils/git/__init__.py +1 -1
- winipedia_utils/git/gitignore/__init__.py +1 -1
- winipedia_utils/git/gitignore/gitignore.py +136 -136
- winipedia_utils/git/pre_commit/__init__.py +1 -1
- winipedia_utils/git/pre_commit/config.py +70 -70
- winipedia_utils/git/pre_commit/hooks.py +109 -109
- winipedia_utils/git/pre_commit/run_hooks.py +49 -49
- winipedia_utils/iterating/__init__.py +1 -1
- winipedia_utils/iterating/iterate.py +29 -29
- winipedia_utils/logging/ansi.py +6 -6
- winipedia_utils/logging/config.py +64 -64
- winipedia_utils/logging/logger.py +26 -26
- winipedia_utils/modules/class_.py +119 -119
- winipedia_utils/modules/function.py +101 -103
- winipedia_utils/modules/module.py +379 -379
- winipedia_utils/modules/package.py +390 -390
- winipedia_utils/oop/mixins/meta.py +333 -331
- winipedia_utils/oop/mixins/mixin.py +37 -37
- winipedia_utils/os/__init__.py +1 -1
- winipedia_utils/os/os.py +63 -63
- winipedia_utils/projects/__init__.py +1 -1
- winipedia_utils/projects/poetry/__init__.py +1 -1
- winipedia_utils/projects/poetry/config.py +91 -91
- winipedia_utils/projects/poetry/poetry.py +31 -31
- winipedia_utils/projects/project.py +48 -48
- winipedia_utils/pyside/__init__.py +1 -1
- winipedia_utils/pyside/core/__init__.py +1 -1
- winipedia_utils/pyside/core/py_qiodevice.py +476 -476
- winipedia_utils/pyside/ui/__init__.py +1 -1
- winipedia_utils/pyside/ui/base/__init__.py +1 -1
- winipedia_utils/pyside/ui/base/base.py +180 -180
- winipedia_utils/pyside/ui/pages/__init__.py +1 -1
- winipedia_utils/pyside/ui/pages/base/__init__.py +1 -1
- winipedia_utils/pyside/ui/pages/base/base.py +92 -92
- winipedia_utils/pyside/ui/pages/browser.py +26 -26
- winipedia_utils/pyside/ui/pages/player.py +85 -85
- winipedia_utils/pyside/ui/widgets/__init__.py +1 -1
- winipedia_utils/pyside/ui/widgets/browser.py +243 -243
- winipedia_utils/pyside/ui/widgets/clickable_widget.py +57 -57
- winipedia_utils/pyside/ui/widgets/media_player.py +430 -430
- winipedia_utils/pyside/ui/widgets/notification.py +78 -78
- winipedia_utils/pyside/ui/windows/__init__.py +1 -1
- winipedia_utils/pyside/ui/windows/base/__init__.py +1 -1
- winipedia_utils/pyside/ui/windows/base/base.py +49 -49
- winipedia_utils/resources/__init__.py +1 -1
- winipedia_utils/resources/svgs/__init__.py +1 -1
- winipedia_utils/resources/svgs/download_arrow.svg +2 -2
- winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +5 -5
- winipedia_utils/resources/svgs/fullscreen_icon.svg +2 -2
- winipedia_utils/resources/svgs/menu_icon.svg +3 -3
- winipedia_utils/resources/svgs/pause_icon.svg +3 -3
- winipedia_utils/resources/svgs/play_icon.svg +16 -16
- winipedia_utils/resources/svgs/plus_icon.svg +23 -23
- winipedia_utils/resources/svgs/svg.py +15 -15
- winipedia_utils/security/__init__.py +1 -1
- winipedia_utils/security/cryptography.py +29 -29
- winipedia_utils/security/keyring.py +70 -70
- winipedia_utils/setup.py +47 -47
- winipedia_utils/testing/assertions.py +23 -23
- winipedia_utils/testing/convention.py +177 -177
- winipedia_utils/testing/create_tests.py +291 -291
- winipedia_utils/testing/fixtures.py +28 -28
- winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -1
- winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -6
- winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -33
- winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -31
- winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +312 -312
- winipedia_utils/testing/tests/base/utils/utils.py +82 -82
- winipedia_utils/testing/tests/conftest.py +32 -32
- winipedia_utils/text/string.py +126 -126
- {winipedia_utils-0.1.63.dist-info → winipedia_utils-0.2.0.dist-info}/METADATA +5 -4
- winipedia_utils-0.2.0.dist-info/RECORD +103 -0
- {winipedia_utils-0.1.63.dist-info → winipedia_utils-0.2.0.dist-info}/WHEEL +1 -1
- {winipedia_utils-0.1.63.dist-info → winipedia_utils-0.2.0.dist-info/licenses}/LICENSE +21 -21
- winipedia_utils/data/dataframe.py +0 -7
- winipedia_utils-0.1.63.dist-info/RECORD +0 -100
|
@@ -1,430 +1,430 @@
|
|
|
1
|
-
"""Media player module.
|
|
2
|
-
|
|
3
|
-
This module contains the media player class.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import time
|
|
7
|
-
from functools import partial
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
12
|
-
from PySide6.QtCore import Qt, QTimer, QUrl
|
|
13
|
-
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
|
|
14
|
-
from PySide6.QtWidgets import (
|
|
15
|
-
QHBoxLayout,
|
|
16
|
-
QLayout,
|
|
17
|
-
QMenu,
|
|
18
|
-
QPushButton,
|
|
19
|
-
QSizePolicy,
|
|
20
|
-
QSlider,
|
|
21
|
-
QVBoxLayout,
|
|
22
|
-
QWidget,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
from winipedia_utils.pyside.core.py_qiodevice import (
|
|
26
|
-
EncryptedPyQFile,
|
|
27
|
-
PyQFile,
|
|
28
|
-
PyQIODevice,
|
|
29
|
-
)
|
|
30
|
-
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
|
31
|
-
from winipedia_utils.pyside.ui.widgets.clickable_widget import ClickableVideoWidget
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class MediaPlayer(QMediaPlayer):
|
|
35
|
-
"""Media player class."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
|
38
|
-
"""Initialize the media player.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
parent_layout: The parent layout to add the media player widget to.
|
|
42
|
-
*args: Additional positional arguments passed to parent constructor.
|
|
43
|
-
**kwargs: Additional keyword arguments passed to parent constructor.
|
|
44
|
-
"""
|
|
45
|
-
super().__init__(*args, **kwargs)
|
|
46
|
-
self.parent_layout = parent_layout
|
|
47
|
-
self.io_device: PyQIODevice | None = None
|
|
48
|
-
self.make_widget()
|
|
49
|
-
|
|
50
|
-
def make_widget(self) -> None:
|
|
51
|
-
"""Make the widget.
|
|
52
|
-
|
|
53
|
-
Creates the main media player widget with vertical layout, adds media controls
|
|
54
|
-
above and below the video widget, and creates the video widget itself.
|
|
55
|
-
"""
|
|
56
|
-
self.media_player_widget = QWidget()
|
|
57
|
-
self.media_player_layout = QVBoxLayout(self.media_player_widget)
|
|
58
|
-
self.parent_layout.addWidget(self.media_player_widget)
|
|
59
|
-
self.add_media_controls_above()
|
|
60
|
-
self.make_video_widget()
|
|
61
|
-
self.add_media_controls_below()
|
|
62
|
-
|
|
63
|
-
def make_video_widget(self) -> None:
|
|
64
|
-
"""Make the video widget.
|
|
65
|
-
|
|
66
|
-
Creates a clickable video widget with expanding size policy, sets up
|
|
67
|
-
audio output, and connects the click signal to toggle media controls.
|
|
68
|
-
"""
|
|
69
|
-
self.video_widget = ClickableVideoWidget()
|
|
70
|
-
self.video_widget.clicked.connect(self.on_video_clicked)
|
|
71
|
-
self.video_widget.setSizePolicy(
|
|
72
|
-
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
73
|
-
)
|
|
74
|
-
self.setVideoOutput(self.video_widget)
|
|
75
|
-
|
|
76
|
-
self.audio_output = QAudioOutput()
|
|
77
|
-
self.setAudioOutput(self.audio_output)
|
|
78
|
-
|
|
79
|
-
self.media_player_layout.addWidget(self.video_widget)
|
|
80
|
-
|
|
81
|
-
def on_video_clicked(self) -> None:
|
|
82
|
-
"""Handle video widget click.
|
|
83
|
-
|
|
84
|
-
Toggles the visibility of media controls when the video widget is clicked.
|
|
85
|
-
"""
|
|
86
|
-
if self.media_controls_widget_above.isVisible():
|
|
87
|
-
self.hide_media_controls()
|
|
88
|
-
return
|
|
89
|
-
self.show_media_controls()
|
|
90
|
-
|
|
91
|
-
def show_media_controls(self) -> None:
|
|
92
|
-
"""Show media controls.
|
|
93
|
-
|
|
94
|
-
Makes both the above and below media control widgets visible.
|
|
95
|
-
"""
|
|
96
|
-
self.media_controls_widget_above.show()
|
|
97
|
-
self.media_controls_widget_below.show()
|
|
98
|
-
|
|
99
|
-
def hide_media_controls(self) -> None:
|
|
100
|
-
"""Hide media controls.
|
|
101
|
-
|
|
102
|
-
Hides both the above and below media control widgets.
|
|
103
|
-
"""
|
|
104
|
-
self.media_controls_widget_above.hide()
|
|
105
|
-
self.media_controls_widget_below.hide()
|
|
106
|
-
|
|
107
|
-
def add_media_controls_above(self) -> None:
|
|
108
|
-
"""Add media controls above the video.
|
|
109
|
-
|
|
110
|
-
Creates the top control bar with left, center, and right sections,
|
|
111
|
-
then adds speed, volume, playback, and fullscreen controls.
|
|
112
|
-
"""
|
|
113
|
-
# main above widget
|
|
114
|
-
self.media_controls_widget_above = QWidget()
|
|
115
|
-
self.media_controls_layout_above = QHBoxLayout(self.media_controls_widget_above)
|
|
116
|
-
self.media_player_layout.addWidget(self.media_controls_widget_above)
|
|
117
|
-
# left contorls
|
|
118
|
-
self.left_controls_widget = QWidget()
|
|
119
|
-
self.left_controls_layout = QHBoxLayout(self.left_controls_widget)
|
|
120
|
-
self.media_controls_layout_above.addWidget(
|
|
121
|
-
self.left_controls_widget, alignment=Qt.AlignmentFlag.AlignLeft
|
|
122
|
-
)
|
|
123
|
-
# center contorls
|
|
124
|
-
self.center_controls_widget = QWidget()
|
|
125
|
-
self.center_controls_layout = QHBoxLayout(self.center_controls_widget)
|
|
126
|
-
self.media_controls_layout_above.addWidget(
|
|
127
|
-
self.center_controls_widget, alignment=Qt.AlignmentFlag.AlignCenter
|
|
128
|
-
)
|
|
129
|
-
self.right_controls_widget = QWidget()
|
|
130
|
-
self.right_controls_layout = QHBoxLayout(self.right_controls_widget)
|
|
131
|
-
self.media_controls_layout_above.addWidget(
|
|
132
|
-
self.right_controls_widget, alignment=Qt.AlignmentFlag.AlignRight
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
self.add_speed_control()
|
|
136
|
-
self.add_volume_control()
|
|
137
|
-
self.add_playback_control()
|
|
138
|
-
self.add_fullscreen_control()
|
|
139
|
-
|
|
140
|
-
def add_media_controls_below(self) -> None:
|
|
141
|
-
"""Add media controls below the video.
|
|
142
|
-
|
|
143
|
-
Creates the bottom control bar and adds the progress control slider.
|
|
144
|
-
"""
|
|
145
|
-
self.media_controls_widget_below = QWidget()
|
|
146
|
-
self.media_controls_layout_below = QHBoxLayout(self.media_controls_widget_below)
|
|
147
|
-
self.media_player_layout.addWidget(self.media_controls_widget_below)
|
|
148
|
-
self.add_progress_control()
|
|
149
|
-
|
|
150
|
-
def add_playback_control(self) -> None:
|
|
151
|
-
"""Add playback control.
|
|
152
|
-
|
|
153
|
-
Creates a play/pause button with appropriate icons and connects it
|
|
154
|
-
to the toggle_playback method. Adds the button to the center controls.
|
|
155
|
-
"""
|
|
156
|
-
self.play_icon = BaseUI.get_svg_icon("play_icon")
|
|
157
|
-
self.pause_icon = BaseUI.get_svg_icon("pause_icon")
|
|
158
|
-
# Pause symbol: ⏸ (U+23F8)
|
|
159
|
-
self.playback_button = QPushButton()
|
|
160
|
-
self.playback_button.setIcon(self.pause_icon)
|
|
161
|
-
self.playback_button.clicked.connect(self.toggle_playback)
|
|
162
|
-
|
|
163
|
-
self.center_controls_layout.addWidget(self.playback_button)
|
|
164
|
-
|
|
165
|
-
def toggle_playback(self) -> None:
|
|
166
|
-
"""Toggle playback.
|
|
167
|
-
|
|
168
|
-
Switches between play and pause states, updating the button icon
|
|
169
|
-
accordingly based on the current playback state.
|
|
170
|
-
"""
|
|
171
|
-
if self.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
|
172
|
-
self.pause()
|
|
173
|
-
self.playback_button.setIcon(self.play_icon)
|
|
174
|
-
else:
|
|
175
|
-
self.play()
|
|
176
|
-
self.playback_button.setIcon(self.pause_icon)
|
|
177
|
-
|
|
178
|
-
def add_speed_control(self) -> None:
|
|
179
|
-
"""Add speed control.
|
|
180
|
-
|
|
181
|
-
Creates a button in the top left that shows a dropdown menu to select
|
|
182
|
-
playback speed from predefined options (0.2x to 5x).
|
|
183
|
-
"""
|
|
184
|
-
self.default_speed = 1
|
|
185
|
-
self.speed_options = [0.2, 0.5, self.default_speed, 1.5, 2, 3, 4, 5]
|
|
186
|
-
self.speed_button = QPushButton(f"{self.default_speed}x")
|
|
187
|
-
self.speed_menu = QMenu(self.speed_button)
|
|
188
|
-
for speed in self.speed_options:
|
|
189
|
-
action = self.speed_menu.addAction(f"{speed}x")
|
|
190
|
-
action.triggered.connect(partial(self.change_speed, speed))
|
|
191
|
-
|
|
192
|
-
self.speed_button.setMenu(self.speed_menu)
|
|
193
|
-
self.left_controls_layout.addWidget(self.speed_button)
|
|
194
|
-
|
|
195
|
-
def change_speed(self, speed: float) -> None:
|
|
196
|
-
"""Change playback speed.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
speed: The new playback speed multiplier.
|
|
200
|
-
"""
|
|
201
|
-
self.setPlaybackRate(speed)
|
|
202
|
-
self.speed_button.setText(f"{speed}x")
|
|
203
|
-
|
|
204
|
-
def add_volume_control(self) -> None:
|
|
205
|
-
"""Add volume control.
|
|
206
|
-
|
|
207
|
-
Creates a horizontal slider for volume control with range 0-100
|
|
208
|
-
and connects it to the volume change handler.
|
|
209
|
-
"""
|
|
210
|
-
self.volume_slider = QSlider(Qt.Orientation.Horizontal)
|
|
211
|
-
self.volume_slider.setRange(0, 100)
|
|
212
|
-
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
|
213
|
-
self.left_controls_layout.addWidget(self.volume_slider)
|
|
214
|
-
|
|
215
|
-
def on_volume_changed(self, value: int) -> None:
|
|
216
|
-
"""Handle volume slider value change.
|
|
217
|
-
|
|
218
|
-
Args:
|
|
219
|
-
value: The new volume value from 0-100.
|
|
220
|
-
"""
|
|
221
|
-
volume = value / 100.0 # Convert to 0.0-1.0 range
|
|
222
|
-
self.audio_output.setVolume(volume)
|
|
223
|
-
|
|
224
|
-
def add_fullscreen_control(self) -> None:
|
|
225
|
-
"""Add fullscreen control.
|
|
226
|
-
|
|
227
|
-
Creates a fullscreen toggle button with appropriate icons and determines
|
|
228
|
-
which widgets to hide/show when entering/exiting fullscreen mode.
|
|
229
|
-
"""
|
|
230
|
-
self.fullscreen_icon = BaseUI.get_svg_icon("fullscreen_icon")
|
|
231
|
-
self.exit_fullscreen_icon = BaseUI.get_svg_icon("exit_fullscreen_icon")
|
|
232
|
-
self.fullscreen_button = QPushButton()
|
|
233
|
-
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
|
234
|
-
|
|
235
|
-
self.parent_widget = self.parent_layout.parentWidget()
|
|
236
|
-
self.other_visible_widgets = [
|
|
237
|
-
w
|
|
238
|
-
for w in set(self.parent_widget.findChildren(QWidget))
|
|
239
|
-
- {
|
|
240
|
-
self.media_player_widget,
|
|
241
|
-
*self.media_player_widget.findChildren(QWidget),
|
|
242
|
-
}
|
|
243
|
-
if w.isVisible() or not (w.isHidden() or w.isVisible())
|
|
244
|
-
]
|
|
245
|
-
self.fullscreen_button.clicked.connect(self.toggle_fullscreen)
|
|
246
|
-
|
|
247
|
-
self.right_controls_layout.addWidget(self.fullscreen_button)
|
|
248
|
-
|
|
249
|
-
def toggle_fullscreen(self) -> None:
|
|
250
|
-
"""Toggle fullscreen mode.
|
|
251
|
-
|
|
252
|
-
Switches between fullscreen and windowed mode, hiding/showing other
|
|
253
|
-
widgets and updating the button icon accordingly.
|
|
254
|
-
"""
|
|
255
|
-
# Get the main window
|
|
256
|
-
main_window = self.media_player_widget.window()
|
|
257
|
-
if main_window.isFullScreen():
|
|
258
|
-
for widget in self.other_visible_widgets:
|
|
259
|
-
widget.show()
|
|
260
|
-
# show the window in the previous size
|
|
261
|
-
main_window.showMaximized()
|
|
262
|
-
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
|
263
|
-
else:
|
|
264
|
-
for widget in self.other_visible_widgets:
|
|
265
|
-
widget.hide()
|
|
266
|
-
main_window.showFullScreen()
|
|
267
|
-
self.fullscreen_button.setIcon(self.exit_fullscreen_icon)
|
|
268
|
-
|
|
269
|
-
def add_progress_control(self) -> None:
|
|
270
|
-
"""Add progress control.
|
|
271
|
-
|
|
272
|
-
Creates a horizontal progress slider and connects it to media player
|
|
273
|
-
signals for position updates and user interaction handling.
|
|
274
|
-
"""
|
|
275
|
-
self.progress_slider = QSlider(Qt.Orientation.Horizontal)
|
|
276
|
-
self.media_controls_layout_below.addWidget(self.progress_slider)
|
|
277
|
-
|
|
278
|
-
# Connect media player signals to update the progress slider
|
|
279
|
-
self.positionChanged.connect(self.update_slider_position)
|
|
280
|
-
self.durationChanged.connect(self.set_slider_range)
|
|
281
|
-
|
|
282
|
-
# Connect slider signals to update video position
|
|
283
|
-
self.last_slider_moved_update = time.time()
|
|
284
|
-
self.slider_moved_update_interval = 0.1
|
|
285
|
-
self.progress_slider.sliderMoved.connect(self.on_slider_moved)
|
|
286
|
-
self.progress_slider.sliderReleased.connect(self.on_slider_released)
|
|
287
|
-
|
|
288
|
-
def update_slider_position(self, position: int) -> None:
|
|
289
|
-
"""Update the progress slider position.
|
|
290
|
-
|
|
291
|
-
Args:
|
|
292
|
-
position: The current media position in milliseconds.
|
|
293
|
-
"""
|
|
294
|
-
# Only update if not being dragged to prevent jumps during manual sliding
|
|
295
|
-
if not self.progress_slider.isSliderDown():
|
|
296
|
-
self.progress_slider.setValue(position)
|
|
297
|
-
|
|
298
|
-
def set_slider_range(self, duration: int) -> None:
|
|
299
|
-
"""Set the progress slider range based on media duration.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
duration: The total media duration in milliseconds.
|
|
303
|
-
"""
|
|
304
|
-
self.progress_slider.setRange(0, duration)
|
|
305
|
-
|
|
306
|
-
def on_slider_moved(self, position: int) -> None:
|
|
307
|
-
"""Set the media position when slider is moved.
|
|
308
|
-
|
|
309
|
-
Implements throttling to prevent excessive position updates during
|
|
310
|
-
slider dragging for better performance.
|
|
311
|
-
|
|
312
|
-
Args:
|
|
313
|
-
position: The new position from the slider in milliseconds.
|
|
314
|
-
"""
|
|
315
|
-
current_time = time.time()
|
|
316
|
-
if (
|
|
317
|
-
current_time - self.last_slider_moved_update
|
|
318
|
-
> self.slider_moved_update_interval
|
|
319
|
-
):
|
|
320
|
-
self.setPosition(position)
|
|
321
|
-
self.last_slider_moved_update = current_time
|
|
322
|
-
|
|
323
|
-
def on_slider_released(self) -> None:
|
|
324
|
-
"""Handle slider release event.
|
|
325
|
-
|
|
326
|
-
Sets the final media position when the user releases the slider.
|
|
327
|
-
"""
|
|
328
|
-
self.setPosition(self.progress_slider.value())
|
|
329
|
-
|
|
330
|
-
def play_video(
|
|
331
|
-
self,
|
|
332
|
-
io_device: PyQIODevice,
|
|
333
|
-
source_url: QUrl,
|
|
334
|
-
position: int = 0,
|
|
335
|
-
) -> None:
|
|
336
|
-
"""Play the video.
|
|
337
|
-
|
|
338
|
-
Stops current playback and starts a new video using the provided
|
|
339
|
-
source function with a delay to prevent freezing.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
io_device: The PyQIODevice to use as the media source.
|
|
343
|
-
source_url: The QUrl representing the source location.
|
|
344
|
-
position: The position to start playback from in milliseconds.
|
|
345
|
-
"""
|
|
346
|
-
self.stop_and_close_io_device()
|
|
347
|
-
|
|
348
|
-
self.resume_func = partial(self.resume_to_position, position=position)
|
|
349
|
-
self.mediaStatusChanged.connect(self.resume_func)
|
|
350
|
-
|
|
351
|
-
# SingleShot prevents freezing when starting new video while another is playing
|
|
352
|
-
QTimer.singleShot(
|
|
353
|
-
100,
|
|
354
|
-
partial(
|
|
355
|
-
self.set_source_and_play, io_device=io_device, source_url=source_url
|
|
356
|
-
),
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
def stop_and_close_io_device(self) -> None:
|
|
360
|
-
"""Stop playback and close the IO device."""
|
|
361
|
-
self.stop()
|
|
362
|
-
if self.io_device is not None:
|
|
363
|
-
self.io_device.close()
|
|
364
|
-
|
|
365
|
-
def resume_to_position(
|
|
366
|
-
self, status: QMediaPlayer.MediaStatus, position: int
|
|
367
|
-
) -> None:
|
|
368
|
-
"""Resume playback to a position.
|
|
369
|
-
|
|
370
|
-
Args:
|
|
371
|
-
status: The current media status.
|
|
372
|
-
position: The position to resume playback from in milliseconds.
|
|
373
|
-
"""
|
|
374
|
-
if status == QMediaPlayer.MediaStatus.BufferedMedia:
|
|
375
|
-
self.setPosition(position)
|
|
376
|
-
self.mediaStatusChanged.disconnect(self.resume_func)
|
|
377
|
-
|
|
378
|
-
def set_source_and_play(
|
|
379
|
-
self,
|
|
380
|
-
io_device: PyQIODevice,
|
|
381
|
-
source_url: QUrl,
|
|
382
|
-
) -> None:
|
|
383
|
-
"""Set the source and play the video.
|
|
384
|
-
|
|
385
|
-
Args:
|
|
386
|
-
io_device: The PyQIODevice to use as the media source.
|
|
387
|
-
source_url: The QUrl representing the source location.
|
|
388
|
-
"""
|
|
389
|
-
self.set_source_device(io_device, source_url)
|
|
390
|
-
self.play()
|
|
391
|
-
|
|
392
|
-
def set_source_device(self, io_device: PyQIODevice, source_url: QUrl) -> None:
|
|
393
|
-
"""Set the source device for playback.
|
|
394
|
-
|
|
395
|
-
Args:
|
|
396
|
-
io_device: The PyQIODevice to use as the media source.
|
|
397
|
-
source_url: The QUrl representing the source location.
|
|
398
|
-
"""
|
|
399
|
-
self.source_url = source_url
|
|
400
|
-
self.io_device = io_device
|
|
401
|
-
self.setSourceDevice(self.io_device, self.source_url)
|
|
402
|
-
|
|
403
|
-
def play_file(self, path: Path, position: int = 0) -> None:
|
|
404
|
-
"""Play a regular video file.
|
|
405
|
-
|
|
406
|
-
Args:
|
|
407
|
-
path: The file path to the video file to play.
|
|
408
|
-
position: The position to start playback from in milliseconds.
|
|
409
|
-
"""
|
|
410
|
-
self.play_video(
|
|
411
|
-
position=position,
|
|
412
|
-
io_device=PyQFile(path),
|
|
413
|
-
source_url=QUrl.fromLocalFile(path),
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
def play_encrypted_file(
|
|
417
|
-
self, path: Path, aes_gcm: AESGCM, position: int = 0
|
|
418
|
-
) -> None:
|
|
419
|
-
"""Play an encrypted video file.
|
|
420
|
-
|
|
421
|
-
Args:
|
|
422
|
-
path: The file path to the encrypted video file to play.
|
|
423
|
-
aes_gcm: The AES-GCM cipher instance for decryption.
|
|
424
|
-
position: The position to start playback from in milliseconds.
|
|
425
|
-
"""
|
|
426
|
-
self.play_video(
|
|
427
|
-
position=position,
|
|
428
|
-
io_device=EncryptedPyQFile(path, aes_gcm),
|
|
429
|
-
source_url=QUrl.fromLocalFile(path),
|
|
430
|
-
)
|
|
1
|
+
"""Media player module.
|
|
2
|
+
|
|
3
|
+
This module contains the media player class.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from functools import partial
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
12
|
+
from PySide6.QtCore import Qt, QTimer, QUrl
|
|
13
|
+
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLayout,
|
|
17
|
+
QMenu,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QSizePolicy,
|
|
20
|
+
QSlider,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from winipedia_utils.pyside.core.py_qiodevice import (
|
|
26
|
+
EncryptedPyQFile,
|
|
27
|
+
PyQFile,
|
|
28
|
+
PyQIODevice,
|
|
29
|
+
)
|
|
30
|
+
from winipedia_utils.pyside.ui.base.base import Base as BaseUI
|
|
31
|
+
from winipedia_utils.pyside.ui.widgets.clickable_widget import ClickableVideoWidget
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MediaPlayer(QMediaPlayer):
|
|
35
|
+
"""Media player class."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
|
|
38
|
+
"""Initialize the media player.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parent_layout: The parent layout to add the media player widget to.
|
|
42
|
+
*args: Additional positional arguments passed to parent constructor.
|
|
43
|
+
**kwargs: Additional keyword arguments passed to parent constructor.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(*args, **kwargs)
|
|
46
|
+
self.parent_layout = parent_layout
|
|
47
|
+
self.io_device: PyQIODevice | None = None
|
|
48
|
+
self.make_widget()
|
|
49
|
+
|
|
50
|
+
def make_widget(self) -> None:
|
|
51
|
+
"""Make the widget.
|
|
52
|
+
|
|
53
|
+
Creates the main media player widget with vertical layout, adds media controls
|
|
54
|
+
above and below the video widget, and creates the video widget itself.
|
|
55
|
+
"""
|
|
56
|
+
self.media_player_widget = QWidget()
|
|
57
|
+
self.media_player_layout = QVBoxLayout(self.media_player_widget)
|
|
58
|
+
self.parent_layout.addWidget(self.media_player_widget)
|
|
59
|
+
self.add_media_controls_above()
|
|
60
|
+
self.make_video_widget()
|
|
61
|
+
self.add_media_controls_below()
|
|
62
|
+
|
|
63
|
+
def make_video_widget(self) -> None:
|
|
64
|
+
"""Make the video widget.
|
|
65
|
+
|
|
66
|
+
Creates a clickable video widget with expanding size policy, sets up
|
|
67
|
+
audio output, and connects the click signal to toggle media controls.
|
|
68
|
+
"""
|
|
69
|
+
self.video_widget = ClickableVideoWidget()
|
|
70
|
+
self.video_widget.clicked.connect(self.on_video_clicked)
|
|
71
|
+
self.video_widget.setSizePolicy(
|
|
72
|
+
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
73
|
+
)
|
|
74
|
+
self.setVideoOutput(self.video_widget)
|
|
75
|
+
|
|
76
|
+
self.audio_output = QAudioOutput()
|
|
77
|
+
self.setAudioOutput(self.audio_output)
|
|
78
|
+
|
|
79
|
+
self.media_player_layout.addWidget(self.video_widget)
|
|
80
|
+
|
|
81
|
+
def on_video_clicked(self) -> None:
|
|
82
|
+
"""Handle video widget click.
|
|
83
|
+
|
|
84
|
+
Toggles the visibility of media controls when the video widget is clicked.
|
|
85
|
+
"""
|
|
86
|
+
if self.media_controls_widget_above.isVisible():
|
|
87
|
+
self.hide_media_controls()
|
|
88
|
+
return
|
|
89
|
+
self.show_media_controls()
|
|
90
|
+
|
|
91
|
+
def show_media_controls(self) -> None:
|
|
92
|
+
"""Show media controls.
|
|
93
|
+
|
|
94
|
+
Makes both the above and below media control widgets visible.
|
|
95
|
+
"""
|
|
96
|
+
self.media_controls_widget_above.show()
|
|
97
|
+
self.media_controls_widget_below.show()
|
|
98
|
+
|
|
99
|
+
def hide_media_controls(self) -> None:
|
|
100
|
+
"""Hide media controls.
|
|
101
|
+
|
|
102
|
+
Hides both the above and below media control widgets.
|
|
103
|
+
"""
|
|
104
|
+
self.media_controls_widget_above.hide()
|
|
105
|
+
self.media_controls_widget_below.hide()
|
|
106
|
+
|
|
107
|
+
def add_media_controls_above(self) -> None:
|
|
108
|
+
"""Add media controls above the video.
|
|
109
|
+
|
|
110
|
+
Creates the top control bar with left, center, and right sections,
|
|
111
|
+
then adds speed, volume, playback, and fullscreen controls.
|
|
112
|
+
"""
|
|
113
|
+
# main above widget
|
|
114
|
+
self.media_controls_widget_above = QWidget()
|
|
115
|
+
self.media_controls_layout_above = QHBoxLayout(self.media_controls_widget_above)
|
|
116
|
+
self.media_player_layout.addWidget(self.media_controls_widget_above)
|
|
117
|
+
# left contorls
|
|
118
|
+
self.left_controls_widget = QWidget()
|
|
119
|
+
self.left_controls_layout = QHBoxLayout(self.left_controls_widget)
|
|
120
|
+
self.media_controls_layout_above.addWidget(
|
|
121
|
+
self.left_controls_widget, alignment=Qt.AlignmentFlag.AlignLeft
|
|
122
|
+
)
|
|
123
|
+
# center contorls
|
|
124
|
+
self.center_controls_widget = QWidget()
|
|
125
|
+
self.center_controls_layout = QHBoxLayout(self.center_controls_widget)
|
|
126
|
+
self.media_controls_layout_above.addWidget(
|
|
127
|
+
self.center_controls_widget, alignment=Qt.AlignmentFlag.AlignCenter
|
|
128
|
+
)
|
|
129
|
+
self.right_controls_widget = QWidget()
|
|
130
|
+
self.right_controls_layout = QHBoxLayout(self.right_controls_widget)
|
|
131
|
+
self.media_controls_layout_above.addWidget(
|
|
132
|
+
self.right_controls_widget, alignment=Qt.AlignmentFlag.AlignRight
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.add_speed_control()
|
|
136
|
+
self.add_volume_control()
|
|
137
|
+
self.add_playback_control()
|
|
138
|
+
self.add_fullscreen_control()
|
|
139
|
+
|
|
140
|
+
def add_media_controls_below(self) -> None:
|
|
141
|
+
"""Add media controls below the video.
|
|
142
|
+
|
|
143
|
+
Creates the bottom control bar and adds the progress control slider.
|
|
144
|
+
"""
|
|
145
|
+
self.media_controls_widget_below = QWidget()
|
|
146
|
+
self.media_controls_layout_below = QHBoxLayout(self.media_controls_widget_below)
|
|
147
|
+
self.media_player_layout.addWidget(self.media_controls_widget_below)
|
|
148
|
+
self.add_progress_control()
|
|
149
|
+
|
|
150
|
+
def add_playback_control(self) -> None:
|
|
151
|
+
"""Add playback control.
|
|
152
|
+
|
|
153
|
+
Creates a play/pause button with appropriate icons and connects it
|
|
154
|
+
to the toggle_playback method. Adds the button to the center controls.
|
|
155
|
+
"""
|
|
156
|
+
self.play_icon = BaseUI.get_svg_icon("play_icon")
|
|
157
|
+
self.pause_icon = BaseUI.get_svg_icon("pause_icon")
|
|
158
|
+
# Pause symbol: ⏸ (U+23F8)
|
|
159
|
+
self.playback_button = QPushButton()
|
|
160
|
+
self.playback_button.setIcon(self.pause_icon)
|
|
161
|
+
self.playback_button.clicked.connect(self.toggle_playback)
|
|
162
|
+
|
|
163
|
+
self.center_controls_layout.addWidget(self.playback_button)
|
|
164
|
+
|
|
165
|
+
def toggle_playback(self) -> None:
|
|
166
|
+
"""Toggle playback.
|
|
167
|
+
|
|
168
|
+
Switches between play and pause states, updating the button icon
|
|
169
|
+
accordingly based on the current playback state.
|
|
170
|
+
"""
|
|
171
|
+
if self.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
|
172
|
+
self.pause()
|
|
173
|
+
self.playback_button.setIcon(self.play_icon)
|
|
174
|
+
else:
|
|
175
|
+
self.play()
|
|
176
|
+
self.playback_button.setIcon(self.pause_icon)
|
|
177
|
+
|
|
178
|
+
def add_speed_control(self) -> None:
|
|
179
|
+
"""Add speed control.
|
|
180
|
+
|
|
181
|
+
Creates a button in the top left that shows a dropdown menu to select
|
|
182
|
+
playback speed from predefined options (0.2x to 5x).
|
|
183
|
+
"""
|
|
184
|
+
self.default_speed = 1
|
|
185
|
+
self.speed_options = [0.2, 0.5, self.default_speed, 1.5, 2, 3, 4, 5]
|
|
186
|
+
self.speed_button = QPushButton(f"{self.default_speed}x")
|
|
187
|
+
self.speed_menu = QMenu(self.speed_button)
|
|
188
|
+
for speed in self.speed_options:
|
|
189
|
+
action = self.speed_menu.addAction(f"{speed}x")
|
|
190
|
+
action.triggered.connect(partial(self.change_speed, speed))
|
|
191
|
+
|
|
192
|
+
self.speed_button.setMenu(self.speed_menu)
|
|
193
|
+
self.left_controls_layout.addWidget(self.speed_button)
|
|
194
|
+
|
|
195
|
+
def change_speed(self, speed: float) -> None:
|
|
196
|
+
"""Change playback speed.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
speed: The new playback speed multiplier.
|
|
200
|
+
"""
|
|
201
|
+
self.setPlaybackRate(speed)
|
|
202
|
+
self.speed_button.setText(f"{speed}x")
|
|
203
|
+
|
|
204
|
+
def add_volume_control(self) -> None:
|
|
205
|
+
"""Add volume control.
|
|
206
|
+
|
|
207
|
+
Creates a horizontal slider for volume control with range 0-100
|
|
208
|
+
and connects it to the volume change handler.
|
|
209
|
+
"""
|
|
210
|
+
self.volume_slider = QSlider(Qt.Orientation.Horizontal)
|
|
211
|
+
self.volume_slider.setRange(0, 100)
|
|
212
|
+
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
|
213
|
+
self.left_controls_layout.addWidget(self.volume_slider)
|
|
214
|
+
|
|
215
|
+
def on_volume_changed(self, value: int) -> None:
|
|
216
|
+
"""Handle volume slider value change.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
value: The new volume value from 0-100.
|
|
220
|
+
"""
|
|
221
|
+
volume = value / 100.0 # Convert to 0.0-1.0 range
|
|
222
|
+
self.audio_output.setVolume(volume)
|
|
223
|
+
|
|
224
|
+
def add_fullscreen_control(self) -> None:
|
|
225
|
+
"""Add fullscreen control.
|
|
226
|
+
|
|
227
|
+
Creates a fullscreen toggle button with appropriate icons and determines
|
|
228
|
+
which widgets to hide/show when entering/exiting fullscreen mode.
|
|
229
|
+
"""
|
|
230
|
+
self.fullscreen_icon = BaseUI.get_svg_icon("fullscreen_icon")
|
|
231
|
+
self.exit_fullscreen_icon = BaseUI.get_svg_icon("exit_fullscreen_icon")
|
|
232
|
+
self.fullscreen_button = QPushButton()
|
|
233
|
+
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
|
234
|
+
|
|
235
|
+
self.parent_widget = self.parent_layout.parentWidget()
|
|
236
|
+
self.other_visible_widgets = [
|
|
237
|
+
w
|
|
238
|
+
for w in set(self.parent_widget.findChildren(QWidget))
|
|
239
|
+
- {
|
|
240
|
+
self.media_player_widget,
|
|
241
|
+
*self.media_player_widget.findChildren(QWidget),
|
|
242
|
+
}
|
|
243
|
+
if w.isVisible() or not (w.isHidden() or w.isVisible())
|
|
244
|
+
]
|
|
245
|
+
self.fullscreen_button.clicked.connect(self.toggle_fullscreen)
|
|
246
|
+
|
|
247
|
+
self.right_controls_layout.addWidget(self.fullscreen_button)
|
|
248
|
+
|
|
249
|
+
def toggle_fullscreen(self) -> None:
|
|
250
|
+
"""Toggle fullscreen mode.
|
|
251
|
+
|
|
252
|
+
Switches between fullscreen and windowed mode, hiding/showing other
|
|
253
|
+
widgets and updating the button icon accordingly.
|
|
254
|
+
"""
|
|
255
|
+
# Get the main window
|
|
256
|
+
main_window = self.media_player_widget.window()
|
|
257
|
+
if main_window.isFullScreen():
|
|
258
|
+
for widget in self.other_visible_widgets:
|
|
259
|
+
widget.show()
|
|
260
|
+
# show the window in the previous size
|
|
261
|
+
main_window.showMaximized()
|
|
262
|
+
self.fullscreen_button.setIcon(self.fullscreen_icon)
|
|
263
|
+
else:
|
|
264
|
+
for widget in self.other_visible_widgets:
|
|
265
|
+
widget.hide()
|
|
266
|
+
main_window.showFullScreen()
|
|
267
|
+
self.fullscreen_button.setIcon(self.exit_fullscreen_icon)
|
|
268
|
+
|
|
269
|
+
def add_progress_control(self) -> None:
|
|
270
|
+
"""Add progress control.
|
|
271
|
+
|
|
272
|
+
Creates a horizontal progress slider and connects it to media player
|
|
273
|
+
signals for position updates and user interaction handling.
|
|
274
|
+
"""
|
|
275
|
+
self.progress_slider = QSlider(Qt.Orientation.Horizontal)
|
|
276
|
+
self.media_controls_layout_below.addWidget(self.progress_slider)
|
|
277
|
+
|
|
278
|
+
# Connect media player signals to update the progress slider
|
|
279
|
+
self.positionChanged.connect(self.update_slider_position)
|
|
280
|
+
self.durationChanged.connect(self.set_slider_range)
|
|
281
|
+
|
|
282
|
+
# Connect slider signals to update video position
|
|
283
|
+
self.last_slider_moved_update = time.time()
|
|
284
|
+
self.slider_moved_update_interval = 0.1
|
|
285
|
+
self.progress_slider.sliderMoved.connect(self.on_slider_moved)
|
|
286
|
+
self.progress_slider.sliderReleased.connect(self.on_slider_released)
|
|
287
|
+
|
|
288
|
+
def update_slider_position(self, position: int) -> None:
|
|
289
|
+
"""Update the progress slider position.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
position: The current media position in milliseconds.
|
|
293
|
+
"""
|
|
294
|
+
# Only update if not being dragged to prevent jumps during manual sliding
|
|
295
|
+
if not self.progress_slider.isSliderDown():
|
|
296
|
+
self.progress_slider.setValue(position)
|
|
297
|
+
|
|
298
|
+
def set_slider_range(self, duration: int) -> None:
|
|
299
|
+
"""Set the progress slider range based on media duration.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
duration: The total media duration in milliseconds.
|
|
303
|
+
"""
|
|
304
|
+
self.progress_slider.setRange(0, duration)
|
|
305
|
+
|
|
306
|
+
def on_slider_moved(self, position: int) -> None:
|
|
307
|
+
"""Set the media position when slider is moved.
|
|
308
|
+
|
|
309
|
+
Implements throttling to prevent excessive position updates during
|
|
310
|
+
slider dragging for better performance.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
position: The new position from the slider in milliseconds.
|
|
314
|
+
"""
|
|
315
|
+
current_time = time.time()
|
|
316
|
+
if (
|
|
317
|
+
current_time - self.last_slider_moved_update
|
|
318
|
+
> self.slider_moved_update_interval
|
|
319
|
+
):
|
|
320
|
+
self.setPosition(position)
|
|
321
|
+
self.last_slider_moved_update = current_time
|
|
322
|
+
|
|
323
|
+
def on_slider_released(self) -> None:
|
|
324
|
+
"""Handle slider release event.
|
|
325
|
+
|
|
326
|
+
Sets the final media position when the user releases the slider.
|
|
327
|
+
"""
|
|
328
|
+
self.setPosition(self.progress_slider.value())
|
|
329
|
+
|
|
330
|
+
def play_video(
|
|
331
|
+
self,
|
|
332
|
+
io_device: PyQIODevice,
|
|
333
|
+
source_url: QUrl,
|
|
334
|
+
position: int = 0,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Play the video.
|
|
337
|
+
|
|
338
|
+
Stops current playback and starts a new video using the provided
|
|
339
|
+
source function with a delay to prevent freezing.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
io_device: The PyQIODevice to use as the media source.
|
|
343
|
+
source_url: The QUrl representing the source location.
|
|
344
|
+
position: The position to start playback from in milliseconds.
|
|
345
|
+
"""
|
|
346
|
+
self.stop_and_close_io_device()
|
|
347
|
+
|
|
348
|
+
self.resume_func = partial(self.resume_to_position, position=position)
|
|
349
|
+
self.mediaStatusChanged.connect(self.resume_func)
|
|
350
|
+
|
|
351
|
+
# SingleShot prevents freezing when starting new video while another is playing
|
|
352
|
+
QTimer.singleShot(
|
|
353
|
+
100,
|
|
354
|
+
partial(
|
|
355
|
+
self.set_source_and_play, io_device=io_device, source_url=source_url
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def stop_and_close_io_device(self) -> None:
|
|
360
|
+
"""Stop playback and close the IO device."""
|
|
361
|
+
self.stop()
|
|
362
|
+
if self.io_device is not None:
|
|
363
|
+
self.io_device.close()
|
|
364
|
+
|
|
365
|
+
def resume_to_position(
|
|
366
|
+
self, status: QMediaPlayer.MediaStatus, position: int
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Resume playback to a position.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
status: The current media status.
|
|
372
|
+
position: The position to resume playback from in milliseconds.
|
|
373
|
+
"""
|
|
374
|
+
if status == QMediaPlayer.MediaStatus.BufferedMedia:
|
|
375
|
+
self.setPosition(position)
|
|
376
|
+
self.mediaStatusChanged.disconnect(self.resume_func)
|
|
377
|
+
|
|
378
|
+
def set_source_and_play(
|
|
379
|
+
self,
|
|
380
|
+
io_device: PyQIODevice,
|
|
381
|
+
source_url: QUrl,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Set the source and play the video.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
io_device: The PyQIODevice to use as the media source.
|
|
387
|
+
source_url: The QUrl representing the source location.
|
|
388
|
+
"""
|
|
389
|
+
self.set_source_device(io_device, source_url)
|
|
390
|
+
self.play()
|
|
391
|
+
|
|
392
|
+
def set_source_device(self, io_device: PyQIODevice, source_url: QUrl) -> None:
|
|
393
|
+
"""Set the source device for playback.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
io_device: The PyQIODevice to use as the media source.
|
|
397
|
+
source_url: The QUrl representing the source location.
|
|
398
|
+
"""
|
|
399
|
+
self.source_url = source_url
|
|
400
|
+
self.io_device = io_device
|
|
401
|
+
self.setSourceDevice(self.io_device, self.source_url)
|
|
402
|
+
|
|
403
|
+
def play_file(self, path: Path, position: int = 0) -> None:
|
|
404
|
+
"""Play a regular video file.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
path: The file path to the video file to play.
|
|
408
|
+
position: The position to start playback from in milliseconds.
|
|
409
|
+
"""
|
|
410
|
+
self.play_video(
|
|
411
|
+
position=position,
|
|
412
|
+
io_device=PyQFile(path),
|
|
413
|
+
source_url=QUrl.fromLocalFile(path),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def play_encrypted_file(
|
|
417
|
+
self, path: Path, aes_gcm: AESGCM, position: int = 0
|
|
418
|
+
) -> None:
|
|
419
|
+
"""Play an encrypted video file.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
path: The file path to the encrypted video file to play.
|
|
423
|
+
aes_gcm: The AES-GCM cipher instance for decryption.
|
|
424
|
+
position: The position to start playback from in milliseconds.
|
|
425
|
+
"""
|
|
426
|
+
self.play_video(
|
|
427
|
+
position=position,
|
|
428
|
+
io_device=EncryptedPyQFile(path, aes_gcm),
|
|
429
|
+
source_url=QUrl.fromLocalFile(path),
|
|
430
|
+
)
|