File: movie.py

package info (click to toggle)
psychopy 2020.2.10%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 44,056 kB
  • sloc: python: 119,649; javascript: 3,022; makefile: 148; sh: 125; xml: 9
file content (386 lines) | stat: -rw-r--r-- 14,067 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy.
"""

# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2020 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

from __future__ import absolute_import, division, print_function

from builtins import next
from builtins import str
import sys
import os

# Ensure setting pyglet.options['debug_gl'] to False is done prior to any
# other calls to pyglet or pyglet submodules, otherwise it may not get picked
# up by the pyglet GL engine and have no effect.
# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
import pyglet
pyglet.options['debug_gl'] = False
GL = pyglet.gl

# on windows try to load avbin now (other libs can interfere)
if sys.platform == 'win32':
    # make sure we also check in SysWOW64 if on 64-bit windows
    if 'C:\\Windows\\SysWOW64' not in os.environ['PATH']:
        os.environ['PATH'] += ';C:\\Windows\\SysWOW64'

    try:
        from pyglet.media import avbin
        haveAvbin = True
    except ImportError:
        # either avbin isn't installed or scipy.stats has been imported
        # (prevents avbin loading)
        haveAvbin = False
    except Exception as e:
        # WindowsError on some systems
        # AttributeError if using avbin5 from pyglet 1.2?
        haveAvbin = False


import psychopy  # so we can get the __path__
from psychopy import core, logging, event
import psychopy.event

# tools must only be imported *after* event or MovieStim breaks on win32
# (JWP has no idea why!)
from psychopy.tools.arraytools import val2array
from psychopy.tools.attributetools import logAttrib, setAttribute
from psychopy.visual.basevisual import BaseVisualStim, ContainerMixin
from psychopy.tools.filetools import pathToString

if sys.platform == 'win32' and not haveAvbin:
    logging.warning("avbin.dll failed to load. "
                    "Try importing psychopy.visual as the first library "
                    "(before anything that uses scipy) or use a different"
                    "movie backend (e.g. moviepy).")

import numpy
try:
    from pyglet import media
    havePygletMedia = True
except Exception:
    havePygletMedia = False

from psychopy.constants import FINISHED, NOT_STARTED, PAUSED, PLAYING, STOPPED


class MovieStim(BaseVisualStim, ContainerMixin):
    """A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy.

    **Example**::

        mov = visual.MovieStim(myWin, 'testMovie.mp4', flipVert=False)
        print(mov.duration)
        # give the original size of the movie in pixels:
        print(mov.format.width, mov.format.height)

        mov.draw()  # draw the current frame (automagically determined)

    See MovieStim.py for demo.
    """

    def __init__(self, win,
                 filename="",
                 units='pix',
                 size=None,
                 pos=(0.0, 0.0),
                 ori=0.0,
                 flipVert=False,
                 flipHoriz=False,
                 color=(1.0, 1.0, 1.0),
                 colorSpace='rgb',
                 opacity=1.0,
                 volume=1.0,
                 name=None,
                 loop=False,
                 autoLog=None,
                 depth=0.0,):
        """
        :Parameters:

            filename :
                a string giving the relative or absolute path to the movie.
                Can be any movie that AVbin can read (e.g. mpeg, DivX)
            flipVert : True or *False*
                If True then the movie will be top-bottom flipped
            flipHoriz : True or *False*
                If True then the movie will be right-left flipped
            volume :
                The nominal level is 1.0, and 0.0 is silence,
                see pyglet.media.Player
            loop : bool, optional
                Whether to start the movie over from the beginning
                if draw is called and the movie is done.

        """
        # what local vars are defined (these are the init params) for use by
        # __repr__
        self._initParams = dir()
        self._initParams.remove('self')

        super(MovieStim, self).__init__(
            win, units=units, name=name, autoLog=False)
        self._verticesBase *= numpy.array([[-1, 1]])  # unflip

        if not havePygletMedia:
            msg = ("pyglet.media is needed for MovieStim and could not be"
                   " imported.\nThis can occur for various reasons;"
                   "    - psychopy.visual was imported too late (after a lib"
                   " that uses scipy)"
                   "    - no audio output is enabled (no audio card or no "
                   "speakers attached)"
                   "    - avbin is not installed")
            raise ImportError(msg)
        self._movie = None  # the actual pyglet media object
        self._player = pyglet.media.ManagedSoundPlayer()
        self._player.volume = volume
        try:
            self._player_default_on_eos = self._player.on_eos
        except Exception:
            # pyglet 1.1.4?
            self._player_default_on_eos = self._player._on_eos

        self.filename = pathToString(filename)
        self.duration = None
        self.loop = loop
        if loop and pyglet.version >= '1.2':
            logging.error("looping of movies is not currently supported "
                          "for pyglet >= 1.2 (only for version 1.1.4)")
        self.loadMovie(self.filename)
        self.format = self._movie.video_format
        self.pos = numpy.asarray(pos, float)
        self.depth = depth
        self.flipVert = flipVert
        self.flipHoriz = flipHoriz
        self.opacity = float(opacity)
        self.status = NOT_STARTED

        # size
        if size is None:
            self.size = numpy.array([self.format.width, self.format.height],
                                    float)
        else:
            self.size = val2array(size)

        self.ori = ori
        self._updateVertices()

        if win.winType != 'pyglet':
            logging.error('Movie stimuli can only be used with a '
                          'pyglet window')
            core.quit()

        # set autoLog now that params have been initialised
        wantLog = autoLog is None and self.win.autoLog
        self.__dict__['autoLog'] = autoLog or wantLog
        if self.autoLog:
            logging.exp("Created %s = %s" % (self.name, str(self)))

    def setMovie(self, filename, log=None):
        """See `~MovieStim.loadMovie` (the functions are identical).
        This form is provided for syntactic consistency with other visual
        stimuli.
        """
        self.loadMovie(filename, log=log)

    def loadMovie(self, filename, log=None):
        """Load a movie from file

        :Parameters:

            filename: string
                The name of the file, including path if necessary

        Brings up a warning if avbin is not found on the computer.
        After the file is loaded MovieStim.duration is updated with the movie
        duration (in seconds).
        """
        filename = pathToString(filename)
        try:
            self._movie = pyglet.media.load(filename, streaming=True)
        except Exception as e:
            # pyglet.media.riff is N/A if avbin is available, and then
            # actual exception would get masked with a new one for unknown
            # (sub)module riff, thus catching any exception and tuning msg
            # up if it has to do anything with avbin
            estr = str(e)
            msg = ''
            if "avbin" in estr.lower():
                msg = ("\nIt seems that avbin was not installed correctly."
                       "\nPlease fetch/install it from "
                       "http://code.google.com/p/avbin/.")
            emsg = "Caught exception '%s' while loading file '%s'.%s"
            raise IOError(emsg % (estr, filename, msg))
        self._player.queue(self._movie)
        self.duration = self._movie.duration
        while self._player.source != self._movie:
            next(self._player)
        self.status = NOT_STARTED
        self._player.pause()  # start 'playing' on the next draw command
        self.filename = filename
        logAttrib(self, log, 'movie', filename)

    def pause(self, log=None):
        """Pause the current point in the movie (sound will stop, current
        frame will not advance). If play() is called again both will restart.
        """
        self._player.pause()
        self._player._on_eos = self._player_default_on_eos
        self.status = PAUSED
        if log or log is None and self.autoLog:
            self.win.logOnFlip("Set %s paused" % self.name,
                               level=logging.EXP, obj=self)

    def stop(self, log=None):
        """Stop the current point in the movie.

        The sound will stop, current frame will not advance. Once stopped
        the movie cannot be restarted - it must be loaded again.
        Use pause() if you may need to restart the movie.
        """
        self._player.stop()
        self._player._on_eos = self._player_default_on_eos
        self.status = STOPPED
        if log or log is None and self.autoLog:
            self.win.logOnFlip("Set %s stopped" % self.name,
                               level=logging.EXP, obj=self)

    def play(self, log=None):
        """Continue a paused movie from current position.
        """
        self._player.play()
        self._player._on_eos = self._onEos
        self.status = PLAYING
        if log or log is None and self.autoLog:
            self.win.logOnFlip("Set %s playing" % self.name,
                               level=logging.EXP, obj=self)

    def seek(self, timestamp, log=None):
        """Seek to a particular timestamp in the movie.

        NB this does not seem very robust as at version 1.62, may crash!
        """
        self._player.seek(float(timestamp))
        logAttrib(self, log, 'seek', timestamp)

    def setFlipHoriz(self, newVal=True, log=None):
        """If set to True then the movie will be flipped horizontally
        (left-to-right). Note that this is relative to the original,
        not relative to the current state.
        """
        self.flipHoriz = newVal
        logAttrib(self, log, 'flipHoriz')
        self._needVertexUpdate = True

    def setFlipVert(self, newVal=True, log=None):
        """If set to True then the movie will be flipped vertically
        (top-to-bottom). Note that this is relative to the original,
        not relative to the current state.
        """
        self.flipVert = newVal
        logAttrib(self, log, 'flipVert')
        self._needVertexUpdate = True

    def draw(self, win=None):
        """Draw the current frame to a particular visual.Window.

        Draw to the default win for this object if not specified.
        The current position in the movie will be determined automatically.

        This method should be called on every frame that the movie is
        meant to appear.
        """

        if self.status == PLAYING and not self._player.playing:
            self.status = FINISHED
        _done = bool(self.status == FINISHED)
        if self.status == NOT_STARTED or (_done and self.loop):
            self.play()
        elif _done and not self.loop:
            return

        if win is None:
            win = self.win
        self._selectWindow(win)

        # make sure that textures are on and GL_TEXTURE0 is active
        GL.glActiveTexture(GL.GL_TEXTURE0)
        GL.glEnable(GL.GL_TEXTURE_2D)
        if pyglet.version >= '1.2':
            # for pyglet 1.1.4 this was done via media.dispatch_events
            self._player.update_texture()
        frameTexture = self._player.get_texture()
        if frameTexture is None:
            return

        # sets opacity (1,1,1 = RGB placeholder)
        GL.glColor4f(1, 1, 1, self.opacity)
        GL.glPushMatrix()
        self.win.setScale('pix')
        # move to centre of stimulus and rotate
        vertsPix = self.verticesPix
        t = frameTexture.tex_coords
        array = (GL.GLfloat * 32)(
            t[0], t[1],
            vertsPix[0, 0], vertsPix[0, 1], 0.,  # vertex
            t[3], t[4],
            vertsPix[1, 0], vertsPix[1, 1], 0.,
            t[6], t[7],
            vertsPix[2, 0], vertsPix[2, 1], 0.,
            t[9], t[10],
            vertsPix[3, 0], vertsPix[3, 1], 0.,
        )

        GL.glPushAttrib(GL.GL_ENABLE_BIT)
        GL.glEnable(frameTexture.target)
        GL.glBindTexture(frameTexture.target, frameTexture.id)
        GL.glPushClientAttrib(GL.GL_CLIENT_VERTEX_ARRAY_BIT)
        # 2D texture array, 3D vertex array
        GL.glInterleavedArrays(GL.GL_T2F_V3F, 0, array)
        GL.glDrawArrays(GL.GL_QUADS, 0, 4)
        GL.glPopClientAttrib()
        GL.glPopAttrib()
        GL.glPopMatrix()

    def setContrast(self):
        """Not yet implemented for MovieStim.
        """
        pass

    def _onEos(self):
        if self.loop:
            self.loadMovie(self.filename)
            self.play()
            self.status = PLAYING
        else:
            self.status = FINISHED
            self._player._on_eos = self._player_default_on_eos
        if self.autoLog:
            self.win.logOnFlip("Set %s finished" % self.name,
                               level=logging.EXP, obj=self)

    def setAutoDraw(self, val, log=None):
        """Add or remove a stimulus from the list of stimuli that will be
        automatically drawn on each flip

        :parameters:
            - val: True/False
                True to add the stimulus to the draw list, False to remove it
        """
        if val:
            self.play(log=False)  # set to play in case stopped
        else:
            self.pause(log=False)
        # add to drawing list and update status
        setAttribute(self, 'autoDraw', val, log)

    def __del__(self):
        try:
            next(self._player)
        except Exception:
            pass