psychopy 2024.1.4__py3-none-any.whl → 2024.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/.DS_Store +0 -0
- psychopy/CHANGELOG.txt +206 -0
- psychopy/GIT_SHA +1 -0
- psychopy/VERSION +1 -0
- psychopy/__init__.py +77 -15
- psychopy/app/Resources/classic/plugin16.png +0 -0
- psychopy/app/Resources/classic/plugin16@2x.png +0 -0
- psychopy/app/Resources/dark/plugin16.png +0 -0
- psychopy/app/Resources/dark/plugin16@2x.png +0 -0
- psychopy/app/Resources/light/plugin16.png +0 -0
- psychopy/app/Resources/light/plugin16@2x.png +0 -0
- psychopy/app/__init__.py +76 -2
- psychopy/app/_psychopyApp.py +126 -101
- psychopy/app/builder/builder.py +14 -10
- psychopy/app/builder/dialogs/__init__.py +8 -8
- psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
- psychopy/app/builder/dialogs/paramCtrls.py +24 -57
- psychopy/app/builder/validators.py +2 -2
- psychopy/app/coder/codeEditorBase.py +8 -8
- psychopy/app/coder/coder.py +4 -4
- psychopy/app/connections/sendusage.py +2 -2
- psychopy/app/connections/updates.py +9 -9
- psychopy/app/dialogs.py +34 -2
- psychopy/app/idle.py +31 -0
- psychopy/app/jobs.py +21 -3
- psychopy/app/linuxconfig/__init__.py +9 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1011 -942
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
- psychopy/app/pavlovia_ui/_base.py +33 -3
- psychopy/app/pavlovia_ui/search.py +0 -1
- psychopy/app/plugin_manager/dialog.py +104 -51
- psychopy/app/plugin_manager/packages.py +5 -0
- psychopy/app/plugin_manager/plugins.py +145 -67
- psychopy/app/preferencesDlg.py +8 -8
- psychopy/app/psychopyApp.py +11 -5
- psychopy/app/ribbon.py +124 -14
- psychopy/app/runner/runner.py +6 -1
- psychopy/app/stdout/stdOutRich.py +27 -11
- psychopy/app/themes/icons.py +52 -2
- psychopy/assets/__init__.py +0 -0
- psychopy/assets/click.png +0 -0
- psychopy/assets/clicknext.png +0 -0
- psychopy/assets/next.png +0 -0
- psychopy/assets/psychopy.ico +0 -0
- psychopy/assets/psychopy.png +0 -0
- psychopy/assets/templates/__init__.py +0 -0
- psychopy/assets/touch.png +0 -0
- psychopy/assets/touchnext.png +0 -0
- psychopy/assets/window.ico +0 -0
- psychopy/changes/2023.1.0.md +9 -0
- psychopy/changes/2024.1.0.md +16 -0
- psychopy/changes/__init__.py +0 -0
- psychopy/clock.py +2 -2
- psychopy/colors.py +2 -1
- psychopy/compatibility.py +53 -1
- psychopy/contrib/.DS_Store +0 -0
- psychopy/contrib/configobj/__init__.py +10 -8
- psychopy/data/__init__.py +3 -2
- psychopy/data/base.py +5 -5
- psychopy/data/experiment.py +130 -4
- psychopy/data/routine.py +56 -0
- psychopy/data/staircase.py +2 -2
- psychopy/data/trial.py +559 -97
- psychopy/data/utils.py +56 -21
- psychopy/demos/.DS_Store +0 -0
- psychopy/demos/builder/.DS_Store +0 -0
- psychopy/demos/builder/Design Templates/.DS_Store +0 -0
- psychopy/demos/builder/Experiments/.DS_Store +0 -0
- psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
- psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
- psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
- psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
- psychopy/demos/builder/Hardware/.DS_Store +0 -0
- psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
- psychopy/demos/coder/iohub/.DS_Store +0 -0
- psychopy/demos/coder/misc/hdf5_2_csv +33 -0
- psychopy/event.py +30 -29
- psychopy/experiment/.DS_Store +0 -0
- psychopy/experiment/_experiment.py +6 -6
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/__init__.py +6 -3
- psychopy/experiment/components/_base.py +286 -131
- psychopy/experiment/components/aperture/.DS_Store +0 -0
- psychopy/experiment/components/brush/.DS_Store +0 -0
- psychopy/experiment/components/button/.DS_Store +0 -0
- psychopy/experiment/components/button/__init__.py +5 -1
- psychopy/experiment/components/buttonBox/.DS_Store +0 -0
- psychopy/experiment/components/camera/.DS_Store +0 -0
- psychopy/experiment/components/code/.DS_Store +0 -0
- psychopy/experiment/components/dots/.DS_Store +0 -0
- psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
- psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/form/__init__.py +6 -2
- psychopy/experiment/components/grating/.DS_Store +0 -0
- psychopy/experiment/components/grating/__init__.py +14 -3
- psychopy/experiment/components/image/.DS_Store +0 -0
- psychopy/experiment/components/image/__init__.py +14 -3
- psychopy/experiment/components/joyButtons/.DS_Store +0 -0
- psychopy/experiment/components/joystick/.DS_Store +0 -0
- psychopy/experiment/components/keyboard/.DS_Store +0 -0
- psychopy/experiment/components/keyboard/__init__.py +22 -10
- psychopy/experiment/components/microphone/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +59 -39
- psychopy/experiment/components/mouse/.DS_Store +0 -0
- psychopy/experiment/components/mouse/__init__.py +44 -29
- psychopy/experiment/components/movie/.DS_Store +0 -0
- psychopy/experiment/components/movie/__init__.py +1 -1
- psychopy/experiment/components/panorama/.DS_Store +0 -0
- psychopy/experiment/components/parallelOut/.DS_Store +0 -0
- psychopy/experiment/components/patch/.DS_Store +0 -0
- psychopy/experiment/components/polygon/.DS_Store +0 -0
- psychopy/experiment/components/polygon/__init__.py +26 -6
- psychopy/experiment/components/progress/.DS_Store +0 -0
- psychopy/experiment/components/ratingScale/.DS_Store +0 -0
- psychopy/experiment/components/resourceManager/.DS_Store +0 -0
- psychopy/experiment/components/roi/.DS_Store +0 -0
- psychopy/experiment/components/roi/__init__.py +5 -0
- psychopy/experiment/components/routineSettings/.DS_Store +0 -0
- psychopy/experiment/components/routineSettings/__init__.py +57 -10
- psychopy/experiment/components/serialOut/.DS_Store +0 -0
- psychopy/experiment/components/settings/.DS_Store +0 -0
- psychopy/experiment/components/settings/__init__.py +117 -42
- psychopy/experiment/components/slider/.DS_Store +0 -0
- psychopy/experiment/components/sound/.DS_Store +0 -0
- psychopy/experiment/components/sound/__init__.py +54 -19
- psychopy/experiment/components/static/.DS_Store +0 -0
- psychopy/experiment/components/static/__init__.py +1 -1
- psychopy/experiment/components/text/.DS_Store +0 -0
- psychopy/experiment/components/text/__init__.py +28 -3
- psychopy/experiment/components/textbox/.DS_Store +0 -0
- psychopy/experiment/components/textbox/__init__.py +12 -2
- psychopy/experiment/components/unknown/.DS_Store +0 -0
- psychopy/experiment/components/unknown/__init__.py +1 -2
- psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
- psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
- psychopy/experiment/components/variable/.DS_Store +0 -0
- psychopy/experiment/flow.py +11 -4
- psychopy/experiment/loops.py +85 -37
- psychopy/experiment/params.py +74 -32
- psychopy/experiment/py2js_transpiler.py +8 -1
- psychopy/experiment/routines/.DS_Store +0 -0
- psychopy/experiment/routines/_base.py +102 -22
- psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
- psychopy/experiment/routines/counterbalance/__init__.py +5 -1
- psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
- psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
- psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
- psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
- psychopy/experiment/routines/photodiodeValidator/__init__.py +6 -5
- psychopy/experiment/routines/unknown/.DS_Store +0 -0
- psychopy/gui/wxgui.py +4 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/__init__.py +1 -1
- psychopy/hardware/base.py +12 -0
- psychopy/hardware/camera/__init__.py +1 -15
- psychopy/hardware/cedrus.py +10 -11
- psychopy/hardware/crs/colorcal.py +13 -22
- psychopy/hardware/crs/optical.py +10 -20
- psychopy/hardware/emulator.py +17 -14
- psychopy/hardware/eyetracker.py +42 -118
- psychopy/hardware/gammasci.py +4 -15
- psychopy/hardware/keyboard.py +102 -10
- psychopy/hardware/listener.py +3 -0
- psychopy/hardware/microphone.py +148 -18
- psychopy/hardware/minolta.py +8 -15
- psychopy/hardware/photodiode.py +191 -16
- psychopy/hardware/photometer/__init__.py +11 -19
- psychopy/hardware/pr.py +8 -15
- psychopy/hardware/speaker.py +39 -4
- psychopy/info.py +0 -71
- psychopy/iohub/.DS_Store +0 -0
- psychopy/iohub/__init__.py +1 -1
- psychopy/iohub/client/__init__.py +30 -20
- psychopy/iohub/client/keyboard.py +24 -24
- psychopy/iohub/datastore/__init__.py +2 -2
- psychopy/iohub/datastore/util.py +2 -2
- psychopy/iohub/default_config.yaml +1 -1
- psychopy/iohub/devices/.DS_Store +0 -0
- psychopy/iohub/devices/__init__.py +112 -25
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
- psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
- psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
- psychopy/iohub/devices/eyetracker/__init__.py +46 -0
- psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
- psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
- psychopy/iohub/server.py +2 -2
- psychopy/iohub/start_iohub_process.py +3 -0
- psychopy/iohub/util/__init__.py +62 -70
- psychopy/layout.py +5 -5
- psychopy/logging.py +8 -1
- psychopy/microphone.py +10 -37
- psychopy/platform_specific/__init__.py +0 -2
- psychopy/platform_specific/darwin.py +1 -3
- psychopy/platform_specific/linux.py +31 -33
- psychopy/platform_specific/win32.py +38 -13
- psychopy/plugins/__init__.py +148 -116
- psychopy/plugins/util.py +39 -0
- psychopy/preferences/Darwin.spec +4 -2
- psychopy/preferences/FreeBSD.spec +4 -2
- psychopy/preferences/Linux.spec +4 -2
- psychopy/preferences/Windows.spec +4 -2
- psychopy/preferences/baseNoArch.spec +4 -2
- psychopy/preferences/preferences.py +47 -24
- psychopy/projects/pavlovia.py +47 -4
- psychopy/scripts/psyexpCompile.py +0 -4
- psychopy/session.py +153 -21
- psychopy/sound/__init__.py +31 -21
- psychopy/sound/_base.py +20 -3
- psychopy/sound/audioclip.py +320 -33
- psychopy/sound/backend_ptb.py +47 -58
- psychopy/sound/backend_pygame.py +1 -1
- psychopy/sound/backend_pysound.py +6 -15
- psychopy/sound/transcribe.py +53 -0
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
- psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
- psychopy/tests/data/test_session/.DS_Store +0 -0
- psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
- psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/conftest.py +2 -2
- psychopy/tests/test_app/test_speed.py +4 -1
- psychopy/tests/test_data/test_TrialHandler2.py +146 -1
- psychopy/tests/test_experiment/.DS_Store +0 -0
- psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
- psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
- psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
- psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
- psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
- psychopy/tests/test_experiment/test_py2js.py +1 -1
- psychopy/tests/test_hardware/test_keyboard.py +31 -0
- psychopy/tests/test_hardware/test_ports.py +1 -11
- psychopy/tests/test_liaison/test_Liaison.py +47 -0
- psychopy/tests/test_misc/test_core.py +5 -0
- psychopy/tests/test_session/test_Session.py +5 -1
- psychopy/tests/test_tools/test_versionchooser.py +39 -8
- psychopy/tests/test_visual/test_all_stimuli.py +0 -97
- psychopy/tests/test_visual/test_image.py +6 -5
- psychopy/tests/test_visual/test_textbox.py +36 -0
- psychopy/tests/utils.py +4 -0
- psychopy/tools/filetools.py +1 -1
- psychopy/tools/pkgtools.py +160 -137
- psychopy/tools/versionchooser.py +10 -10
- psychopy/tools/wizard.py +3 -3
- psychopy/visual/.DS_Store +0 -0
- psychopy/visual/backends/pygletbackend.py +24 -13
- psychopy/visual/basevisual.py +5 -11
- psychopy/visual/button.py +2 -14
- psychopy/visual/helpers.py +5 -5
- psychopy/visual/line.py +1 -2
- psychopy/visual/movie2.py +7 -816
- psychopy/visual/movie3.py +7 -589
- psychopy/visual/movies/__init__.py +8 -11
- psychopy/visual/movies/frame.py +5 -2
- psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
- psychopy/visual/noise.py +8 -7
- psychopy/visual/patch.py +7 -16
- psychopy/visual/radial.py +9 -7
- psychopy/visual/ratingscale.py +8 -1415
- psychopy/visual/secondorder.py +10 -9
- psychopy/visual/shape.py +7 -2
- psychopy/visual/text.py +1 -1
- psychopy/visual/textbox2/textbox2.py +28 -5
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/RECORD +307 -213
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
- psychopy/app/Resources/click.png +0 -0
- psychopy/app/Resources/next.png +0 -0
- psychopy/experiment/components/patch/__init__.py +0 -121
- psychopy/experiment/components/patch/classic/patch.png +0 -0
- psychopy/experiment/components/patch/dark/patch.png +0 -0
- psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
- psychopy/experiment/components/patch/light/patch.png +0 -0
- psychopy/experiment/components/patch/light/patch@2x.png +0 -0
- psychopy/experiment/components/ratingScale/__init__.py +0 -337
- psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
- psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
- psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
- psychopy/platform_specific/posix.py +0 -16
- psychopy/tests/test_sound/test_microphone.py +0 -217
- psychopy/tests/test_visual/test_ratingScale.py +0 -299
- /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
- /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
- /psychopy/{app/Resources → assets}/USB-C.png +0 -0
- /psychopy/{app/Resources → assets}/USB.png +0 -0
- /psychopy/{app/Resources → assets}/creditCard.png +0 -0
- /psychopy/{app/Resources → assets}/default.mp3 +0 -0
- /psychopy/{app/Resources → assets}/default.mp4 +0 -0
- /psychopy/{app/Resources → assets}/default.png +0 -0
- /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
- /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
psychopy/visual/movie2.py
CHANGED
|
@@ -1,62 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy.
|
|
6
|
-
Demo using the experimental movie2 stim to play a video file. Path of video
|
|
7
|
-
needs to updated to point to a video you have. movie2 does /not/ require
|
|
8
|
-
avbin to be installed.
|
|
9
|
-
|
|
10
|
-
Movie2 does require:
|
|
11
|
-
~~~~~~~~~~~~~~~~~~~~~
|
|
12
|
-
|
|
13
|
-
1. Python OpenCV package (so openCV libs and the cv2 python interface).
|
|
14
|
-
*. For Windows, a binary installer is available at
|
|
15
|
-
http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv
|
|
16
|
-
*. For Linux, it is available via whatever package manager you use.
|
|
17
|
-
*. For OSX, ..... ?
|
|
18
|
-
2. VLC application. Just install the standard VLC (32bit) for your OS.
|
|
19
|
-
http://www.videolan.org/vlc/index.html
|
|
20
|
-
|
|
21
|
-
To play a video, you /must/:
|
|
22
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
23
|
-
|
|
24
|
-
a. Create a visual.MovieStim2(..) instance; pretend it is called mov.
|
|
25
|
-
b. Call mov.play() when you want to start playing the video.
|
|
26
|
-
c. Call win.flip(), which will display the first frame of the video.
|
|
27
|
-
d. In the experiment loop, call mov.draw() followed by win.flip() to draw
|
|
28
|
-
the video frame again mov.draw() determines if the current frame,
|
|
29
|
-
or the next frame should be redrawn and does so accordingly. If the next
|
|
30
|
-
frame is drawn, mov.draw() will return the frame index just drawn. If
|
|
31
|
-
the same frame is drawn as before, None is returned.
|
|
32
|
-
|
|
33
|
-
This method call sequence must be followed. This should be improved (I think)
|
|
34
|
-
depending on how movie stim calls are actually made. The current movie stim
|
|
35
|
-
code doc's seem a bit mixed in message.
|
|
36
|
-
|
|
37
|
-
Current known issues:
|
|
38
|
-
~~~~~~~~~~~~~~~~~~~~~~
|
|
39
|
-
|
|
40
|
-
1. Loop functionality are known to be broken at this time.
|
|
41
|
-
2. Auto draw not implemented.
|
|
42
|
-
3. Video must have 3 color channels.
|
|
43
|
-
4. Intentional Frame dropping (to keep video playing at expected rate
|
|
44
|
-
on slow machines) is not yet implemented.
|
|
45
|
-
|
|
46
|
-
What does work so far:
|
|
47
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
48
|
-
|
|
49
|
-
1. mov.setMovie(filename) / mov.loadMovie(filename)
|
|
50
|
-
2. mov.play()
|
|
51
|
-
3. mov.pause()
|
|
52
|
-
4. mov.seek()
|
|
53
|
-
4. mov.stop()
|
|
54
|
-
5. mov.set/getVolume()
|
|
55
|
-
6. Standard BaseVisualStim, ContainerMixin methods, unless noted above.
|
|
56
|
-
|
|
57
|
-
Testing has only been done on Windows and Linux so far.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
3
|
# Part of the PsychoPy library
|
|
61
4
|
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
|
|
62
5
|
# Distributed under the terms of the GNU General Public License (GPL).
|
|
@@ -66,764 +9,12 @@ Testing has only been done on Windows and Linux so far.
|
|
|
66
9
|
# of avbin
|
|
67
10
|
|
|
68
11
|
|
|
12
|
+
from psychopy.tools.pkgtools import PluginStub
|
|
69
13
|
|
|
70
14
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
import weakref # don't create circular references with vlc classes
|
|
78
|
-
|
|
79
|
-
# Ensure setting pyglet.options['debug_gl'] to False is done prior to any
|
|
80
|
-
# other calls to pyglet or pyglet submodules, otherwise it may not get picked
|
|
81
|
-
# up by the pyglet GL engine and have no effect.
|
|
82
|
-
# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
|
|
83
|
-
import pyglet
|
|
84
|
-
pyglet.options['debug_gl'] = False
|
|
85
|
-
GL = pyglet.gl
|
|
86
|
-
|
|
87
|
-
import psychopy # so we can get the __path__
|
|
88
|
-
from psychopy import core, logging
|
|
89
|
-
|
|
90
|
-
from psychopy.tools.arraytools import val2array
|
|
91
|
-
from psychopy.tools.attributetools import logAttrib, setAttribute
|
|
92
|
-
from psychopy.tools.filetools import pathToString
|
|
93
|
-
from psychopy.visual.basevisual import BaseVisualStim, ContainerMixin
|
|
94
|
-
from psychopy.clock import Clock
|
|
95
|
-
from psychopy.constants import FINISHED, NOT_STARTED, PAUSED, PLAYING, STOPPED
|
|
96
|
-
|
|
97
|
-
import ctypes
|
|
98
|
-
import numpy
|
|
99
|
-
import cv2
|
|
100
|
-
if hasattr(cv2, 'cv'):
|
|
101
|
-
# as of version 3 these ar in cv2 not cv2.cv
|
|
102
|
-
cv2.CAP_PROP_FRAME_COUNT = cv2.cv.CV_CAP_PROP_FRAME_COUNT
|
|
103
|
-
cv2.CAP_PROP_FRAME_WIDTH = cv2.cv.CV_CAP_PROP_FRAME_WIDTH
|
|
104
|
-
cv2.CAP_PROP_FRAME_HEIGHT = cv2.cv.CV_CAP_PROP_FRAME_HEIGHT
|
|
105
|
-
cv2.CAP_PROP_FORMAT = cv2.cv.CV_CAP_PROP_FORMAT
|
|
106
|
-
cv2.CAP_PROP_FPS = cv2.cv.CV_CAP_PROP_FPS
|
|
107
|
-
cv2.CAP_PROP_POS_MSEC = cv2.cv.CV_CAP_PROP_POS_MSEC
|
|
108
|
-
cv2.CAP_PROP_POS_FRAMES = cv2.cv.CV_CAP_PROP_POS_FRAMES
|
|
109
|
-
cv2.CAP_PROP_POS_AVI_RATIO = cv2.cv.CV_CAP_PROP_POS_AVI_RATIO
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
import vlc
|
|
113
|
-
except Exception as err:
|
|
114
|
-
if sys.maxsize == 9223372036854775807:
|
|
115
|
-
bits = 64
|
|
116
|
-
else:
|
|
117
|
-
bits = 32
|
|
118
|
-
if "wrong architecture" in str(err):
|
|
119
|
-
msg = ("Failed to import vlc module for MovieStim2.\n"
|
|
120
|
-
"You're using %i-bit python. Is your VLC install the same?"
|
|
121
|
-
% bits)
|
|
122
|
-
raise OSError(msg)
|
|
123
|
-
else:
|
|
124
|
-
raise err
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# these are used internally by the MovieStim2 class but need to be kept
|
|
128
|
-
# separate to prevent circular references with vlc's event handler
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _audioEndCallback(event, movieInstanceRef):
|
|
132
|
-
movieInstanceRef()._onEos()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _audioTimeCallback(event, movieInstanceRef, streamPlayer):
|
|
136
|
-
"""
|
|
137
|
-
Called by VLC every few hundred msec providing the current audio track
|
|
138
|
-
time. This info is used to pace the display of video frames read using
|
|
139
|
-
cv2.
|
|
140
|
-
"""
|
|
141
|
-
if movieInstanceRef():
|
|
142
|
-
tm = -event.u.new_time/1000.0
|
|
143
|
-
movieInstanceRef()._audio_stream_clock.reset(tm)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _setPluginPathEnviron():
|
|
147
|
-
"""Plugins aren't in the same path as the libvlc.dylib
|
|
148
|
-
"""
|
|
149
|
-
if 'VLC_PLUGIN_PATH' in os.environ:
|
|
150
|
-
return
|
|
151
|
-
dllPath = vlc.dll._name
|
|
152
|
-
from os.path import split, join
|
|
153
|
-
# try stepping back from dll path and adding 'plugins'
|
|
154
|
-
# (2 steps on OSX, 1 on win32?)
|
|
155
|
-
nSteps = 0
|
|
156
|
-
last = dllPath
|
|
157
|
-
while nSteps < 4:
|
|
158
|
-
if last is None:
|
|
159
|
-
return 0
|
|
160
|
-
last = split(last)[0]
|
|
161
|
-
pluginPath = join(last, 'plugins')
|
|
162
|
-
if os.path.isdir(pluginPath):
|
|
163
|
-
os.environ['VLC_PLUGIN_PATH'] = pluginPath
|
|
164
|
-
return 1
|
|
165
|
-
nSteps += 1
|
|
166
|
-
# if we got here we never found a path
|
|
167
|
-
return 0
|
|
168
|
-
|
|
169
|
-
OK = _setPluginPathEnviron()
|
|
170
|
-
if not OK:
|
|
171
|
-
logging.warn("Failed to set VLC plugins path. This is only important for "
|
|
172
|
-
"MovieStim2 movies (the OpenCV backend)")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class MovieStim2(BaseVisualStim, ContainerMixin):
|
|
176
|
-
"""A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy
|
|
177
|
-
that does not require avbin. Instead it requires the cv2 python package
|
|
178
|
-
for OpenCV. The VLC media player also needs to be installed on the
|
|
179
|
-
psychopy computer. This is a lazy-imported class, therefore import using
|
|
180
|
-
full path `from psychopy.visual.movie2 import MovieStim2` when
|
|
181
|
-
inheriting from it.
|
|
182
|
-
|
|
183
|
-
**Example**::
|
|
184
|
-
|
|
185
|
-
See Movie2Stim.py for demo.
|
|
186
|
-
"""
|
|
187
|
-
|
|
188
|
-
def __init__(self, win,
|
|
189
|
-
filename="",
|
|
190
|
-
units='pix',
|
|
191
|
-
size=None,
|
|
192
|
-
pos=(0.0, 0.0),
|
|
193
|
-
anchor="center",
|
|
194
|
-
ori=0.0,
|
|
195
|
-
flipVert=False,
|
|
196
|
-
flipHoriz=False,
|
|
197
|
-
color=(1.0, 1.0, 1.0),
|
|
198
|
-
colorSpace='rgb',
|
|
199
|
-
opacity=1.0,
|
|
200
|
-
volume=1.0,
|
|
201
|
-
name='',
|
|
202
|
-
loop=False,
|
|
203
|
-
autoLog=True,
|
|
204
|
-
depth=0.0,
|
|
205
|
-
noAudio=False,
|
|
206
|
-
vframe_callback=None,
|
|
207
|
-
fps=None,
|
|
208
|
-
interpolate=True):
|
|
209
|
-
"""
|
|
210
|
-
:Parameters:
|
|
211
|
-
|
|
212
|
-
filename :
|
|
213
|
-
a string giving the relative or absolute path to the movie.
|
|
214
|
-
flipVert : True or *False*
|
|
215
|
-
If True then the movie will be top-bottom flipped
|
|
216
|
-
flipHoriz : True or *False*
|
|
217
|
-
If True then the movie will be right-left flipped
|
|
218
|
-
volume :
|
|
219
|
-
The nominal level is 100, and 0 is silence.
|
|
220
|
-
loop : bool, optional
|
|
221
|
-
Whether to start the movie over from the beginning if draw is
|
|
222
|
-
called and the movie is done.
|
|
223
|
-
|
|
224
|
-
"""
|
|
225
|
-
# what local vars are defined (these are the init params) for use
|
|
226
|
-
# by __repr__
|
|
227
|
-
self._initParams = dir()
|
|
228
|
-
self._initParams.remove('self')
|
|
229
|
-
super(MovieStim2, self).__init__(win, units=units, name=name,
|
|
230
|
-
autoLog=False)
|
|
231
|
-
# check for pyglet
|
|
232
|
-
if win.winType != 'pyglet':
|
|
233
|
-
logging.error(
|
|
234
|
-
'Movie stimuli can only be used with a pyglet window')
|
|
235
|
-
core.quit()
|
|
236
|
-
self._retracerate = win._monitorFrameRate
|
|
237
|
-
# if self._retracerate is None:
|
|
238
|
-
# self._retracerate = win.getActualFrameRate()
|
|
239
|
-
if self._retracerate is None:
|
|
240
|
-
logging.warning("FrameRate could not be supplied by psychopy; "
|
|
241
|
-
"defaulting to 60.0")
|
|
242
|
-
self._retracerate = 60.0
|
|
243
|
-
self.filename = pathToString(filename)
|
|
244
|
-
self.loop = loop
|
|
245
|
-
self.flipVert = flipVert
|
|
246
|
-
self.flipHoriz = flipHoriz
|
|
247
|
-
self.pos = numpy.asarray(pos, float)
|
|
248
|
-
self.anchor = anchor
|
|
249
|
-
self.depth = depth
|
|
250
|
-
self.opacity = float(opacity)
|
|
251
|
-
self.volume = volume
|
|
252
|
-
self._av_stream_time_offset = 0.145
|
|
253
|
-
self._no_audio = noAudio
|
|
254
|
-
self._vframe_callback = vframe_callback
|
|
255
|
-
self.interpolate = interpolate
|
|
256
|
-
|
|
257
|
-
self.useTexSubImage2D = True
|
|
258
|
-
|
|
259
|
-
self._texID = None
|
|
260
|
-
self._video_stream = cv2.VideoCapture()
|
|
261
|
-
|
|
262
|
-
self._reset()
|
|
263
|
-
self.loadMovie(self.filename)
|
|
264
|
-
self.setVolume(volume)
|
|
265
|
-
self.nDroppedFrames = 0
|
|
266
|
-
|
|
267
|
-
self.aspectRatio = self._video_width/float(self._video_height)
|
|
268
|
-
# size
|
|
269
|
-
if size is None:
|
|
270
|
-
self.size = numpy.array([self._video_width, self._video_height],
|
|
271
|
-
float)
|
|
272
|
-
elif isinstance(size, (int, float, int)):
|
|
273
|
-
# treat size as desired width, and calc a height
|
|
274
|
-
# that maintains the aspect ratio of the video.
|
|
275
|
-
self.size = numpy.array([size, size/self.aspectRatio], float)
|
|
276
|
-
else:
|
|
277
|
-
self.size = val2array(size)
|
|
278
|
-
self.ori = ori
|
|
279
|
-
self._updateVertices()
|
|
280
|
-
# set autoLog (now that params have been initialised)
|
|
281
|
-
self.autoLog = autoLog
|
|
282
|
-
if autoLog:
|
|
283
|
-
logging.exp("Created {} = {}".format(self.name, self))
|
|
284
|
-
|
|
285
|
-
def _reset(self):
|
|
286
|
-
self.duration = None
|
|
287
|
-
self.status = NOT_STARTED
|
|
288
|
-
self._numpy_frame = None
|
|
289
|
-
if self._texID is not None:
|
|
290
|
-
GL.glDeleteTextures(1, self._texID)
|
|
291
|
-
self._texID = None
|
|
292
|
-
# self._video_stream = None
|
|
293
|
-
self._total_frame_count = None
|
|
294
|
-
self._video_width = None
|
|
295
|
-
self._video_height = None
|
|
296
|
-
# TODO: Read depth from video source
|
|
297
|
-
self._video_frame_depth = 3
|
|
298
|
-
self._video_frame_rate = None
|
|
299
|
-
self._inter_frame_interval = None
|
|
300
|
-
self._prev_frame_sec = None
|
|
301
|
-
self._next_frame_sec = None
|
|
302
|
-
self._next_frame_index = None
|
|
303
|
-
self._prev_frame_index = None
|
|
304
|
-
self._video_perc_done = None
|
|
305
|
-
# self._last_video_flip_time = None
|
|
306
|
-
self._next_frame_displayed = False
|
|
307
|
-
self._video_track_clock = Clock()
|
|
308
|
-
|
|
309
|
-
self._audio_stream_clock = Clock()
|
|
310
|
-
self._vlc_instance = None
|
|
311
|
-
self._audio_stream = None
|
|
312
|
-
self._audio_stream_player = None
|
|
313
|
-
self._audio_stream_started = False
|
|
314
|
-
self._audio_stream_event_manager = None
|
|
315
|
-
|
|
316
|
-
def setMovie(self, filename, log=True):
|
|
317
|
-
"""See `~MovieStim.loadMovie` (the functions are identical).
|
|
318
|
-
|
|
319
|
-
This form is provided for syntactic consistency with other
|
|
320
|
-
visual stimuli.
|
|
321
|
-
"""
|
|
322
|
-
self.loadMovie(filename, log=log)
|
|
323
|
-
|
|
324
|
-
def loadMovie(self, filename, log=True):
|
|
325
|
-
"""Load a movie from file
|
|
326
|
-
|
|
327
|
-
:Parameters:
|
|
328
|
-
|
|
329
|
-
filename: string
|
|
330
|
-
The name of the file, including path if necessary
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
After the file is loaded MovieStim.duration is updated with the movie
|
|
334
|
-
duration (in seconds).
|
|
335
|
-
"""
|
|
336
|
-
filename = pathToString(filename)
|
|
337
|
-
self._unload()
|
|
338
|
-
self._reset()
|
|
339
|
-
if self._no_audio is False:
|
|
340
|
-
self._createAudioStream()
|
|
341
|
-
|
|
342
|
-
# Create Video Stream stuff
|
|
343
|
-
self._video_stream.open(filename)
|
|
344
|
-
vfstime = core.getTime()
|
|
345
|
-
opened = self._video_stream.isOpened()
|
|
346
|
-
if not opened and core.getTime() - vfstime < 1:
|
|
347
|
-
raise RuntimeError("Error when reading image file")
|
|
348
|
-
|
|
349
|
-
if not opened:
|
|
350
|
-
raise RuntimeError("Error when reading image file")
|
|
351
|
-
|
|
352
|
-
self._total_frame_count = self._video_stream.get(
|
|
353
|
-
cv2.CAP_PROP_FRAME_COUNT)
|
|
354
|
-
self._video_width = int(self._video_stream.get(
|
|
355
|
-
cv2.CAP_PROP_FRAME_WIDTH))
|
|
356
|
-
self._video_height = int(self._video_stream.get(
|
|
357
|
-
cv2.CAP_PROP_FRAME_HEIGHT))
|
|
358
|
-
self._format = self._video_stream.get(
|
|
359
|
-
cv2.CAP_PROP_FORMAT)
|
|
360
|
-
# TODO: Read depth from video source
|
|
361
|
-
self._video_frame_depth = 3
|
|
362
|
-
|
|
363
|
-
cv_fps = self._video_stream.get(cv2.CAP_PROP_FPS)
|
|
364
|
-
|
|
365
|
-
self._video_frame_rate = cv_fps
|
|
366
|
-
|
|
367
|
-
self._inter_frame_interval = 1.0/self._video_frame_rate
|
|
368
|
-
|
|
369
|
-
# Create a numpy array that can hold one video frame, as returned by
|
|
370
|
-
# cv2.
|
|
371
|
-
self._numpy_frame = numpy.zeros((self._video_height,
|
|
372
|
-
self._video_width,
|
|
373
|
-
self._video_frame_depth),
|
|
374
|
-
dtype=numpy.uint8)
|
|
375
|
-
self.duration = self._total_frame_count * self._inter_frame_interval
|
|
376
|
-
self.status = NOT_STARTED
|
|
377
|
-
|
|
378
|
-
self.filename = filename
|
|
379
|
-
logAttrib(self, log, 'movie', filename)
|
|
380
|
-
|
|
381
|
-
def _createAudioStream(self):
|
|
382
|
-
"""
|
|
383
|
-
Create the audio stream player for the video using pyvlc.
|
|
384
|
-
"""
|
|
385
|
-
if not os.access(self.filename, os.R_OK):
|
|
386
|
-
raise RuntimeError('Error: %s file not readable' % self.filename)
|
|
387
|
-
self._vlc_instance = vlc.Instance('--novideo')
|
|
388
|
-
try:
|
|
389
|
-
self._audio_stream = self._vlc_instance.media_new(self.filename)
|
|
390
|
-
except NameError:
|
|
391
|
-
msg = 'NameError: %s vs LibVLC %s'
|
|
392
|
-
raise ImportError(msg % (vlc.__version__,
|
|
393
|
-
vlc.libvlc_get_version()))
|
|
394
|
-
self._audio_stream_player = self._vlc_instance.media_player_new()
|
|
395
|
-
self._audio_stream_player.set_media(self._audio_stream)
|
|
396
|
-
self._audio_stream_event_manager = self._audio_stream_player.event_manager()
|
|
397
|
-
self._audio_stream_event_manager.event_attach(
|
|
398
|
-
vlc.EventType.MediaPlayerTimeChanged, _audioTimeCallback,
|
|
399
|
-
weakref.ref(self), self._audio_stream_player)
|
|
400
|
-
self._audio_stream_event_manager.event_attach(
|
|
401
|
-
vlc.EventType.MediaPlayerEndReached, _audioEndCallback,
|
|
402
|
-
weakref.ref(self))
|
|
403
|
-
|
|
404
|
-
def _releaseAudioStream(self):
|
|
405
|
-
if self._audio_stream_player:
|
|
406
|
-
self._audio_stream_player.stop()
|
|
407
|
-
|
|
408
|
-
if self._audio_stream_event_manager:
|
|
409
|
-
self._audio_stream_event_manager.event_detach(
|
|
410
|
-
vlc.EventType.MediaPlayerTimeChanged)
|
|
411
|
-
self._audio_stream_event_manager.event_detach(
|
|
412
|
-
vlc.EventType.MediaPlayerEndReached)
|
|
413
|
-
|
|
414
|
-
if self._audio_stream:
|
|
415
|
-
self._audio_stream.release()
|
|
416
|
-
|
|
417
|
-
if self._vlc_instance:
|
|
418
|
-
self._vlc_instance.release()
|
|
419
|
-
|
|
420
|
-
self._audio_stream = None
|
|
421
|
-
self._audio_stream_event_manager = None
|
|
422
|
-
self._audio_stream_player = None
|
|
423
|
-
self._vlc_instance = None
|
|
424
|
-
|
|
425
|
-
def _flipCallback(self):
|
|
426
|
-
self._next_frame_displayed = True
|
|
427
|
-
|
|
428
|
-
def play(self, log=True):
|
|
429
|
-
"""Continue a paused movie from current position.
|
|
430
|
-
"""
|
|
431
|
-
cstat = self.status
|
|
432
|
-
if cstat != PLAYING:
|
|
433
|
-
self.status = PLAYING
|
|
434
|
-
|
|
435
|
-
if self._next_frame_sec is None:
|
|
436
|
-
# movie has no current position, need to reset the clock
|
|
437
|
-
# to zero in order to have the timing logic work
|
|
438
|
-
# otherwise the video stream would skip frames until the
|
|
439
|
-
# time since creating the movie object has passed
|
|
440
|
-
self._video_track_clock.reset()
|
|
441
|
-
|
|
442
|
-
if cstat == PAUSED:
|
|
443
|
-
# toggle audio pause
|
|
444
|
-
if self._audio_stream_player:
|
|
445
|
-
self._audio_stream_player.pause()
|
|
446
|
-
self._audio_stream_clock.reset(
|
|
447
|
-
-self._audio_stream_player.get_time()/1000.0)
|
|
448
|
-
if self._next_frame_sec:
|
|
449
|
-
self._video_track_clock.reset(-self._next_frame_sec)
|
|
450
|
-
else:
|
|
451
|
-
nt = self._getNextFrame()
|
|
452
|
-
self._video_track_clock.reset(-nt)
|
|
453
|
-
|
|
454
|
-
if log and self.autoLog:
|
|
455
|
-
self.win.logOnFlip("Set %s playing" % (self.name),
|
|
456
|
-
level=logging.EXP, obj=self)
|
|
457
|
-
|
|
458
|
-
self._updateFrameTexture()
|
|
459
|
-
self.win.callOnFlip(self._flipCallback)
|
|
460
|
-
return self._next_frame_index
|
|
461
|
-
|
|
462
|
-
def pause(self, log=True):
|
|
463
|
-
"""Pause the current point in the movie (sound will stop, current
|
|
464
|
-
frame will not advance). If play() is called again both will restart.
|
|
465
|
-
"""
|
|
466
|
-
if self.status == PLAYING:
|
|
467
|
-
self.status = PAUSED
|
|
468
|
-
player = self._audio_stream_player
|
|
469
|
-
if player and player.can_pause():
|
|
470
|
-
player.pause()
|
|
471
|
-
if log and self.autoLog:
|
|
472
|
-
self.win.logOnFlip("Set %s paused" % self.name,
|
|
473
|
-
level=logging.EXP, obj=self)
|
|
474
|
-
return True
|
|
475
|
-
if log and self.autoLog:
|
|
476
|
-
self.win.logOnFlip("Failed Set %s paused" % self.name,
|
|
477
|
-
level=logging.EXP, obj=self)
|
|
478
|
-
return False
|
|
479
|
-
|
|
480
|
-
def stop(self, log=True):
|
|
481
|
-
"""Stop the current point in the movie (sound will stop,
|
|
482
|
-
current frame will not advance). Once stopped the movie cannot
|
|
483
|
-
be restarted - it must be loaded again.
|
|
484
|
-
|
|
485
|
-
Use pause() if you may need to restart the movie.
|
|
486
|
-
"""
|
|
487
|
-
if self.status != STOPPED:
|
|
488
|
-
self.status = STOPPED
|
|
489
|
-
self._unload()
|
|
490
|
-
self._reset()
|
|
491
|
-
if log and self.autoLog:
|
|
492
|
-
self.win.logOnFlip("Set %s stopped" % (self.name),
|
|
493
|
-
level=logging.EXP, obj=self)
|
|
494
|
-
|
|
495
|
-
def seek(self, timestamp, log=True):
|
|
496
|
-
"""Seek to a particular timestamp in the movie.
|
|
497
|
-
"""
|
|
498
|
-
if self.status in [PLAYING, PAUSED]:
|
|
499
|
-
if timestamp > 0.0:
|
|
500
|
-
if self.status == PLAYING:
|
|
501
|
-
self.pause()
|
|
502
|
-
player = self._audio_stream_player
|
|
503
|
-
if player and player.is_seekable():
|
|
504
|
-
player.set_time(int(timestamp * 1000.0))
|
|
505
|
-
self._audio_stream_clock.reset(-timestamp)
|
|
506
|
-
|
|
507
|
-
MSEC = cv2.CAP_PROP_POS_MSEC
|
|
508
|
-
FRAMES = cv2.CAP_PROP_POS_FRAMES
|
|
509
|
-
self._video_stream.set(MSEC, timestamp * 1000.0)
|
|
510
|
-
self._video_track_clock.reset(-timestamp)
|
|
511
|
-
self._next_frame_index = self._video_stream.get(FRAMES)
|
|
512
|
-
self._next_frame_sec = self._video_stream.get(MSEC)/1000.0
|
|
513
|
-
else:
|
|
514
|
-
self.stop()
|
|
515
|
-
self.loadMovie(self.filename)
|
|
516
|
-
if log:
|
|
517
|
-
logAttrib(self, log, 'seek', timestamp)
|
|
518
|
-
|
|
519
|
-
self.play()
|
|
520
|
-
|
|
521
|
-
def setFlipHoriz(self, newVal=True, log=True):
|
|
522
|
-
"""If set to True then the movie will be flipped horizontally
|
|
523
|
-
(left-to-right). Note that this is relative to the original,
|
|
524
|
-
not relative to the current state.
|
|
525
|
-
"""
|
|
526
|
-
self.flipHoriz = newVal
|
|
527
|
-
logAttrib(self, log, 'flipHoriz')
|
|
528
|
-
|
|
529
|
-
def setFlipVert(self, newVal=True, log=True):
|
|
530
|
-
"""If set to True then the movie will be flipped vertically
|
|
531
|
-
(top-to-bottom). Note that this is relative to the original,
|
|
532
|
-
not relative to the current state.
|
|
533
|
-
"""
|
|
534
|
-
self.flipVert = not newVal
|
|
535
|
-
logAttrib(self, log, 'flipVert')
|
|
536
|
-
|
|
537
|
-
def setVolume(self, v):
|
|
538
|
-
"""Set the audio track volume. 0 = mute, 100 = 0 dB. float values
|
|
539
|
-
between 0.0 and 1.0 are also accepted, and scaled to an int
|
|
540
|
-
between 0 and 100.
|
|
541
|
-
"""
|
|
542
|
-
if self._audio_stream_player:
|
|
543
|
-
if 0.0 <= v <= 1.0 and isinstance(v, float):
|
|
544
|
-
v = int(v * 100)
|
|
545
|
-
else:
|
|
546
|
-
v = int(v)
|
|
547
|
-
self.volume = v
|
|
548
|
-
if self._audio_stream_player:
|
|
549
|
-
self._audio_stream_player.audio_set_volume(v)
|
|
550
|
-
|
|
551
|
-
def getVolume(self):
|
|
552
|
-
"""Returns the current movie audio volume.
|
|
553
|
-
|
|
554
|
-
0 is no audio, 100 is max audio volume.
|
|
555
|
-
"""
|
|
556
|
-
if self._audio_stream_player:
|
|
557
|
-
self.volume = self._audio_stream_player.audio_get_volume()
|
|
558
|
-
return self.volume
|
|
559
|
-
|
|
560
|
-
def getFPS(self):
|
|
561
|
-
"""
|
|
562
|
-
Returns the movie frames per second playback speed.
|
|
563
|
-
"""
|
|
564
|
-
return self._video_frame_rate
|
|
565
|
-
|
|
566
|
-
def getTimeToNextFrameDraw(self):
|
|
567
|
-
"""Get the number of sec.msec remaining until the next
|
|
568
|
-
movie video frame should be drawn.
|
|
569
|
-
"""
|
|
570
|
-
try:
|
|
571
|
-
_tm = self._video_track_clock.getTime()
|
|
572
|
-
return self._next_frame_sec - 1.0/self._retracerate - _tm
|
|
573
|
-
except Exception:
|
|
574
|
-
logging.warning("MovieStim2.getTimeToNextFrameDraw failed.")
|
|
575
|
-
return 0.0
|
|
576
|
-
|
|
577
|
-
def shouldDrawVideoFrame(self):
|
|
578
|
-
"""True if the next movie frame should be drawn,
|
|
579
|
-
False if it is not yet time. See getTimeToNextFrameDraw().
|
|
580
|
-
"""
|
|
581
|
-
return self.getTimeToNextFrameDraw() <= 0.0
|
|
582
|
-
|
|
583
|
-
def getCurrentFrameNumber(self):
|
|
584
|
-
"""Get the current movie frame number.
|
|
585
|
-
The first frame number in a file is 1.
|
|
586
|
-
"""
|
|
587
|
-
return self._next_frame_index
|
|
588
|
-
|
|
589
|
-
def getCurrentFrameTime(self):
|
|
590
|
-
"""Get the time that the movie file specified the current
|
|
591
|
-
video frame as having.
|
|
592
|
-
"""
|
|
593
|
-
return self._next_frame_sec
|
|
594
|
-
|
|
595
|
-
def getPercentageComplete(self):
|
|
596
|
-
"""Provides a value between 0.0 and 100.0, indicating the
|
|
597
|
-
amount of the movie that has been already played.
|
|
598
|
-
"""
|
|
599
|
-
return self._video_perc_done
|
|
600
|
-
|
|
601
|
-
def isCurrentFrameVisible(self):
|
|
602
|
-
"""The current video frame goes through two stages;
|
|
603
|
-
the first being when the movie frame is being loaded,
|
|
604
|
-
but is not visible on the display.
|
|
605
|
-
The second is when the frame has actually been presented
|
|
606
|
-
on the display. Returns False if the frame is in the first stage,
|
|
607
|
-
True when in stage 2.
|
|
608
|
-
"""
|
|
609
|
-
return self._next_frame_displayed
|
|
610
|
-
|
|
611
|
-
def _getNextFrame(self):
|
|
612
|
-
"""get next frame info ( do not decode frame yet)
|
|
613
|
-
"""
|
|
614
|
-
while self.status == PLAYING:
|
|
615
|
-
if self._video_stream.grab():
|
|
616
|
-
self._prev_frame_index = self._next_frame_index
|
|
617
|
-
self._prev_frame_sec = self._next_frame_sec
|
|
618
|
-
self._next_frame_index = self._video_stream.get(
|
|
619
|
-
cv2.CAP_PROP_POS_FRAMES)
|
|
620
|
-
self._next_frame_sec = self._video_stream.get(
|
|
621
|
-
cv2.CAP_PROP_POS_MSEC)/1000.0
|
|
622
|
-
self._video_perc_done = self._video_stream.get(
|
|
623
|
-
cv2.CAP_PROP_POS_AVI_RATIO)
|
|
624
|
-
self._next_frame_displayed = False
|
|
625
|
-
halfInterval = self._inter_frame_interval/2.0
|
|
626
|
-
if self.getTimeToNextFrameDraw() > -halfInterval:
|
|
627
|
-
return self._next_frame_sec
|
|
628
|
-
else:
|
|
629
|
-
self.nDroppedFrames += 1
|
|
630
|
-
if self.nDroppedFrames < reportNDroppedFrames:
|
|
631
|
-
msg = "MovieStim2 dropping video frame index: %d"
|
|
632
|
-
logging.warning(msg % self._next_frame_index)
|
|
633
|
-
elif self.nDroppedFrames == reportNDroppedFrames:
|
|
634
|
-
msg = ("Multiple Movie frames have occurred - "
|
|
635
|
-
"I'll stop bothering you about them!")
|
|
636
|
-
logging.warning(msg)
|
|
637
|
-
else:
|
|
638
|
-
self._onEos()
|
|
639
|
-
break
|
|
640
|
-
|
|
641
|
-
def _updateFrameTexture(self):
|
|
642
|
-
"""Decode frame into np array and move to opengl tex.
|
|
643
|
-
"""
|
|
644
|
-
ret, self._numpy_frame = self._video_stream.retrieve()
|
|
645
|
-
if ret:
|
|
646
|
-
useSubTex = self.useTexSubImage2D
|
|
647
|
-
if self._texID is None:
|
|
648
|
-
self._texID = GL.GLuint()
|
|
649
|
-
GL.glGenTextures(1, ctypes.byref(self._texID))
|
|
650
|
-
useSubTex = False
|
|
651
|
-
|
|
652
|
-
# bind the texture in openGL
|
|
653
|
-
GL.glEnable(GL.GL_TEXTURE_2D)
|
|
654
|
-
# bind that name to the target
|
|
655
|
-
GL.glBindTexture(GL.GL_TEXTURE_2D, self._texID)
|
|
656
|
-
# don't allow a movie texture to wrap around
|
|
657
|
-
GL.glTexParameteri(
|
|
658
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP)
|
|
659
|
-
GL.glTexParameteri(
|
|
660
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP)
|
|
661
|
-
# data from PIL/numpy is packed, but default for GL is 4 bytes
|
|
662
|
-
GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1)
|
|
663
|
-
# important if using bits++ because GL_LINEAR
|
|
664
|
-
# sometimes extrapolates to pixel vals outside range
|
|
665
|
-
if self.interpolate:
|
|
666
|
-
GL.glTexParameteri(
|
|
667
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
|
|
668
|
-
GL.glTexParameteri(
|
|
669
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
|
|
670
|
-
if useSubTex is False:
|
|
671
|
-
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, pyglet.gl.GL_RGB8,
|
|
672
|
-
self._numpy_frame.shape[1],
|
|
673
|
-
self._numpy_frame.shape[0], 0,
|
|
674
|
-
GL.GL_BGR, GL.GL_UNSIGNED_BYTE,
|
|
675
|
-
self._numpy_frame.ctypes)
|
|
676
|
-
else:
|
|
677
|
-
GL.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, 0,
|
|
678
|
-
self._numpy_frame.shape[1],
|
|
679
|
-
self._numpy_frame.shape[0],
|
|
680
|
-
GL.GL_BGR, GL.GL_UNSIGNED_BYTE,
|
|
681
|
-
self._numpy_frame.ctypes)
|
|
682
|
-
else:
|
|
683
|
-
GL.glTexParameteri(
|
|
684
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
|
|
685
|
-
GL.glTexParameteri(
|
|
686
|
-
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
|
|
687
|
-
if useSubTex is False:
|
|
688
|
-
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGB8,
|
|
689
|
-
self._numpy_frame.shape[1],
|
|
690
|
-
self._numpy_frame.shape[0], 0,
|
|
691
|
-
GL.GL_BGR, GL.GL_UNSIGNED_BYTE,
|
|
692
|
-
self._numpy_frame.ctypes)
|
|
693
|
-
else:
|
|
694
|
-
GL.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, 0,
|
|
695
|
-
self._numpy_frame.shape[1],
|
|
696
|
-
self._numpy_frame.shape[0],
|
|
697
|
-
GL.GL_BGR, GL.GL_UNSIGNED_BYTE,
|
|
698
|
-
self._numpy_frame.ctypes)
|
|
699
|
-
GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE,
|
|
700
|
-
GL.GL_MODULATE) # ?? do we need this - think not!
|
|
701
|
-
else:
|
|
702
|
-
raise RuntimeError("Could not load video frame data.")
|
|
703
|
-
|
|
704
|
-
def _getVideoAudioTimeDiff(self):
|
|
705
|
-
if self._audio_stream_started is False:
|
|
706
|
-
return 0
|
|
707
|
-
return self.getCurrentFrameTime() - self._getAudioStreamTime()
|
|
708
|
-
|
|
709
|
-
def draw(self, win=None):
|
|
710
|
-
"""Draw the current frame to a particular visual.Window (or to the
|
|
711
|
-
default win for this object if not specified).
|
|
712
|
-
The current position in the movie will be determined automatically.
|
|
713
|
-
|
|
714
|
-
This method should be called on every frame that the movie is meant
|
|
715
|
-
to appear.
|
|
716
|
-
"""
|
|
717
|
-
if self.status == NOT_STARTED or (self.status == FINISHED and self.loop):
|
|
718
|
-
self.play()
|
|
719
|
-
elif self.status == FINISHED and not self.loop:
|
|
720
|
-
return
|
|
721
|
-
return_next_frame_index = False
|
|
722
|
-
if win is None:
|
|
723
|
-
win = self.win
|
|
724
|
-
self._selectWindow(win)
|
|
725
|
-
|
|
726
|
-
vtClock = self._video_track_clock
|
|
727
|
-
if (self._no_audio is False and
|
|
728
|
-
not self._audio_stream_started and
|
|
729
|
-
vtClock.getTime() >= self._av_stream_time_offset):
|
|
730
|
-
self._startAudio()
|
|
731
|
-
|
|
732
|
-
if self._next_frame_displayed:
|
|
733
|
-
if self._getVideoAudioTimeDiff() > self._inter_frame_interval:
|
|
734
|
-
vtClock.reset(-self._next_frame_sec)
|
|
735
|
-
else:
|
|
736
|
-
self._getNextFrame()
|
|
737
|
-
|
|
738
|
-
if self.shouldDrawVideoFrame() and not self._next_frame_displayed:
|
|
739
|
-
self._updateFrameTexture()
|
|
740
|
-
return_next_frame_index = True
|
|
741
|
-
|
|
742
|
-
# make sure that textures are on and GL_TEXTURE0 is active
|
|
743
|
-
GL.glActiveTexture(GL.GL_TEXTURE0)
|
|
744
|
-
GL.glEnable(GL.GL_TEXTURE_2D)
|
|
745
|
-
# sets opacity (1,1,1 = RGB placeholder)
|
|
746
|
-
GL.glColor4f(1, 1, 1, self.opacity)
|
|
747
|
-
GL.glPushMatrix()
|
|
748
|
-
self.win.setScale('pix')
|
|
749
|
-
# move to centre of stimulus and rotate
|
|
750
|
-
vertsPix = self.verticesPix
|
|
751
|
-
|
|
752
|
-
array = (GL.GLfloat * 32)(
|
|
753
|
-
1, 1, # texture coords
|
|
754
|
-
vertsPix[0, 0], vertsPix[0, 1], 0., # vertex
|
|
755
|
-
0, 1,
|
|
756
|
-
vertsPix[1, 0], vertsPix[1, 1], 0.,
|
|
757
|
-
0, 0,
|
|
758
|
-
vertsPix[2, 0], vertsPix[2, 1], 0.,
|
|
759
|
-
1, 0,
|
|
760
|
-
vertsPix[3, 0], vertsPix[3, 1], 0.,
|
|
761
|
-
)
|
|
762
|
-
GL.glPushAttrib(GL.GL_ENABLE_BIT)
|
|
763
|
-
GL.glEnable(GL.GL_TEXTURE_2D)
|
|
764
|
-
GL.glBindTexture(GL.GL_TEXTURE_2D, self._texID)
|
|
765
|
-
GL.glPushClientAttrib(GL.GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
766
|
-
# 2D texture array, 3D vertex array
|
|
767
|
-
GL.glInterleavedArrays(GL.GL_T2F_V3F, 0, array)
|
|
768
|
-
GL.glDrawArrays(GL.GL_QUADS, 0, 4)
|
|
769
|
-
GL.glPopClientAttrib()
|
|
770
|
-
GL.glPopAttrib()
|
|
771
|
-
GL.glPopMatrix()
|
|
772
|
-
# GL.glActiveTexture(0)
|
|
773
|
-
# GL.glDisable(GL.GL_TEXTURE_2D)
|
|
774
|
-
if return_next_frame_index:
|
|
775
|
-
self.win.callOnFlip(self._flipCallback)
|
|
776
|
-
return self._next_frame_index
|
|
777
|
-
|
|
778
|
-
def setContrast(self):
|
|
779
|
-
"""Not yet implemented for MovieStim
|
|
780
|
-
"""
|
|
781
|
-
pass
|
|
782
|
-
|
|
783
|
-
def _startAudio(self):
|
|
784
|
-
"""Start the audio playback stream.
|
|
785
|
-
"""
|
|
786
|
-
if self._audio_stream_player:
|
|
787
|
-
self._audio_stream_started = True
|
|
788
|
-
self._audio_stream_player.play()
|
|
789
|
-
_tm = -self._audio_stream_player.get_time()
|
|
790
|
-
self._audio_stream_clock.reset(_tm/1000.0)
|
|
791
|
-
|
|
792
|
-
def _getAudioStreamTime(self):
|
|
793
|
-
return self._audio_stream_clock.getTime()
|
|
794
|
-
|
|
795
|
-
def _unload(self):
|
|
796
|
-
# if self._video_stream:
|
|
797
|
-
self._video_stream.release()
|
|
798
|
-
# self._video_stream = None
|
|
799
|
-
self._numpy_frame = None
|
|
800
|
-
self._releaseAudioStream()
|
|
801
|
-
self.status = FINISHED
|
|
802
|
-
|
|
803
|
-
def _onEos(self):
|
|
804
|
-
if self.loop:
|
|
805
|
-
self.seek(0.0)
|
|
806
|
-
else:
|
|
807
|
-
self.status = FINISHED
|
|
808
|
-
self.stop()
|
|
809
|
-
if self.autoLog:
|
|
810
|
-
self.win.logOnFlip("Set %s finished" % self.name,
|
|
811
|
-
level=logging.EXP, obj=self)
|
|
812
|
-
|
|
813
|
-
def __del__(self):
|
|
814
|
-
self._unload()
|
|
815
|
-
|
|
816
|
-
def setAutoDraw(self, val, log=None):
|
|
817
|
-
"""Add or remove a stimulus from the list of stimuli that will be
|
|
818
|
-
automatically drawn on each flip
|
|
819
|
-
|
|
820
|
-
:parameters:
|
|
821
|
-
- val: True/False
|
|
822
|
-
True to add the stimulus to the draw list, False to remove it
|
|
823
|
-
"""
|
|
824
|
-
if val:
|
|
825
|
-
self.play(log=False) # set to play in case stopped
|
|
826
|
-
else:
|
|
827
|
-
self.pause(log=False)
|
|
828
|
-
# add to drawing list and update status
|
|
829
|
-
setAttribute(self, 'autoDraw', val, log)
|
|
15
|
+
class MovieStim2(
|
|
16
|
+
PluginStub,
|
|
17
|
+
plugin="psychopy-legacy",
|
|
18
|
+
doclink="https://psychopy.github.io/psychopy-legacy/coder/visual/MovieStim2"
|
|
19
|
+
):
|
|
20
|
+
pass
|