psychopy 2025.1.1__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.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {psychopy-2025.1.1.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