psychopy 2025.1.0__py3-none-any.whl → 2025.2.1__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 psychopy might be problematic. Click here for more details.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/colors.py +10 -5
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +43 -3
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +53 -7
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/gltools.py +4 -2
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/helpers.py +3 -1
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +35 -12
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1401 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
"""Classes for movie player interfaces.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
# Part of the PsychoPy library
|
|
8
|
-
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2025 Open Science Tools Ltd.
|
|
9
|
-
# Distributed under the terms of the GNU General Public License (GPL).
|
|
10
|
-
|
|
11
|
-
__all__ = [
|
|
12
|
-
'FFPyPlayer'
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
import sys
|
|
16
|
-
|
|
17
|
-
from ffpyplayer.player import MediaPlayer # very first thing to import
|
|
18
|
-
import time
|
|
19
|
-
import psychopy.logging as logging
|
|
20
|
-
import math
|
|
21
|
-
import numpy as np
|
|
22
|
-
import threading
|
|
23
|
-
import queue
|
|
24
|
-
from psychopy.core import getTime
|
|
25
|
-
from ._base import BaseMoviePlayer
|
|
26
|
-
from ..metadata import MovieMetadata
|
|
27
|
-
from ..frame import MovieFrame, NULL_MOVIE_FRAME_INFO
|
|
28
|
-
from psychopy.constants import (
|
|
29
|
-
FINISHED, NOT_STARTED, PAUSED, PLAYING, STOPPED, STOPPING, INVALID, SEEKING)
|
|
30
|
-
from psychopy.tools.filetools import pathToString
|
|
31
|
-
import atexit
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Options that PsychoPy devs picked to provide better performance, these can
|
|
35
|
-
# be overridden, but it might result in undefined behavior.
|
|
36
|
-
DEFAULT_FF_OPTS = {
|
|
37
|
-
'sync': 'audio', # sync to audio
|
|
38
|
-
'paused': True, # start paused
|
|
39
|
-
'autoexit': False, # don't exit ffmpeg automatically
|
|
40
|
-
'loop': 0 # enable looping
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
# default queue size for the stream reader
|
|
44
|
-
DEFAULT_FRAME_QUEUE_SIZE = 1
|
|
45
|
-
|
|
46
|
-
# event to close all opened movie reader threads
|
|
47
|
-
_evtCleanUpMovieEvent = threading.Event()
|
|
48
|
-
_evtCleanUpMovieEvent.clear()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Cleanup routines for threads. This allows the app to crash gracefully rather
|
|
52
|
-
# than locking up on error.
|
|
53
|
-
def _closeMovieThreads():
|
|
54
|
-
"""Callback function when the application exits which cleans up movie
|
|
55
|
-
threads. When this function is called, any outstanding movie threads will
|
|
56
|
-
be closed automatically. This must not be called at any other point of the
|
|
57
|
-
program.
|
|
58
|
-
"""
|
|
59
|
-
global _evtCleanUpMovieEvent
|
|
60
|
-
_evtCleanUpMovieEvent.set()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
atexit.register(_closeMovieThreads) # register the function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class StreamStatus:
|
|
67
|
-
"""Descriptor class for stream status.
|
|
68
|
-
|
|
69
|
-
This class is used to report the current status of the movie stream at the
|
|
70
|
-
time the movie frame was obtained.
|
|
71
|
-
|
|
72
|
-
Parameters
|
|
73
|
-
----------
|
|
74
|
-
status : int
|
|
75
|
-
Status flag for the stream.
|
|
76
|
-
streamTime : float
|
|
77
|
-
Current stream (movie) time in seconds. Resets after a loop has
|
|
78
|
-
completed.
|
|
79
|
-
frameIndex : int
|
|
80
|
-
Current frame index, increases monotonically as a movie plays and resets
|
|
81
|
-
when finished or beginning another loop.
|
|
82
|
-
loopCount : int
|
|
83
|
-
If looping is enabled, this value increases by 1 each time the movie
|
|
84
|
-
loops. Initial value is 0.
|
|
85
|
-
|
|
86
|
-
"""
|
|
87
|
-
__slots__ = ['_status',
|
|
88
|
-
'_streamTime',
|
|
89
|
-
'_frameIndex',
|
|
90
|
-
'_loopCount']
|
|
91
|
-
|
|
92
|
-
def __init__(self,
|
|
93
|
-
status=NOT_STARTED,
|
|
94
|
-
streamTime=0.0,
|
|
95
|
-
frameIndex=-1,
|
|
96
|
-
loopCount=-1):
|
|
97
|
-
|
|
98
|
-
self._status = int(status)
|
|
99
|
-
self._streamTime = float(streamTime)
|
|
100
|
-
self._frameIndex = frameIndex
|
|
101
|
-
self._loopCount = loopCount
|
|
102
|
-
|
|
103
|
-
@property
|
|
104
|
-
def status(self):
|
|
105
|
-
"""Status flag for the stream (`int`).
|
|
106
|
-
"""
|
|
107
|
-
return self._status
|
|
108
|
-
|
|
109
|
-
@property
|
|
110
|
-
def streamTime(self):
|
|
111
|
-
"""Current stream time in seconds (`float`).
|
|
112
|
-
|
|
113
|
-
This value increases monotonically and is common timebase for all
|
|
114
|
-
cameras attached to the system.
|
|
115
|
-
"""
|
|
116
|
-
return self._streamTime
|
|
117
|
-
|
|
118
|
-
@property
|
|
119
|
-
def frameIndex(self):
|
|
120
|
-
"""Current frame in the stream (`float`).
|
|
121
|
-
|
|
122
|
-
This value increases monotonically as the movie plays. The first frame
|
|
123
|
-
has an index of 0.
|
|
124
|
-
"""
|
|
125
|
-
return self._frameIndex
|
|
126
|
-
|
|
127
|
-
@property
|
|
128
|
-
def loopCount(self):
|
|
129
|
-
"""Number of times the movie has looped (`float`).
|
|
130
|
-
|
|
131
|
-
This value increases monotonically as the movie plays. This is
|
|
132
|
-
incremented when the movie finishes.
|
|
133
|
-
"""
|
|
134
|
-
return self._loopCount
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class StreamData:
|
|
138
|
-
"""Descriptor class for movie stream data.
|
|
139
|
-
|
|
140
|
-
Instances of this class are produced by the movie stream reader thread
|
|
141
|
-
which contains metadata about the stream, frame image data (i.e. pixel
|
|
142
|
-
values), and the stream status.
|
|
143
|
-
|
|
144
|
-
Parameters
|
|
145
|
-
----------
|
|
146
|
-
metadata : MovieMetadata
|
|
147
|
-
Stream metadata.
|
|
148
|
-
frameImage : object
|
|
149
|
-
Video frame image data.
|
|
150
|
-
streamStatus : StreamStatus
|
|
151
|
-
Video stream status.
|
|
152
|
-
cameraLib : str
|
|
153
|
-
Camera library in use to process the stream.
|
|
154
|
-
|
|
155
|
-
"""
|
|
156
|
-
__slots__ = ['_metadata',
|
|
157
|
-
'_frameImage',
|
|
158
|
-
'_streamStatus',
|
|
159
|
-
'_cameraLib']
|
|
160
|
-
|
|
161
|
-
def __init__(self, metadata, frameImage, streamStatus, cameraLib):
|
|
162
|
-
self._metadata = metadata
|
|
163
|
-
self._frameImage = frameImage
|
|
164
|
-
self._streamStatus = streamStatus
|
|
165
|
-
self._cameraLib = cameraLib
|
|
166
|
-
|
|
167
|
-
@property
|
|
168
|
-
def metadata(self):
|
|
169
|
-
"""Stream metadata at the time the video frame was acquired
|
|
170
|
-
(`MovieMetadata`).
|
|
171
|
-
"""
|
|
172
|
-
return self._metadata
|
|
173
|
-
|
|
174
|
-
@metadata.setter
|
|
175
|
-
def metadata(self, value):
|
|
176
|
-
if not isinstance(value, MovieMetadata) or value is not None:
|
|
177
|
-
raise TypeError("Incorrect type for property `metadata`, expected "
|
|
178
|
-
"`MovieMetadata` or `None`.")
|
|
179
|
-
|
|
180
|
-
self._metadata = value
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def frameImage(self):
|
|
184
|
-
"""Frame image data from the codec (`ffpyplayer.pic.Image`).
|
|
185
|
-
"""
|
|
186
|
-
return self._frameImage
|
|
187
|
-
|
|
188
|
-
@frameImage.setter
|
|
189
|
-
def frameImage(self, value):
|
|
190
|
-
self._frameImage = value
|
|
191
|
-
|
|
192
|
-
@property
|
|
193
|
-
def streamStatus(self):
|
|
194
|
-
"""Stream status (`StreamStatus`).
|
|
195
|
-
"""
|
|
196
|
-
return self._streamStatus
|
|
197
|
-
|
|
198
|
-
@streamStatus.setter
|
|
199
|
-
def streamStatus(self, value):
|
|
200
|
-
if not isinstance(value, StreamStatus) or value is not None:
|
|
201
|
-
raise TypeError("Incorrect type for property `streamStatus`, "
|
|
202
|
-
"expected `StreamStatus` or `None`.")
|
|
203
|
-
|
|
204
|
-
self._streamStatus = value
|
|
205
|
-
|
|
206
|
-
@property
|
|
207
|
-
def cameraLib(self):
|
|
208
|
-
"""Camera library in use to obtain the stream (`str`). Value is
|
|
209
|
-
blank if `metadata` is `None`.
|
|
210
|
-
"""
|
|
211
|
-
if self._metadata is not None:
|
|
212
|
-
return self._metadata.movieLib
|
|
213
|
-
|
|
214
|
-
return u''
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
class MovieStreamThreadFFPyPlayer(threading.Thread):
|
|
218
|
-
"""Class for reading movie streams asynchronously.
|
|
219
|
-
|
|
220
|
-
The rate of which frames are read is controlled dynamically based on values
|
|
221
|
-
within stream metadata. This will ensure that CPU load is kept to a minimum,
|
|
222
|
-
only polling for new frames at the rate they are being made available.
|
|
223
|
-
|
|
224
|
-
Parameters
|
|
225
|
-
----------
|
|
226
|
-
player : `ffpyplayer.player.MediaPlayer`
|
|
227
|
-
Media player instance, should be configured and initialized. Note that
|
|
228
|
-
player instance methods might not be thread-safe after handing off the
|
|
229
|
-
object to this thread.
|
|
230
|
-
bufferFrames : int
|
|
231
|
-
Number of frames to buffer. Sets the frame queue size for the thread.
|
|
232
|
-
Use a queue size >1 for video recorded with a framerate above 60Hz.
|
|
233
|
-
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
def __init__(self, player, bufferFrames=DEFAULT_FRAME_QUEUE_SIZE):
|
|
237
|
-
threading.Thread.__init__(self)
|
|
238
|
-
# Make this thread daemonic since we don't yet have a way of tracking
|
|
239
|
-
# them down. Since we're only reading resources, it's unlikely that
|
|
240
|
-
# we'll break or corrupt something. Good practice is to call `stop()`
|
|
241
|
-
# before exiting, this thread will join as usual and cleanly free up
|
|
242
|
-
# any resources.
|
|
243
|
-
self.daemon = True
|
|
244
|
-
|
|
245
|
-
self._player = player # player interface to FFMPEG
|
|
246
|
-
self._frameQueue = queue.Queue(maxsize=bufferFrames)
|
|
247
|
-
self._cmdQueue = queue.Queue() # queue for player commands
|
|
248
|
-
|
|
249
|
-
# some values the user might want
|
|
250
|
-
self._status = NOT_STARTED
|
|
251
|
-
self._streamTime = 0.0
|
|
252
|
-
self._isIdling = False
|
|
253
|
-
self._isFinished = False
|
|
254
|
-
|
|
255
|
-
# Locks for syncing the player and main application thread
|
|
256
|
-
self._warmUpLock = threading.Lock()
|
|
257
|
-
self._warmUpLock.acquire(blocking=False)
|
|
258
|
-
|
|
259
|
-
def run(self):
|
|
260
|
-
"""Main sub-routine for this thread.
|
|
261
|
-
|
|
262
|
-
When the thread is running, data about captured frames are put into the
|
|
263
|
-
`frameQueue` as `(metadata, img, pts)`. If the queue is empty, that
|
|
264
|
-
means the main application thread is running faster than the encoder
|
|
265
|
-
can get frames. Recommended behaviour in such cases it to return the
|
|
266
|
-
last valid frame when the queue is empty.
|
|
267
|
-
|
|
268
|
-
"""
|
|
269
|
-
global _evtCleanUpMovieEvent
|
|
270
|
-
if self._player is None:
|
|
271
|
-
return # exit thread if no player
|
|
272
|
-
|
|
273
|
-
# these should stay within the scope of this subroutine
|
|
274
|
-
frameInterval = 0.004 # frame interval, start at 4ms (250Hz)
|
|
275
|
-
frameData = None # frame data from the reader
|
|
276
|
-
val = '' # status value from reader
|
|
277
|
-
statusFlag = NOT_STARTED # status flag for stream reader state
|
|
278
|
-
frameIndex = -1 # frame index, 0 == first frame
|
|
279
|
-
loopCount = 0 # number of movie loops so far
|
|
280
|
-
mustShutdown = False # player thread should shut down
|
|
281
|
-
|
|
282
|
-
# Subroutines for various player functions -----------------------------
|
|
283
|
-
|
|
284
|
-
def seekTo(player, ptsTarget, maxAttempts=16):
|
|
285
|
-
"""Seek to a position in the video. Return the frame at that
|
|
286
|
-
position.
|
|
287
|
-
|
|
288
|
-
Parameters
|
|
289
|
-
----------
|
|
290
|
-
player : `MediaPlayer`
|
|
291
|
-
Handle to player.
|
|
292
|
-
ptsTarget : float
|
|
293
|
-
Location in the movie to seek to. Must be a positive number.
|
|
294
|
-
maxAttempts : int
|
|
295
|
-
Number of attempts to converge.
|
|
296
|
-
|
|
297
|
-
Returns
|
|
298
|
-
-------
|
|
299
|
-
tuple
|
|
300
|
-
Frame data and value from the `MediaPlayer` after seeking to the
|
|
301
|
-
position.
|
|
302
|
-
|
|
303
|
-
"""
|
|
304
|
-
wasPaused = player.get_pause()
|
|
305
|
-
player.set_pause(False)
|
|
306
|
-
player.set_mute(True)
|
|
307
|
-
|
|
308
|
-
# issue seek command to the player
|
|
309
|
-
player.seek(ptsTarget, relative=False, accurate=True)
|
|
310
|
-
# wait until we are at the seek position
|
|
311
|
-
n = 0
|
|
312
|
-
ptsLast = float(2 ** 32)
|
|
313
|
-
while n < maxAttempts: # converge on position
|
|
314
|
-
frameData_, val_ = player.get_frame(show=True)
|
|
315
|
-
if frameData_ is None:
|
|
316
|
-
time.sleep(0.0025)
|
|
317
|
-
n += 1
|
|
318
|
-
continue
|
|
319
|
-
|
|
320
|
-
_, pts_ = frameData_
|
|
321
|
-
ptsClock = player.get_pts()
|
|
322
|
-
# Check if the PTS is the same as the last attempt, if so
|
|
323
|
-
# we are likely not going to converge on some other value.
|
|
324
|
-
if math.isclose(ptsClock, ptsLast):
|
|
325
|
-
break
|
|
326
|
-
|
|
327
|
-
# If the PTS is different than the last one, check if it's
|
|
328
|
-
# close to the target.
|
|
329
|
-
if math.isclose(pts_, ptsTarget) and math.isclose(
|
|
330
|
-
ptsClock, ptsTarget):
|
|
331
|
-
break
|
|
332
|
-
|
|
333
|
-
ptsLast = ptsClock
|
|
334
|
-
n += 1
|
|
335
|
-
|
|
336
|
-
else:
|
|
337
|
-
frameData_, val_ = None, ''
|
|
338
|
-
|
|
339
|
-
player.set_mute(False)
|
|
340
|
-
player.set_pause(wasPaused)
|
|
341
|
-
|
|
342
|
-
return frameData_, val_
|
|
343
|
-
|
|
344
|
-
def calcFrameIndex(pts_, frameInterval_):
|
|
345
|
-
"""Calculate the frame index from the presentation time stamp and
|
|
346
|
-
frame interval.
|
|
347
|
-
|
|
348
|
-
Parameters
|
|
349
|
-
----------
|
|
350
|
-
pts_ : float
|
|
351
|
-
Presentation timestamp.
|
|
352
|
-
frameInterval_ : float
|
|
353
|
-
Frame interval of the movie in seconds.
|
|
354
|
-
|
|
355
|
-
Returns
|
|
356
|
-
-------
|
|
357
|
-
int
|
|
358
|
-
Frame index.
|
|
359
|
-
|
|
360
|
-
"""
|
|
361
|
-
return int(math.floor(pts_ / frameInterval_)) - 1
|
|
362
|
-
|
|
363
|
-
# ----------------------------------------------------------------------
|
|
364
|
-
# Initialization
|
|
365
|
-
#
|
|
366
|
-
# Warmup the reader and get the first frame, this will be presented when
|
|
367
|
-
# the player is first initialized, we should block until this process
|
|
368
|
-
# completes using a lock object. To get the first frame we start the
|
|
369
|
-
# video, acquire the frame, then seek to the beginning. The frame will
|
|
370
|
-
# remain in the queue until accessed. The first frame is important since
|
|
371
|
-
# it is needed to configure the texture buffers in the rendering thread.
|
|
372
|
-
#
|
|
373
|
-
|
|
374
|
-
# We need to start playback to access the first frame. This can be done
|
|
375
|
-
# "silently" by muting the audio and playing the video for a single
|
|
376
|
-
# frame. We then seek back to the beginning and pause the video. This
|
|
377
|
-
# will ensure the first frame is presented.
|
|
378
|
-
#
|
|
379
|
-
self._player.set_mute(True)
|
|
380
|
-
self._player.set_pause(False)
|
|
381
|
-
|
|
382
|
-
# consume frames until we get a valid one, need its metadata
|
|
383
|
-
while frameData is None or val == 'not ready':
|
|
384
|
-
frameData, val = self._player.get_frame(show=True)
|
|
385
|
-
# end of the file? ... at this point? something went wrong ...
|
|
386
|
-
if val == 'eof':
|
|
387
|
-
break
|
|
388
|
-
time.sleep(frameInterval) # sleep a bit to avoid mashing the CPU
|
|
389
|
-
|
|
390
|
-
# Obtain metadata from the frame now that we have a flowing stream. This
|
|
391
|
-
# data is needed by the main thread to process to configure additional
|
|
392
|
-
# resources needed to present the video.
|
|
393
|
-
metadata = self._player.get_metadata()
|
|
394
|
-
|
|
395
|
-
# Compute the frame interval that will be used, this is dynamically set
|
|
396
|
-
# to reduce the amount of CPU load when obtaining new frames. Aliasing
|
|
397
|
-
# may occur sometimes, possibly looking like a frame is being skipped,
|
|
398
|
-
# but we're not sure if this actually happens in practice.
|
|
399
|
-
frameRate = metadata['frame_rate']
|
|
400
|
-
numer, denom = frameRate
|
|
401
|
-
try:
|
|
402
|
-
frameInterval = 1.0 / (numer / float(denom))
|
|
403
|
-
except ZeroDivisionError:
|
|
404
|
-
# likely won't happen since we always get a valid frame before
|
|
405
|
-
# reaching here, but you never know ...
|
|
406
|
-
raise RuntimeError(
|
|
407
|
-
"Cannot play movie. Failed to acquire metadata from video "
|
|
408
|
-
"stream!")
|
|
409
|
-
|
|
410
|
-
# Get the movie duration, needed to determine when we get to the end of
|
|
411
|
-
# movie. We need to reset some params when there. This is in seconds.
|
|
412
|
-
duration = metadata['duration']
|
|
413
|
-
|
|
414
|
-
# Get color and timestamp data from the returned frame object, this will
|
|
415
|
-
# be encapsulated in a `StreamData` object and passed back to the main
|
|
416
|
-
# thread with status information.
|
|
417
|
-
colorData, pts = frameData
|
|
418
|
-
|
|
419
|
-
# Build up the object which we'll pass to the application thread. Stream
|
|
420
|
-
# status information hold timestamp and playback information.
|
|
421
|
-
streamStatus = StreamStatus(
|
|
422
|
-
status=statusFlag, # current status flag, should be `NOT_STARTED`
|
|
423
|
-
streamTime=pts) # frame timestamp
|
|
424
|
-
|
|
425
|
-
# Put the frame in the frame queue so the main thread can read access it
|
|
426
|
-
# safely. The main thread should hold onto any frame it gets when the
|
|
427
|
-
# queue is empty.
|
|
428
|
-
if self._frameQueue.full():
|
|
429
|
-
raise RuntimeError(
|
|
430
|
-
"Movie decoder frame queue is full and it really shouldn't be "
|
|
431
|
-
"at this point.")
|
|
432
|
-
|
|
433
|
-
# Object to pass video frame data back to the application thread for
|
|
434
|
-
# presentation or processing.
|
|
435
|
-
lastFrame = StreamData(
|
|
436
|
-
metadata,
|
|
437
|
-
colorData,
|
|
438
|
-
streamStatus,
|
|
439
|
-
u'ffpyplayer')
|
|
440
|
-
|
|
441
|
-
# Pass the object to the main thread using the frame queue.
|
|
442
|
-
self._frameQueue.put(lastFrame) # put frame data in here
|
|
443
|
-
|
|
444
|
-
# Rewind back to the beginning of the file, we should have the first
|
|
445
|
-
# frame and metadata from the file by now.
|
|
446
|
-
self._player.set_pause(True) # start paused
|
|
447
|
-
self._player.set_mute(False)
|
|
448
|
-
# frameData, val = seekTo(self._player, 0.0)
|
|
449
|
-
|
|
450
|
-
# set the volume again because irt doesn't seem to remember it
|
|
451
|
-
self._player.set_volume(self._player.get_volume())
|
|
452
|
-
|
|
453
|
-
# Release the lock to unblock the parent thread once we have the first
|
|
454
|
-
# frame and valid metadata from the stream. After this returns the
|
|
455
|
-
# main thread should call `getRecentFrame` to get the frame data.
|
|
456
|
-
self._warmUpLock.release()
|
|
457
|
-
|
|
458
|
-
# ----------------------------------------------------------------------
|
|
459
|
-
# Playback
|
|
460
|
-
#
|
|
461
|
-
# Main playback loop, this will continually pull frames from the stream
|
|
462
|
-
# and push them into the frame queue. The user can pause and resume
|
|
463
|
-
# playback. Avoid blocking anything outside the use of timers to prevent
|
|
464
|
-
# stalling the thread.
|
|
465
|
-
#
|
|
466
|
-
while 1:
|
|
467
|
-
# pull a new frame
|
|
468
|
-
frameData, val = self._player.get_frame()
|
|
469
|
-
# if no frame, just pause the thread and restart the loop
|
|
470
|
-
if val == 'eof': # end of stream/file
|
|
471
|
-
self._isFinished = self._isIdling = True
|
|
472
|
-
time.sleep(frameInterval)
|
|
473
|
-
elif frameData is None or val == 'paused': # paused or not ready
|
|
474
|
-
self._isIdling = True
|
|
475
|
-
self._isFinished = False
|
|
476
|
-
time.sleep(frameInterval)
|
|
477
|
-
else: # playing
|
|
478
|
-
self._isIdling = self._isFinished = False
|
|
479
|
-
colorData, pts = frameData # got a valid frame
|
|
480
|
-
|
|
481
|
-
# updated last valid frame data
|
|
482
|
-
lastFrame = StreamData(
|
|
483
|
-
metadata,
|
|
484
|
-
colorData,
|
|
485
|
-
StreamStatus(
|
|
486
|
-
status=statusFlag, # might remove
|
|
487
|
-
streamTime=pts,
|
|
488
|
-
frameIndex=calcFrameIndex(pts, frameInterval),
|
|
489
|
-
loopCount=loopCount),
|
|
490
|
-
u'ffpyplayer')
|
|
491
|
-
|
|
492
|
-
# is the next frame the last? increment the number of loops then
|
|
493
|
-
if pts + frameInterval * 1.5 >= duration:
|
|
494
|
-
loopCount += 1 # inc number of loops
|
|
495
|
-
|
|
496
|
-
if isinstance(val, float):
|
|
497
|
-
time.sleep(val) # time to sleep
|
|
498
|
-
else:
|
|
499
|
-
time.sleep(frameInterval)
|
|
500
|
-
|
|
501
|
-
# If the queue is full, just discard the frame and get the
|
|
502
|
-
# next one to allow us to catch up.
|
|
503
|
-
try:
|
|
504
|
-
self._frameQueue.put_nowait(lastFrame)
|
|
505
|
-
except queue.Full:
|
|
506
|
-
pass # do nothing
|
|
507
|
-
|
|
508
|
-
# ------------------------------------------------------------------
|
|
509
|
-
# Process playback controls
|
|
510
|
-
#
|
|
511
|
-
# Check the command queue for playback commands. Process all
|
|
512
|
-
# commands in the queue before progressing. A command is a tuple put
|
|
513
|
-
# into the queue where the first value is the op-code and the second
|
|
514
|
-
# is the value:
|
|
515
|
-
#
|
|
516
|
-
# OPCODE, VALUE = COMMAND
|
|
517
|
-
#
|
|
518
|
-
# The op-code is a string specifying the command to execute, while
|
|
519
|
-
# the value can be any object needed to carry out the command.
|
|
520
|
-
# Possible opcodes and their values are shown in the table below:
|
|
521
|
-
#
|
|
522
|
-
# OPCODE | VALUE | DESCRIPTION
|
|
523
|
-
# ------------+--------------------+------------------------------
|
|
524
|
-
# 'volume' | float (0.0 -> 1.0) | Set the volume
|
|
525
|
-
# 'mute' | bool | Enable/disable sound
|
|
526
|
-
# 'play' | None | Play a stream
|
|
527
|
-
# 'pause' | None | Pause a stream
|
|
528
|
-
# 'stop' | None | Pause and restart
|
|
529
|
-
# 'seek' | pts, bool | Seek to a movie position
|
|
530
|
-
# 'shutdown' | None | Kill the thread
|
|
531
|
-
#
|
|
532
|
-
needsWait = False
|
|
533
|
-
if not self._cmdQueue.empty():
|
|
534
|
-
cmdOpCode, cmdVal = self._cmdQueue.get_nowait()
|
|
535
|
-
|
|
536
|
-
# process the command
|
|
537
|
-
if cmdOpCode == 'volume': # set the volume
|
|
538
|
-
self._player.set_volume(float(cmdVal))
|
|
539
|
-
needsWait = True
|
|
540
|
-
elif cmdOpCode == 'mute':
|
|
541
|
-
self._player.set_mute(bool(cmdVal))
|
|
542
|
-
needsWait = True
|
|
543
|
-
elif cmdOpCode == 'play':
|
|
544
|
-
self._player.set_mute(False)
|
|
545
|
-
self._player.set_pause(False)
|
|
546
|
-
elif cmdOpCode == 'pause':
|
|
547
|
-
self._player.set_mute(True)
|
|
548
|
-
self._player.set_pause(True)
|
|
549
|
-
elif cmdOpCode == 'seek':
|
|
550
|
-
seekToPts, seekRel = cmdVal
|
|
551
|
-
self._player.seek(
|
|
552
|
-
seekToPts,
|
|
553
|
-
relative=seekRel,
|
|
554
|
-
accurate=True)
|
|
555
|
-
time.sleep(0.1) # long wait for seeking
|
|
556
|
-
elif cmdOpCode == 'stop': # stop playback, return to start
|
|
557
|
-
self._player.set_mute(True)
|
|
558
|
-
self._player.seek(
|
|
559
|
-
-1.0, # seek to beginning
|
|
560
|
-
relative=False,
|
|
561
|
-
accurate=True)
|
|
562
|
-
self._player.set_pause(True)
|
|
563
|
-
loopCount = 0 # reset loop count
|
|
564
|
-
time.sleep(0.1)
|
|
565
|
-
elif cmdOpCode == 'shutdown': # shutdown the player
|
|
566
|
-
mustShutdown = True
|
|
567
|
-
|
|
568
|
-
# signal to the main thread that the command has been processed
|
|
569
|
-
if not mustShutdown:
|
|
570
|
-
self._cmdQueue.task_done()
|
|
571
|
-
else:
|
|
572
|
-
break
|
|
573
|
-
|
|
574
|
-
# if the command needs some additional processing time
|
|
575
|
-
if needsWait:
|
|
576
|
-
time.sleep(frameInterval)
|
|
577
|
-
|
|
578
|
-
# close the player when the thread exits
|
|
579
|
-
self._player.close_player()
|
|
580
|
-
self._cmdQueue.task_done()
|
|
581
|
-
|
|
582
|
-
@property
|
|
583
|
-
def isFinished(self):
|
|
584
|
-
"""Is the movie done playing (`bool`)? This is `True` if the movie
|
|
585
|
-
stream is at EOF.
|
|
586
|
-
"""
|
|
587
|
-
return self._isFinished
|
|
588
|
-
|
|
589
|
-
@property
|
|
590
|
-
def isIdling(self):
|
|
591
|
-
"""Is the movie reader thread "idling" (`bool`)? If `True`, the movie is
|
|
592
|
-
finished playing and no frames are being polled from FFMPEG.
|
|
593
|
-
"""
|
|
594
|
-
return self._isIdling
|
|
595
|
-
|
|
596
|
-
@property
|
|
597
|
-
def isReady(self):
|
|
598
|
-
"""`True` if the stream reader is ready (`bool`).
|
|
599
|
-
"""
|
|
600
|
-
return not self._warmUpLock.locked()
|
|
601
|
-
|
|
602
|
-
def begin(self):
|
|
603
|
-
"""Call this to start the thread and begin reading frames. This will
|
|
604
|
-
block until we get a valid frame.
|
|
605
|
-
"""
|
|
606
|
-
self.start() # start the thread, will begin decoding frames
|
|
607
|
-
# hold until the lock is released when the thread gets a valid frame
|
|
608
|
-
# this will prevent the main loop for executing until we're ready
|
|
609
|
-
self._warmUpLock.acquire(blocking=True)
|
|
610
|
-
|
|
611
|
-
def play(self):
|
|
612
|
-
"""Start playing the video from the stream.
|
|
613
|
-
"""
|
|
614
|
-
cmd = ('play', None)
|
|
615
|
-
self._cmdQueue.put(cmd)
|
|
616
|
-
self._cmdQueue.join()
|
|
617
|
-
|
|
618
|
-
def pause(self):
|
|
619
|
-
"""Stop recording frames to the output file.
|
|
620
|
-
"""
|
|
621
|
-
cmd = ('pause', None)
|
|
622
|
-
self._cmdQueue.put(cmd)
|
|
623
|
-
self._cmdQueue.join()
|
|
624
|
-
|
|
625
|
-
def seek(self, pts, relative=False):
|
|
626
|
-
"""Seek to a position in the video.
|
|
627
|
-
"""
|
|
628
|
-
cmd = ('seek', (pts, relative))
|
|
629
|
-
self._cmdQueue.put(cmd)
|
|
630
|
-
self._cmdQueue.join()
|
|
631
|
-
|
|
632
|
-
def stop(self):
|
|
633
|
-
"""Stop playback, reset the movie to the beginning.
|
|
634
|
-
"""
|
|
635
|
-
cmd = ('stop', None)
|
|
636
|
-
self._cmdQueue.put(cmd)
|
|
637
|
-
self._cmdQueue.join()
|
|
638
|
-
|
|
639
|
-
def shutdown(self):
|
|
640
|
-
"""Shutdown the movie reader thread.
|
|
641
|
-
"""
|
|
642
|
-
cmd = ('shutdown', None)
|
|
643
|
-
self._cmdQueue.put(cmd)
|
|
644
|
-
self._cmdQueue.join()
|
|
645
|
-
|
|
646
|
-
def isDone(self):
|
|
647
|
-
"""Check if the video is done playing.
|
|
648
|
-
|
|
649
|
-
Returns
|
|
650
|
-
-------
|
|
651
|
-
bool
|
|
652
|
-
Is the video done?
|
|
653
|
-
|
|
654
|
-
"""
|
|
655
|
-
return not self.is_alive()
|
|
656
|
-
|
|
657
|
-
def getVolume(self):
|
|
658
|
-
"""Get the current volume level."""
|
|
659
|
-
if self._player is not None:
|
|
660
|
-
return self._player.get_volume()
|
|
661
|
-
|
|
662
|
-
return 0.0
|
|
663
|
-
|
|
664
|
-
def setVolume(self, volume):
|
|
665
|
-
"""Set the volume for the video.
|
|
666
|
-
|
|
667
|
-
Parameters
|
|
668
|
-
----------
|
|
669
|
-
volume : float
|
|
670
|
-
New volume level, ranging between 0 and 1.
|
|
671
|
-
|
|
672
|
-
"""
|
|
673
|
-
cmd = ('volume', volume)
|
|
674
|
-
self._cmdQueue.put(cmd)
|
|
675
|
-
self._cmdQueue.join()
|
|
676
|
-
|
|
677
|
-
def setMute(self, mute):
|
|
678
|
-
"""Set the volume for the video.
|
|
679
|
-
|
|
680
|
-
Parameters
|
|
681
|
-
----------
|
|
682
|
-
mute : bool
|
|
683
|
-
Mute state. If `True`, audio will be muted.
|
|
684
|
-
|
|
685
|
-
"""
|
|
686
|
-
cmd = ('mute', mute)
|
|
687
|
-
self._cmdQueue.put(cmd)
|
|
688
|
-
self._cmdQueue.join()
|
|
689
|
-
|
|
690
|
-
def getRecentFrame(self):
|
|
691
|
-
"""Get the most recent frame data from the feed (`tuple`).
|
|
692
|
-
|
|
693
|
-
Returns
|
|
694
|
-
-------
|
|
695
|
-
tuple or None
|
|
696
|
-
Frame data formatted as `(metadata, frameData, val)`. The `metadata`
|
|
697
|
-
is a `dict`, `frameData` is a `tuple` with format (`colorData`,
|
|
698
|
-
`pts`) and `val` is a `str` returned by the
|
|
699
|
-
`MediaPlayer.get_frame()` method. Returns `None` if there is no
|
|
700
|
-
frame data.
|
|
701
|
-
|
|
702
|
-
"""
|
|
703
|
-
if self._frameQueue.empty():
|
|
704
|
-
return None
|
|
705
|
-
|
|
706
|
-
# hold only last frame and return that instead of None?
|
|
707
|
-
return self._frameQueue.get()
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
class FFPyPlayer(BaseMoviePlayer):
|
|
711
|
-
"""Interface class for the FFPyPlayer library for use with `MovieStim`.
|
|
712
|
-
|
|
713
|
-
This class also serves as the reference implementation for classes which
|
|
714
|
-
interface with movie codec libraries for use with `MovieStim`. Creating new
|
|
715
|
-
player classes which closely replicate the behaviour of this one should
|
|
716
|
-
allow them to smoothly plug into `MovieStim`.
|
|
717
|
-
|
|
718
|
-
"""
|
|
719
|
-
_movieLib = 'ffpyplayer'
|
|
720
|
-
|
|
721
|
-
def __init__(self, parent):
|
|
722
|
-
self._filename = u""
|
|
723
|
-
|
|
724
|
-
self.parent = parent
|
|
725
|
-
|
|
726
|
-
# handle to `ffpyplayer`
|
|
727
|
-
self._handle = None
|
|
728
|
-
|
|
729
|
-
# thread for reading frames asynchronously
|
|
730
|
-
self._tStream = None
|
|
731
|
-
|
|
732
|
-
# data from stream thread
|
|
733
|
-
self._lastFrame = NULL_MOVIE_FRAME_INFO
|
|
734
|
-
self._frameIndex = -1
|
|
735
|
-
self._loopCount = 0
|
|
736
|
-
self._metadata = None # metadata from the stream
|
|
737
|
-
|
|
738
|
-
self._lastPlayerOpts = DEFAULT_FF_OPTS.copy()
|
|
739
|
-
|
|
740
|
-
self._lastPlayerOpts['out_fmt'] = 'bgra'
|
|
741
|
-
|
|
742
|
-
# options from the parent
|
|
743
|
-
if self.parent.loop: # infinite loop
|
|
744
|
-
self._lastPlayerOpts['loop'] = 0
|
|
745
|
-
else:
|
|
746
|
-
self._lastPlayerOpts['loop'] = 1 # play once
|
|
747
|
-
|
|
748
|
-
if hasattr(self.parent, '_noAudio'):
|
|
749
|
-
self._lastPlayerOpts['an'] = self.parent._noAudio
|
|
750
|
-
|
|
751
|
-
# status flags
|
|
752
|
-
self._status = NOT_STARTED
|
|
753
|
-
|
|
754
|
-
def start(self, log=True):
|
|
755
|
-
"""Initialize and start the decoder. This method will return when a
|
|
756
|
-
valid frame is made available.
|
|
757
|
-
|
|
758
|
-
"""
|
|
759
|
-
# clear queued data from previous streams
|
|
760
|
-
self._lastFrame = None
|
|
761
|
-
self._frameIndex = -1
|
|
762
|
-
|
|
763
|
-
# open the media player
|
|
764
|
-
handle = MediaPlayer(self._filename, ff_opts=self._lastPlayerOpts)
|
|
765
|
-
handle.set_pause(True)
|
|
766
|
-
|
|
767
|
-
# Pull the first frame to get metadata. NB - `_enqueueFrame` should be
|
|
768
|
-
# able to do this but the logic in there depends on having access to
|
|
769
|
-
# metadata first. That may be rewritten at some point to reduce all of
|
|
770
|
-
# this to just a single `_enqeueFrame` call.
|
|
771
|
-
#
|
|
772
|
-
self._status = NOT_STARTED
|
|
773
|
-
|
|
774
|
-
# hand off the player interface to the thread
|
|
775
|
-
self._tStream = MovieStreamThreadFFPyPlayer(handle)
|
|
776
|
-
self._tStream.begin()
|
|
777
|
-
|
|
778
|
-
# make sure we have metadata
|
|
779
|
-
self.update()
|
|
780
|
-
|
|
781
|
-
def load(self, pathToMovie):
|
|
782
|
-
"""Load a movie file from disk.
|
|
783
|
-
|
|
784
|
-
Parameters
|
|
785
|
-
----------
|
|
786
|
-
pathToMovie : str
|
|
787
|
-
Path to movie file, stream (URI) or camera. Must be a format that
|
|
788
|
-
FFMPEG supports.
|
|
789
|
-
|
|
790
|
-
"""
|
|
791
|
-
# set the file path
|
|
792
|
-
self._filename = pathToString(pathToMovie)
|
|
793
|
-
|
|
794
|
-
# Check if the player is already started. Close it and load a new
|
|
795
|
-
# instance if so.
|
|
796
|
-
if self._tStream is not None: # player already started
|
|
797
|
-
# make sure it's the correct type
|
|
798
|
-
# if not isinstance(self._handle, MediaPlayer):
|
|
799
|
-
# raise TypeError(
|
|
800
|
-
# 'Incorrect type for `FFMovieStim._player`, expected '
|
|
801
|
-
# '`ffpyplayer.player.MediaPlayer`. Got type `{}` '
|
|
802
|
-
# 'instead.'.format(type(self._handle).__name__))
|
|
803
|
-
|
|
804
|
-
# close the player and reset
|
|
805
|
-
self.unload()
|
|
806
|
-
|
|
807
|
-
# self._selectWindow(self.win) # free buffers here !!!
|
|
808
|
-
|
|
809
|
-
self.start()
|
|
810
|
-
|
|
811
|
-
self._status = NOT_STARTED
|
|
812
|
-
|
|
813
|
-
def unload(self):
|
|
814
|
-
"""Unload the video stream and reset.
|
|
815
|
-
"""
|
|
816
|
-
self._tStream.shutdown()
|
|
817
|
-
self._tStream.join() # wait until thread exits
|
|
818
|
-
self._tStream = None
|
|
819
|
-
|
|
820
|
-
# if self._handle is not None:
|
|
821
|
-
# self._handle.close_player()
|
|
822
|
-
# self._handle = None # reset
|
|
823
|
-
|
|
824
|
-
self._filename = u""
|
|
825
|
-
self._frameIndex = -1
|
|
826
|
-
self._handle = None # reset
|
|
827
|
-
|
|
828
|
-
# @property
|
|
829
|
-
# def handle(self):
|
|
830
|
-
# """Handle to the `MediaPlayer` object exposed by FFPyPlayer. If `None`,
|
|
831
|
-
# no media player object has yet been initialized.
|
|
832
|
-
# """
|
|
833
|
-
# return self._handle
|
|
834
|
-
|
|
835
|
-
@property
|
|
836
|
-
def isLoaded(self):
|
|
837
|
-
return self._tStream is not None
|
|
838
|
-
|
|
839
|
-
@property
|
|
840
|
-
def metadata(self):
|
|
841
|
-
"""Most recent metadata (`MovieMetadata`).
|
|
842
|
-
"""
|
|
843
|
-
return self.getMetadata()
|
|
844
|
-
|
|
845
|
-
def getMetadata(self):
|
|
846
|
-
"""Get metadata from the movie stream.
|
|
847
|
-
|
|
848
|
-
Returns
|
|
849
|
-
-------
|
|
850
|
-
MovieMetadata
|
|
851
|
-
Movie metadata object. If no movie is loaded, `NULL_MOVIE_METADATA`
|
|
852
|
-
is returned. At a minimum, fields `duration`, `size`, and
|
|
853
|
-
`frameRate` are populated if a valid movie has been previously
|
|
854
|
-
loaded.
|
|
855
|
-
|
|
856
|
-
"""
|
|
857
|
-
self._assertMediaPlayer()
|
|
858
|
-
|
|
859
|
-
metadata = self._metadata
|
|
860
|
-
|
|
861
|
-
# write metadata to the fields of a `MovieMetadata` object
|
|
862
|
-
toReturn = MovieMetadata(
|
|
863
|
-
mediaPath=self._filename,
|
|
864
|
-
title=metadata['title'],
|
|
865
|
-
duration=metadata['duration'],
|
|
866
|
-
frameRate=metadata['frame_rate'],
|
|
867
|
-
size=metadata['src_vid_size'],
|
|
868
|
-
pixelFormat=metadata['src_pix_fmt'],
|
|
869
|
-
movieLib=self._movieLib,
|
|
870
|
-
userData=None
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
return toReturn
|
|
874
|
-
|
|
875
|
-
def _assertMediaPlayer(self):
|
|
876
|
-
"""Ensure the media player instance is available. Raises a
|
|
877
|
-
`RuntimeError` if no movie is loaded.
|
|
878
|
-
"""
|
|
879
|
-
if self._tStream is not None:
|
|
880
|
-
return # nop if we're good
|
|
881
|
-
|
|
882
|
-
raise RuntimeError(
|
|
883
|
-
"Calling this class method requires a successful call to "
|
|
884
|
-
"`load` first.")
|
|
885
|
-
|
|
886
|
-
@property
|
|
887
|
-
def status(self):
|
|
888
|
-
"""Player status flag (`int`).
|
|
889
|
-
"""
|
|
890
|
-
return self._status
|
|
891
|
-
|
|
892
|
-
@property
|
|
893
|
-
def isPlaying(self):
|
|
894
|
-
"""`True` if the video is presently playing (`bool`)."""
|
|
895
|
-
# Status flags as properties are pretty useful for users since they are
|
|
896
|
-
# self documenting and prevent the user from touching the status flag
|
|
897
|
-
# attribute directly.
|
|
898
|
-
#
|
|
899
|
-
return self.status == PLAYING
|
|
900
|
-
|
|
901
|
-
@property
|
|
902
|
-
def isNotStarted(self):
|
|
903
|
-
"""`True` if the video has not be started yet (`bool`). This status is
|
|
904
|
-
given after a video is loaded and play has yet to be called.
|
|
905
|
-
"""
|
|
906
|
-
return self.status == NOT_STARTED
|
|
907
|
-
|
|
908
|
-
@property
|
|
909
|
-
def isStopped(self):
|
|
910
|
-
"""`True` if the movie has been stopped.
|
|
911
|
-
"""
|
|
912
|
-
return self.status == STOPPED
|
|
913
|
-
|
|
914
|
-
@property
|
|
915
|
-
def isPaused(self):
|
|
916
|
-
"""`True` if the movie has been paused.
|
|
917
|
-
"""
|
|
918
|
-
self._assertMediaPlayer()
|
|
919
|
-
|
|
920
|
-
return self._status == PAUSED
|
|
921
|
-
|
|
922
|
-
@property
|
|
923
|
-
def isFinished(self):
|
|
924
|
-
"""`True` if the video is finished (`bool`).
|
|
925
|
-
"""
|
|
926
|
-
return self._tStream.isFinished
|
|
927
|
-
|
|
928
|
-
def play(self, log=False):
|
|
929
|
-
"""Start or continue a paused movie from current position.
|
|
930
|
-
|
|
931
|
-
Parameters
|
|
932
|
-
----------
|
|
933
|
-
log : bool
|
|
934
|
-
Log the play event.
|
|
935
|
-
|
|
936
|
-
Returns
|
|
937
|
-
-------
|
|
938
|
-
int or None
|
|
939
|
-
Frame index playback started at. Should always be `0` if starting at
|
|
940
|
-
the beginning of the video. Returns `None` if the player has not
|
|
941
|
-
been initialized.
|
|
942
|
-
|
|
943
|
-
"""
|
|
944
|
-
self._assertMediaPlayer()
|
|
945
|
-
|
|
946
|
-
self._tStream.play()
|
|
947
|
-
self._status = PLAYING
|
|
948
|
-
|
|
949
|
-
def stop(self, log=False):
|
|
950
|
-
"""Stop the current point in the movie (sound will stop, current frame
|
|
951
|
-
will not advance). Once stopped the movie cannot be restarted - it must
|
|
952
|
-
be loaded again.
|
|
953
|
-
|
|
954
|
-
Use `pause()` instead if you may need to restart the movie.
|
|
955
|
-
|
|
956
|
-
Parameters
|
|
957
|
-
----------
|
|
958
|
-
log : bool
|
|
959
|
-
Log the stop event.
|
|
960
|
-
|
|
961
|
-
"""
|
|
962
|
-
self._tStream.stop()
|
|
963
|
-
self._status = STOPPED
|
|
964
|
-
|
|
965
|
-
def pause(self, log=False):
|
|
966
|
-
"""Pause the current point in the movie. The image of the last frame
|
|
967
|
-
will persist on-screen until `play()` or `stop()` are called.
|
|
968
|
-
|
|
969
|
-
Parameters
|
|
970
|
-
----------
|
|
971
|
-
log : bool
|
|
972
|
-
Log this event.
|
|
973
|
-
|
|
974
|
-
"""
|
|
975
|
-
self._assertMediaPlayer()
|
|
976
|
-
|
|
977
|
-
self._tStream.pause()
|
|
978
|
-
self._enqueueFrame()
|
|
979
|
-
|
|
980
|
-
self._status = PAUSED
|
|
981
|
-
|
|
982
|
-
return False
|
|
983
|
-
|
|
984
|
-
def seek(self, timestamp, log=False):
|
|
985
|
-
"""Seek to a particular timestamp in the movie.
|
|
986
|
-
|
|
987
|
-
Parameters
|
|
988
|
-
----------
|
|
989
|
-
timestamp : float
|
|
990
|
-
Time in seconds.
|
|
991
|
-
log : bool
|
|
992
|
-
Log the seek event.
|
|
993
|
-
|
|
994
|
-
"""
|
|
995
|
-
self._assertMediaPlayer()
|
|
996
|
-
self._tStream.seek(timestamp, relative=False)
|
|
997
|
-
self._enqueueFrame()
|
|
998
|
-
|
|
999
|
-
def rewind(self, seconds=5, log=False):
|
|
1000
|
-
"""Rewind the video.
|
|
1001
|
-
|
|
1002
|
-
Parameters
|
|
1003
|
-
----------
|
|
1004
|
-
seconds : float
|
|
1005
|
-
Time in seconds to rewind from the current position. Default is 5
|
|
1006
|
-
seconds.
|
|
1007
|
-
log : bool
|
|
1008
|
-
Log this event.
|
|
1009
|
-
|
|
1010
|
-
Returns
|
|
1011
|
-
-------
|
|
1012
|
-
float
|
|
1013
|
-
Timestamp after rewinding the video.
|
|
1014
|
-
|
|
1015
|
-
"""
|
|
1016
|
-
self._assertMediaPlayer()
|
|
1017
|
-
self._tStream.seek(-seconds, relative=True)
|
|
1018
|
-
|
|
1019
|
-
def fastForward(self, seconds=5, log=False):
|
|
1020
|
-
"""Fast-forward the video.
|
|
1021
|
-
|
|
1022
|
-
Parameters
|
|
1023
|
-
----------
|
|
1024
|
-
seconds : float
|
|
1025
|
-
Time in seconds to fast forward from the current position. Default
|
|
1026
|
-
is 5 seconds.
|
|
1027
|
-
log : bool
|
|
1028
|
-
Log this event.
|
|
1029
|
-
|
|
1030
|
-
"""
|
|
1031
|
-
self._assertMediaPlayer()
|
|
1032
|
-
self._tStream.seek(seconds, relative=True)
|
|
1033
|
-
|
|
1034
|
-
def replay(self, autoStart=False, log=False):
|
|
1035
|
-
"""Replay the movie from the beginning.
|
|
1036
|
-
|
|
1037
|
-
Parameters
|
|
1038
|
-
----------
|
|
1039
|
-
autoStart : bool
|
|
1040
|
-
Start playback immediately. If `False`, you must call `play()`
|
|
1041
|
-
afterwards to initiate playback.
|
|
1042
|
-
log : bool
|
|
1043
|
-
Log this event.
|
|
1044
|
-
|
|
1045
|
-
"""
|
|
1046
|
-
self._assertMediaPlayer()
|
|
1047
|
-
self.pause(log=log)
|
|
1048
|
-
self.seek(0.0, log=log)
|
|
1049
|
-
|
|
1050
|
-
if autoStart:
|
|
1051
|
-
self.play(log=log)
|
|
1052
|
-
|
|
1053
|
-
def restart(self, autoStart=True, log=False):
|
|
1054
|
-
"""Restart the movie from the beginning.
|
|
1055
|
-
|
|
1056
|
-
Parameters
|
|
1057
|
-
----------
|
|
1058
|
-
autoStart : bool
|
|
1059
|
-
Start playback immediately. If `False`, you must call `play()`
|
|
1060
|
-
afterwards to initiate playback.
|
|
1061
|
-
log : bool
|
|
1062
|
-
Log this event.
|
|
1063
|
-
|
|
1064
|
-
Notes
|
|
1065
|
-
-----
|
|
1066
|
-
* This tears down the current media player instance and creates a new
|
|
1067
|
-
one. Similar to calling `stop()` and `loadMovie()`. Use `seek(0.0)` if
|
|
1068
|
-
you would like to restart the movie without reloading.
|
|
1069
|
-
|
|
1070
|
-
"""
|
|
1071
|
-
lastMovieFile = self._filename
|
|
1072
|
-
self.load(lastMovieFile) # will play if auto start
|
|
1073
|
-
|
|
1074
|
-
# --------------------------------------------------------------------------
|
|
1075
|
-
# Audio stream control methods
|
|
1076
|
-
#
|
|
1077
|
-
|
|
1078
|
-
# @property
|
|
1079
|
-
# def muted(self):
|
|
1080
|
-
# """`True` if the stream audio is muted (`bool`).
|
|
1081
|
-
# """
|
|
1082
|
-
# return self._handle.get_mute() # thread-safe?
|
|
1083
|
-
#
|
|
1084
|
-
# @muted.setter
|
|
1085
|
-
# def muted(self, value):
|
|
1086
|
-
# self._tStream.setMute(value)
|
|
1087
|
-
|
|
1088
|
-
def volumeUp(self, amount):
|
|
1089
|
-
"""Increase the volume by a fixed amount.
|
|
1090
|
-
|
|
1091
|
-
Parameters
|
|
1092
|
-
----------
|
|
1093
|
-
amount : float or int
|
|
1094
|
-
Amount to increase the volume relative to the current volume.
|
|
1095
|
-
|
|
1096
|
-
"""
|
|
1097
|
-
self._assertMediaPlayer()
|
|
1098
|
-
|
|
1099
|
-
# get the current volume from the player
|
|
1100
|
-
self.volume = self.volume + amount
|
|
1101
|
-
|
|
1102
|
-
return self.volume
|
|
1103
|
-
|
|
1104
|
-
def volumeDown(self, amount):
|
|
1105
|
-
"""Decrease the volume by a fixed amount.
|
|
1106
|
-
|
|
1107
|
-
Parameters
|
|
1108
|
-
----------
|
|
1109
|
-
amount : float or int
|
|
1110
|
-
Amount to decrease the volume relative to the current volume.
|
|
1111
|
-
|
|
1112
|
-
"""
|
|
1113
|
-
self._assertMediaPlayer()
|
|
1114
|
-
|
|
1115
|
-
# get the current volume from the player
|
|
1116
|
-
self.volume = self.volume - amount
|
|
1117
|
-
|
|
1118
|
-
return self.volume
|
|
1119
|
-
|
|
1120
|
-
@property
|
|
1121
|
-
def volume(self):
|
|
1122
|
-
"""Volume for the audio track for this movie (`int` or `float`).
|
|
1123
|
-
"""
|
|
1124
|
-
self._assertMediaPlayer()
|
|
1125
|
-
|
|
1126
|
-
return self._tStream.getVolume()
|
|
1127
|
-
|
|
1128
|
-
@volume.setter
|
|
1129
|
-
def volume(self, value):
|
|
1130
|
-
self._assertMediaPlayer()
|
|
1131
|
-
self._tStream.setVolume(max(min(value, 1.0), 0.0))
|
|
1132
|
-
|
|
1133
|
-
@property
|
|
1134
|
-
def loopCount(self):
|
|
1135
|
-
"""Number of loops completed since playback started (`int`). This value
|
|
1136
|
-
is reset when either `stop` or `loadMovie` is called.
|
|
1137
|
-
"""
|
|
1138
|
-
return self._loopCount
|
|
1139
|
-
|
|
1140
|
-
# --------------------------------------------------------------------------
|
|
1141
|
-
# Timing related methods
|
|
1142
|
-
#
|
|
1143
|
-
# The methods here are used to handle timing, such as converting between
|
|
1144
|
-
# movie and experiment timestamps.
|
|
1145
|
-
#
|
|
1146
|
-
|
|
1147
|
-
@property
|
|
1148
|
-
def pts(self):
|
|
1149
|
-
"""Presentation timestamp for the current movie frame in seconds
|
|
1150
|
-
(`float`).
|
|
1151
|
-
|
|
1152
|
-
The value for this either comes from the decoder or some other time
|
|
1153
|
-
source. This should be synchronized to the start of the audio track. A
|
|
1154
|
-
value of `-1.0` is invalid.
|
|
1155
|
-
|
|
1156
|
-
"""
|
|
1157
|
-
if self._tStream is None:
|
|
1158
|
-
return -1.0
|
|
1159
|
-
|
|
1160
|
-
return self._lastFrame.absTime
|
|
1161
|
-
|
|
1162
|
-
def getStartAbsTime(self):
|
|
1163
|
-
"""Get the absolute experiment time in seconds the movie starts at
|
|
1164
|
-
(`float`).
|
|
1165
|
-
|
|
1166
|
-
This value reflects the time which the movie would have started if
|
|
1167
|
-
played continuously from the start. Seeking and pausing the movie causes
|
|
1168
|
-
this value to change.
|
|
1169
|
-
|
|
1170
|
-
Returns
|
|
1171
|
-
-------
|
|
1172
|
-
float
|
|
1173
|
-
Start time of the movie in absolute experiment time.
|
|
1174
|
-
|
|
1175
|
-
"""
|
|
1176
|
-
self._assertMediaPlayer()
|
|
1177
|
-
|
|
1178
|
-
return getTime() - self._lastFrame.absTime
|
|
1179
|
-
|
|
1180
|
-
def movieToAbsTime(self, movieTime):
|
|
1181
|
-
"""Convert a movie timestamp to absolute experiment timestamp.
|
|
1182
|
-
|
|
1183
|
-
Parameters
|
|
1184
|
-
----------
|
|
1185
|
-
movieTime : float
|
|
1186
|
-
Movie timestamp to convert to absolute experiment time.
|
|
1187
|
-
|
|
1188
|
-
Returns
|
|
1189
|
-
-------
|
|
1190
|
-
float
|
|
1191
|
-
Timestamp in experiment time which is coincident with the provided
|
|
1192
|
-
`movieTime` timestamp. The returned value should usually be precise
|
|
1193
|
-
down to about five decimal places.
|
|
1194
|
-
|
|
1195
|
-
"""
|
|
1196
|
-
self._assertMediaPlayer()
|
|
1197
|
-
|
|
1198
|
-
# type checks on parameters
|
|
1199
|
-
if not isinstance(movieTime, float):
|
|
1200
|
-
raise TypeError(
|
|
1201
|
-
"Value for parameter `movieTime` must have type `float` or "
|
|
1202
|
-
"`int`.")
|
|
1203
|
-
|
|
1204
|
-
return self.getStartAbsTime() + movieTime
|
|
1205
|
-
|
|
1206
|
-
def absToMovieTime(self, absTime):
|
|
1207
|
-
"""Convert absolute experiment timestamp to a movie timestamp.
|
|
1208
|
-
|
|
1209
|
-
Parameters
|
|
1210
|
-
----------
|
|
1211
|
-
absTime : float
|
|
1212
|
-
Absolute experiment time to convert to movie time.
|
|
1213
|
-
|
|
1214
|
-
Returns
|
|
1215
|
-
-------
|
|
1216
|
-
float
|
|
1217
|
-
Movie time referenced to absolute experiment time. If the value is
|
|
1218
|
-
negative then provided `absTime` happens before the beginning of the
|
|
1219
|
-
movie from the current time stamp. The returned value should usually
|
|
1220
|
-
be precise down to about five decimal places.
|
|
1221
|
-
|
|
1222
|
-
"""
|
|
1223
|
-
self._assertMediaPlayer()
|
|
1224
|
-
|
|
1225
|
-
# type checks on parameters
|
|
1226
|
-
if not isinstance(absTime, float):
|
|
1227
|
-
raise TypeError(
|
|
1228
|
-
"Value for parameter `absTime` must have type `float` or "
|
|
1229
|
-
"`int`.")
|
|
1230
|
-
|
|
1231
|
-
return absTime - self.getStartAbsTime()
|
|
1232
|
-
|
|
1233
|
-
def movieTimeFromFrameIndex(self, frameIdx):
|
|
1234
|
-
"""Get the movie time a specific a frame with a given index is
|
|
1235
|
-
scheduled to be presented.
|
|
1236
|
-
|
|
1237
|
-
This is used to handle logic for seeking through a video feed (if
|
|
1238
|
-
permitted by the player).
|
|
1239
|
-
|
|
1240
|
-
Parameters
|
|
1241
|
-
----------
|
|
1242
|
-
frameIdx : int
|
|
1243
|
-
Frame index. Negative values are accepted but they will return
|
|
1244
|
-
negative timestamps.
|
|
1245
|
-
|
|
1246
|
-
"""
|
|
1247
|
-
self._assertMediaPlayer()
|
|
1248
|
-
|
|
1249
|
-
return frameIdx * self._metadata.frameInterval
|
|
1250
|
-
|
|
1251
|
-
def frameIndexFromMovieTime(self, movieTime):
|
|
1252
|
-
"""Get the frame index of a given movie time.
|
|
1253
|
-
|
|
1254
|
-
Parameters
|
|
1255
|
-
----------
|
|
1256
|
-
movieTime : float
|
|
1257
|
-
Timestamp in movie time to convert to a frame index.
|
|
1258
|
-
|
|
1259
|
-
Returns
|
|
1260
|
-
-------
|
|
1261
|
-
int
|
|
1262
|
-
Frame index that should be presented at the specified movie time.
|
|
1263
|
-
|
|
1264
|
-
"""
|
|
1265
|
-
self._assertMediaPlayer()
|
|
1266
|
-
|
|
1267
|
-
return math.floor(movieTime / self._metadata.frameInterval)
|
|
1268
|
-
|
|
1269
|
-
@property
|
|
1270
|
-
def isSeekable(self):
|
|
1271
|
-
"""Is seeking allowed for the video stream (`bool`)? If `False` then
|
|
1272
|
-
`frameIndex` will increase monotonically.
|
|
1273
|
-
"""
|
|
1274
|
-
return False # fixed for now
|
|
1275
|
-
|
|
1276
|
-
@property
|
|
1277
|
-
def frameInterval(self):
|
|
1278
|
-
"""Duration a single frame is to be presented in seconds (`float`). This
|
|
1279
|
-
is derived from the framerate information in the metadata. If not movie
|
|
1280
|
-
is loaded, the returned value will be invalid.
|
|
1281
|
-
"""
|
|
1282
|
-
return self.metadata.frameInterval
|
|
1283
|
-
|
|
1284
|
-
@property
|
|
1285
|
-
def frameIndex(self):
|
|
1286
|
-
"""Current frame index (`int`).
|
|
1287
|
-
|
|
1288
|
-
Index of the current frame in the stream. If playing from a file or any
|
|
1289
|
-
other seekable source, this value may not increase monotonically with
|
|
1290
|
-
time. A value of `-1` is invalid, meaning either the video is not
|
|
1291
|
-
started or there is some issue with the stream.
|
|
1292
|
-
|
|
1293
|
-
"""
|
|
1294
|
-
return self._lastFrame.frameIndex
|
|
1295
|
-
|
|
1296
|
-
def getPercentageComplete(self):
|
|
1297
|
-
"""Provides a value between 0.0 and 100.0, indicating the amount of the
|
|
1298
|
-
movie that has been already played (`float`).
|
|
1299
|
-
"""
|
|
1300
|
-
duration = self.metadata.duration
|
|
1301
|
-
|
|
1302
|
-
return (self.pts / duration) * 100.0
|
|
1303
|
-
|
|
1304
|
-
# --------------------------------------------------------------------------
|
|
1305
|
-
# Methods for getting video frames from the encoder
|
|
1306
|
-
#
|
|
1307
|
-
|
|
1308
|
-
def _enqueueFrame(self):
|
|
1309
|
-
"""Grab the latest frame from the stream.
|
|
1310
|
-
|
|
1311
|
-
Returns
|
|
1312
|
-
-------
|
|
1313
|
-
bool
|
|
1314
|
-
`True` if a frame has been enqueued. Returns `False` if the camera
|
|
1315
|
-
is not ready or if the stream was closed.
|
|
1316
|
-
|
|
1317
|
-
"""
|
|
1318
|
-
self._assertMediaPlayer()
|
|
1319
|
-
|
|
1320
|
-
# If the queue is empty, the decoder thread has not yielded a new frame
|
|
1321
|
-
# since the last call.
|
|
1322
|
-
enqueuedFrame = self._tStream.getRecentFrame()
|
|
1323
|
-
if enqueuedFrame is None:
|
|
1324
|
-
return False
|
|
1325
|
-
|
|
1326
|
-
# Unpack the data we got back ...
|
|
1327
|
-
# Note - Bit messy here, we should just hold onto the `enqueuedFrame`
|
|
1328
|
-
# instance and reference its fields from properties. Keeping like this
|
|
1329
|
-
# for now.
|
|
1330
|
-
frameImage = enqueuedFrame.frameImage
|
|
1331
|
-
streamStatus = enqueuedFrame.streamStatus
|
|
1332
|
-
self._metadata = enqueuedFrame.metadata
|
|
1333
|
-
self._frameIndex = streamStatus.frameIndex
|
|
1334
|
-
self._loopCount = streamStatus.loopCount
|
|
1335
|
-
|
|
1336
|
-
# status information
|
|
1337
|
-
self._streamTime = streamStatus.streamTime # stream time for the camera
|
|
1338
|
-
|
|
1339
|
-
# if we have a new frame, update the frame information
|
|
1340
|
-
videoBuffer = frameImage.to_memoryview()[0].memview
|
|
1341
|
-
videoFrameArray = np.frombuffer(videoBuffer, dtype=np.uint8)
|
|
1342
|
-
|
|
1343
|
-
# provide the last frame
|
|
1344
|
-
self._lastFrame = MovieFrame(
|
|
1345
|
-
frameIndex=self._frameIndex,
|
|
1346
|
-
absTime=self._streamTime,
|
|
1347
|
-
displayTime=self.metadata.frameInterval,
|
|
1348
|
-
size=frameImage.get_size(),
|
|
1349
|
-
colorData=videoFrameArray,
|
|
1350
|
-
audioChannels=0, # not populated yet ...
|
|
1351
|
-
audioSamples=None,
|
|
1352
|
-
metadata=self.metadata,
|
|
1353
|
-
movieLib=u'ffpyplayer',
|
|
1354
|
-
userData=None,
|
|
1355
|
-
keepAlive=frameImage)
|
|
1356
|
-
|
|
1357
|
-
return True
|
|
1358
|
-
|
|
1359
|
-
def update(self):
|
|
1360
|
-
"""Update this player.
|
|
1361
|
-
|
|
1362
|
-
This get the latest data from the video stream and updates the player
|
|
1363
|
-
accordingly. This should be called at a higher frequency than the frame
|
|
1364
|
-
rate of the movie to avoid frame skips.
|
|
1365
|
-
|
|
1366
|
-
"""
|
|
1367
|
-
self._assertMediaPlayer()
|
|
1368
|
-
|
|
1369
|
-
# check if the stream reader thread is present and alive, if not the
|
|
1370
|
-
# movie is finished
|
|
1371
|
-
self._enqueueFrame()
|
|
1372
|
-
|
|
1373
|
-
if self._tStream.isFinished: # are we done?
|
|
1374
|
-
self._status = FINISHED
|
|
1375
|
-
|
|
1376
|
-
def getMovieFrame(self):
|
|
1377
|
-
"""Get the movie frame scheduled to be displayed at the current time.
|
|
1378
|
-
|
|
1379
|
-
Returns
|
|
1380
|
-
-------
|
|
1381
|
-
`~psychopy.visual.movies.frame.MovieFrame`
|
|
1382
|
-
Current movie frame.
|
|
1383
|
-
|
|
1384
|
-
"""
|
|
1385
|
-
self.update()
|
|
1386
|
-
|
|
1387
|
-
return self._lastFrame
|
|
1388
|
-
|
|
1389
|
-
def __del__(self):
|
|
1390
|
-
"""Cleanup when unloading.
|
|
1391
|
-
"""
|
|
1392
|
-
global _evtCleanUpMovieEvent
|
|
1393
|
-
if hasattr(self, '_tStream'):
|
|
1394
|
-
if self._tStream is not None:
|
|
1395
|
-
if not _evtCleanUpMovieEvent.is_set():
|
|
1396
|
-
self._tStream.shutdown()
|
|
1397
|
-
self._tStream.join()
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if __name__ == "__main__":
|
|
1401
|
-
pass
|