#!/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-2022 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
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
[docs]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),
anchor="center",
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.anchor = anchor
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)))
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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
[docs] 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()
[docs] 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)
[docs] 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