#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy using a
local installation of VLC media player (https://www.videolan.org/).
"""
# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
#
# VlcMovieStim originally contributed by Dan Fitch, April 2019. The `MovieStim2`
# class was taken and rewritten to use only VLC.
#
import os
import sys
import threading
import ctypes
import weakref
from psychopy import core, logging
from psychopy.tools.attributetools import logAttrib, setAttribute
from psychopy.tools.filetools import pathToString
from psychopy.visual.basevisual import BaseVisualStim, ContainerMixin
from psychopy.constants import FINISHED, NOT_STARTED, PAUSED, PLAYING, STOPPED
import numpy
import pyglet
pyglet.options['debug_gl'] = False
GL = pyglet.gl
try:
# check if the lib can be loaded
import vlc
haveVLC = True
except Exception as err:
haveVLC = False
# store the error but only raise it if the
if "wrong architecture" in str(err):
msg = ("Failed to import `vlc` module required by `vlcmoviestim`.\n"
"You're using %i-bit python. Is your VLC install the same?"
% 64 if sys.maxsize == 2 ** 64 else 32)
_vlcImportErr = OSError(msg)
else:
_vlcImportErr = err
# flip time, and time since last movie frame flip will be printed
reportNDroppedFrames = 10
[docs]class VlcMovieStim(BaseVisualStim, ContainerMixin):
"""A stimulus class for playing movies in various formats (mpeg, avi,
etc...) in PsychoPy using the VLC media player as a decoder. This is
a lazy-imported class, therefore import using full path
`from psychopy.visual.vlcmoviestim import VlcMovieStim` when inheriting
from it.
This movie class is very efficient and better suited for playing
high-resolution videos (720p+) than the other movie classes. However, audio
is only played using the default output device. This may be adequate for
most applications where the user is not concerned about precision audio
onset times.
The VLC media player (https://www.videolan.org/) must be installed on the
machine running PsychoPy to use this class. Make certain that the version
of VLC installed matches the architecture of the Python interpreter hosting
PsychoPy.
Parameters
----------
win : :class:`~psychopy.visual.Window`
Window the video is being drawn to.
filename : str
Name of the file or stream URL to play. If an empty string, no file will
be loaded on initialization but can be set later.
units : str
Units to use when sizing the video frame on the window, affects how
`size` is interpreted.
size : ArrayLike or None
Size of the video frame on the window in `units`. If `None`, the native
size of the video will be used.
flipVert : bool
If `True` then the movie will be top-bottom flipped.
flipHoriz : bool
If `True` then the movie will be right-left flipped.
volume : int or float
If specifying an `int` the nominal level is 100, and 0 is silence. If a
`float`, values between 0 and 1 may be used.
loop : bool
Whether to start the movie over from the beginning if draw is called and
the movie is done. Default is `False.
autoStart : bool
Automatically begin playback of the video when `flip()` is called.
Notes
-----
* You may see error messages in your log output from VLC (e.g.,
`get_buffer() failed`, `no frame!`, etc.) after shutting down. These
errors originate from the decoder and can be safely ignored.
"""
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), # remove?
colorSpace='rgb',
opacity=1.0,
volume=1.0,
name='',
loop=False,
autoLog=True,
depth=0.0,
noAudio=False,
interpolate=True,
autoStart=True):
# check if we have the VLC lib
if not haveVLC:
raise _vlcImportErr
# what local vars are defined (these are the init params) for use
# by __repr__
self._initParams = dir()
self._initParams.remove('self')
super(VlcMovieStim, self).__init__(win, units=units, name=name,
autoLog=False)
# check for pyglet
if win.winType != 'pyglet':
logging.error('Movie stimuli can only be used with a pyglet window')
core.quit()
# drawing stuff
self.flipVert = flipVert
self.flipHoriz = flipHoriz
self.pos = numpy.asarray(pos, float)
# original size to keep BaseVisualStim happy
self._origSize = numpy.asarray((128, 128,), float)
# Defer setting size until after the video is loaded to use it's native
# size instead of the one set by the user.
self._useFrameSizeFromVideo = size is None
if not self._useFrameSizeFromVideo:
self.size = numpy.asarray(size, float)
self.depth = depth
self.opacity = float(opacity)
# playback stuff
self._filename = pathToString(filename)
self._volume = volume
self._noAudio = noAudio # cannot be changed
self._currentFrame = -1
self._loopCount = 0
self.loop = loop
# video pixel and texture buffer variables, setup later
self.interpolate = interpolate # use setter
self._textureId = GL.GLuint()
self._pixbuffId = GL.GLuint()
# VLC related attributes
self._instance = None
self._player = None
self._manager = None
self._stream = None
self._videoWidth = 0
self._videoHeight = 0
self._frameRate = 0.0
self._vlcInitialized = False
self._pixelLock = threading.Lock() # semaphore for VLC pixel transfer
self._framePixelBuffer = None # buffer pointer to draw to
self._videoFrameBufferSize = None
self._streamEnded = False
# spawn a VLC instance for this class instance
# self._createVLCInstance()
# load a movie if provided
if self._filename:
self.loadMovie(self._filename)
self.setVolume(volume)
self.nDroppedFrames = 0
self._autoStart = autoStart
self.ori = ori
# set autoLog (now that params have been initialised)
self.autoLog = autoLog
if autoLog:
logging.exp("Created {} = {}".format(self.name, self))
@property
def filename(self):
"""File name for the loaded video (`str`)."""
return self._filename
@filename.setter
def filename(self, value):
self.loadMovie(value)
@property
def autoStart(self):
"""Start playback when `.draw()` is called (`bool`)."""
return self._autoStart
@autoStart.setter
def autoStart(self, value):
self._autoStart = bool(value)
[docs] def setMovie(self, filename, log=True):
"""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=True):
"""Load a movie from file
Parameters
----------
filename : str
The name of the file or URL, including path if necessary.
log : bool
Log this event.
Notes
-----
* Due to VLC oddness, `.duration` is not correct until the movie starts
playing.
"""
self._filename = pathToString(filename)
# open the media using a new player
self._openMedia()
self.status = NOT_STARTED
logAttrib(self, log, 'movie', filename)
[docs] def _createVLCInstance(self):
"""Internal method to create a new VLC instance.
Raises an error if an instance is already spawned and hasn't been
released.
"""
logging.debug("Spawning new VLC instance ...")
# Creating VLC instances is slow, so we want to create once per class
# instantiation and reuse it as much as possible. Stopping and starting
# instances too frequently can result in an errors and affect the
# stability of the system.
if self._instance is not None:
self._releaseVLCInstance()
# errmsg = ("Attempted to create another VLC instance without "
# "releasing the previous one first!")
# logging.fatal(errmsg, obj=self)
# logging.flush()
# raise RuntimeError(errmsg)
# Using "--quiet" here is just sweeping anything VLC pukes out under the
# rug. Most of the time the errors only look scary but can be ignored.
params = " ".join(
["--no-audio" if self._noAudio else "", "--sout-keep", "--quiet"])
self._instance = vlc.Instance(params)
# used to capture log messages from VLC
self._instance.log_set(vlcLogCallback, None)
# create a new player object, reusable by by just changing the stream
self._player = self._instance.media_player_new()
# setup the event manager
self._manager = self._player.event_manager()
# self._manager.event_attach(
# vlc.EventType.MediaPlayerTimeChanged, vlcMediaEventCallback,
# weakref.ref(self), self._player)
self._manager.event_attach(
vlc.EventType.MediaPlayerEndReached, vlcMediaEventCallback,
weakref.ref(self), self._player)
self._vlcInitialized = True
logging.debug("VLC instance created.")
[docs] def _releaseVLCInstance(self):
"""Internal method to release a VLC instance. Calling this implicitly
stops and releases any stream presently loaded and playing.
"""
self._vlcInitialized = False
if self._player is not None:
self._player.stop()
# shutdown the manager
self._manager.event_detach(vlc.EventType.MediaPlayerEndReached)
self._manager = None
# Doing this here since I figured Python wasn't shutting down due to
# callbacks remaining bound. Seems to actually fix the problem.
self._player.video_set_callbacks(None, None, None, None)
self._player.set_media(None)
self._player.release()
self._player = None
# reset video information
self._filename = None
self._videoWidth = self._videoHeight = 0
self._frameCounter = self._loopCount = 0
self._frameRate = 0.0
self._framePixelBuffer = None
if self._stream is not None:
self._stream.release()
self._streamEnded = False
self._stream = None
if self._instance is not None:
self._instance.release()
self._instance = None
self.status = STOPPED
[docs] def _onEos(self):
"""Internal method called when the decoder encounters the end of the
stream.
"""
# Do not call the libvlc API in this method, causes a deadlock which
# freezes PsychoPy. Just set the flag below and the stream will restart
# on the next `.draw()` call.
self._streamEnded = True
if not self.loop:
self.status = FINISHED
if self.autoLog:
self.win.logOnFlip(
"Set %s finished" % self.name, level=logging.EXP, obj=self)
[docs] def _freeBuffers(self):
"""Free texture and pixel buffers. Call this when tearing down this
class or if a movie is stopped.
"""
try:
# delete buffers and textures if previously created
if self._pixbuffId.value > 0:
GL.glDeleteBuffers(1, self._pixbuffId)
self._pixbuffId = GL.GLuint()
# delete the old texture if present
if self._textureId.value > 0:
GL.glDeleteTextures(1, self._textureId)
self._textureId = GL.GLuint()
except TypeError: # can happen when unloading or shutting down
pass
[docs] def _setupTextureBuffers(self):
"""Setup texture buffers which hold frame data. This creates a 2D
RGB texture and pixel buffer. The pixel buffer serves as the store for
texture color data. Each frame, the pixel buffer memory is mapped and
frame data is copied over to the GPU from the decoder.
This is called every time a video file is loaded. The `_freeBuffers`
method is called in this routine prior to creating new buffers, so it's
safe to call this right after loading a new movie without having to
`_freeBuffers` first.
"""
self._freeBuffers() # clean up any buffers previously allocated
# Calculate the total size of the pixel store in bytes needed to hold a
# single video frame. This value is reused during the pixel upload
# process. Assumes RGBA color format.
self._videoFrameBufferSize = \
self._videoWidth * self._videoHeight * 4 * ctypes.sizeof(GL.GLubyte)
# Create the pixel buffer object which will serve as the texture memory
# store. Pixel data will be copied to this buffer each frame.
GL.glGenBuffers(1, ctypes.byref(self._pixbuffId))
GL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER, self._pixbuffId)
GL.glBufferData(
GL.GL_PIXEL_UNPACK_BUFFER,
self._videoFrameBufferSize,
None,
GL.GL_STREAM_DRAW) # one-way app -> GL
GL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER, 0)
# Create a texture which will hold the data streamed to the pixel
# buffer. Only one texture needs to be allocated.
GL.glGenTextures(1, ctypes.byref(self._textureId))
GL.glBindTexture(GL.GL_TEXTURE_2D, self._textureId)
GL.glTexImage2D(
GL.GL_TEXTURE_2D,
0,
GL.GL_RGBA8,
self._videoWidth, self._videoHeight, # frame dims in pixels
0,
GL.GL_RGBA,
GL.GL_UNSIGNED_BYTE,
None)
GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1) # needs to be 1
# setup texture filtering
if self.interpolate:
texFilter = GL.GL_LINEAR
else:
texFilter = GL.GL_NEAREST
GL.glTexParameteri(
GL.GL_TEXTURE_2D,
GL.GL_TEXTURE_MAG_FILTER,
texFilter)
GL.glTexParameteri(
GL.GL_TEXTURE_2D,
GL.GL_TEXTURE_MIN_FILTER,
texFilter)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glFlush() # make sure all buffers are ready
@property
def frameTexture(self):
"""Texture ID for the current video frame (`GLuint`). You can use this
as a video texture. However, you must periodically call `updateTexture`
to keep this up to date.
"""
return self._textureId
[docs] def _pixelTransfer(self):
"""Internal method which maps the pixel buffer for the video texture
to client memory, allowing for VLC to directly draw a video frame to it.
This method is not thread-safe and should never be called without the
pixel lock semaphore being first set by VLC.
"""
# bind pixel unpack buffer
GL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER, self._pixbuffId)
# Free last storage buffer before mapping and writing new frame data.
# This allows the GPU to process the extant buffer in VRAM uploaded last
# cycle without being stalled by the CPU accessing it. Also allows VLC
# to access the buffer while the previous one is still in use.
GL.glBufferData(
GL.GL_PIXEL_UNPACK_BUFFER,
self._videoFrameBufferSize,
None,
GL.GL_STREAM_DRAW)
# Map the buffer to client memory, `GL_WRITE_ONLY` to tell the driver to
# optimize for a one-way write operation if it can.
bufferPtr = GL.glMapBuffer(
GL.GL_PIXEL_UNPACK_BUFFER,
GL.GL_WRITE_ONLY)
# comment to disable direct VLC -> GPU frame write
# self._framePixelBuffer = bufferPtr
# uncomment if not using direct GPU write, might provide better thread
# safety ...
ctypes.memmove(
bufferPtr,
ctypes.byref(self._framePixelBuffer),
ctypes.sizeof(self._framePixelBuffer))
# Very important that we unmap the buffer data after copying, but
# keep the buffer bound for setting the texture.
GL.glUnmapBuffer(GL.GL_PIXEL_UNPACK_BUFFER)
# bind the texture in OpenGL
GL.glEnable(GL.GL_TEXTURE_2D)
# copy the PBO to the texture
GL.glBindTexture(GL.GL_TEXTURE_2D, self._textureId)
GL.glTexSubImage2D(
GL.GL_TEXTURE_2D, 0, 0, 0,
self._videoWidth,
self._videoHeight,
GL.GL_RGBA,
GL.GL_UNSIGNED_BYTE,
0) # point to the presently bound buffer
# update texture filtering only if needed
if self._texFilterNeedsUpdate:
if self.interpolate:
texFilter = GL.GL_LINEAR
else:
texFilter = GL.GL_NEAREST
GL.glTexParameteri(
GL.GL_TEXTURE_2D,
GL.GL_TEXTURE_MAG_FILTER,
texFilter)
GL.glTexParameteri(
GL.GL_TEXTURE_2D,
GL.GL_TEXTURE_MIN_FILTER,
texFilter)
self._texFilterNeedsUpdate = False
# important to unbind the PBO
GL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER, 0)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glDisable(GL.GL_TEXTURE_2D)
[docs] def updateTexture(self):
"""Update the video texture buffer to the most recent video frame.
"""
with self._pixelLock:
self._pixelTransfer()
# --------------------------------------------------------------------------
# Video playback controls and status
#
@property
def isPlaying(self):
"""`True` if the video is presently playing (`bool`)."""
# Status flags as properties are pretty useful for users since they are
# self documenting and prevent the user from touching the status flag
# attribute directly.
#
return self.status == PLAYING
@property
def isNotStarted(self):
"""`True` if the video has not be started yet (`bool`). This status is
given after a video is loaded and play has yet to be called."""
return self.status == NOT_STARTED
@property
def isStopped(self):
"""`True` if the video is stopped (`bool`)."""
return self.status == STOPPED
@property
def isPaused(self):
"""`True` if the video is presently paused (`bool`)."""
return self.status == PAUSED
@property
def isFinished(self):
"""`True` if the video is finished (`bool`)."""
# why is this the same as STOPPED?
return self.status == FINISHED
[docs] def play(self, log=True):
"""Start or continue a paused movie from current position.
Parameters
----------
log : bool
Log the play event.
Returns
-------
int or None
Frame index playback started at. Should always be `0` if starting at
the beginning of the video. Returns `None` if the player has not
been initialized.
"""
if self.isNotStarted: # video has not been played yet
if log and self.autoLog:
self.win.logOnFlip(
"Set %s playing" % self.name,
level=logging.EXP,
obj=self)
self._player.play()
elif self.isPaused:
self.win.logOnFlip(
"Resuming playback at position {:.4f}".format(
self._player.get_time() / 1000.0),
level=logging.EXP,
obj=self)
if self._player.will_play():
self._player.play()
self.status = PLAYING
return self._currentFrame
[docs] def pause(self, log=True):
"""Pause the current point in the movie.
Parameters
----------
log : bool
Log the pause event.
"""
if self.isPlaying:
self.status = PAUSED
player = self._player
if player and player.can_pause():
player.pause()
if log and self.autoLog:
self.win.logOnFlip("Set %s paused" % self.name,
level=logging.EXP, obj=self)
return True
if log and self.autoLog:
self.win.logOnFlip("Failed Set %s paused" % self.name,
level=logging.EXP, obj=self)
return False
[docs] def stop(self, log=True):
"""Stop the current point in the movie (sound will stop, current frame
will not advance). Once stopped the movie cannot be restarted - it must
be loaded again.
Use `pause()` instead if you may need to restart the movie.
Parameters
----------
log : bool
Log the stop event.
"""
if self._player is None:
return
self.status = STOPPED
if log and self.autoLog:
self.win.logOnFlip(
"Set %s stopped" % self.name, level=logging.EXP, obj=self)
self._releaseVLCInstance()
[docs] def seek(self, timestamp, log=True):
"""Seek to a particular timestamp in the movie.
Parameters
----------
timestamp : float
Time in seconds.
log : bool
Log the seek event.
"""
if self.isPlaying or self.isPaused:
player = self._player
if player and player.is_seekable():
# pause while seeking
player.set_time(int(timestamp * 1000.0))
if log:
logAttrib(self, log, 'seek', timestamp)
[docs] def rewind(self, seconds=5):
"""Rewind the video.
Parameters
----------
seconds : float
Time in seconds to rewind from the current position. Default is 5
seconds.
Returns
-------
float
Timestamp after rewinding the video.
"""
self.seek(max(self.getCurrentFrameTime() - float(seconds), 0.0))
# after seeking
return self.getCurrentFrameTime()
[docs] def fastForward(self, seconds=5):
"""Fast-forward the video.
Parameters
----------
seconds : float
Time in seconds to fast forward from the current position. Default
is 5 seconds.
Returns
-------
float
Timestamp at new position after fast forwarding the video.
"""
self.seek(
min(self.getCurrentFrameTime() + float(seconds), self.duration))
return self.getCurrentFrameTime()
[docs] def replay(self, autoPlay=True):
"""Replay the movie from the beginning.
Parameters
----------
autoPlay : bool
Start playback immediately. If `False`, you must call `play()`
afterwards to initiate playback.
Notes
-----
* This tears down the current VLC instance and creates a new one.
Similar to calling `stop()` and `loadMovie()`. Use `seek(0.0)` if you
would like to restart the movie without reloading.
"""
lastMovieFile = self._filename
self.stop()
self.loadMovie(lastMovieFile)
if autoPlay:
self.play()
# --------------------------------------------------------------------------
# Volume controls
#
@property
def volume(self):
"""Audio track volume (`int` or `float`). See `setVolume` for more
information about valid values.
"""
return self.getVolume()
@volume.setter
def volume(self, value):
self.setVolume(value)
[docs] def setVolume(self, volume):
"""Set the audio track volume.
Parameters
----------
volume : int or float
Volume level to set. 0 = mute, 100 = 0 dB. float values between 0.0
and 1.0 are also accepted, and scaled to an int between 0 and 100.
"""
if self._player:
if 0.0 <= volume <= 1.0 and isinstance(volume, float):
v = int(volume * 100)
else:
v = int(volume)
self._volume = v
if self._player:
self._player.audio_set_volume(v)
[docs] def getVolume(self):
"""Returns the current movie audio volume.
Returns
-------
int
Volume level, 0 is no audio, 100 is max audio volume.
"""
if self._player:
self._volume = self._player.audio_get_volume()
return self._volume
[docs] def increaseVolume(self, amount=10):
"""Increase the volume.
Parameters
----------
amount : int
Increase the volume by this amount (percent). This gets added to the
present volume level. If the value of `amount` and the current
volume is outside the valid range of 0 to 100, the value will be
clipped. The default value is 10 (or 10% increase).
Returns
-------
int
Volume after changed.
See also
--------
getVolume
setVolume
decreaseVolume
Examples
--------
Adjust the volume of the current video using key presses::
# assume `mov` is an instance of this class defined previously
for key in event.getKeys():
if key == 'minus':
mov.decreaseVolume()
elif key == 'equals':
mov.increaseVolume()
"""
if not self._player:
return 0
self.setVolume(min(max(self.getVolume() + int(amount), 0), 100))
return self._volume
[docs] def decreaseVolume(self, amount=10):
"""Decrease the volume.
Parameters
----------
amount : int
Decrease the volume by this amount (percent). This gets subtracted
from the present volume level. If the value of `amount` and the
current volume is outside the valid range of 0 to 100, the value
will be clipped. The default value is 10 (or 10% decrease).
Returns
-------
int
Volume after changed.
See also
--------
getVolume
setVolume
increaseVolume
Examples
--------
Adjust the volume of the current video using key presses::
# assume `mov` is an instance of this class defined previously
for key in event.getKeys():
if key == 'minus':
mov.decreaseVolume()
elif key == 'equals':
mov.increaseVolume()
"""
if not self._player:
return 0
self.setVolume(min(max(self.getVolume() - int(amount), 0), 100))
return self._volume
# --------------------------------------------------------------------------
# Video and playback information
#
@property
def frameIndex(self):
"""Current frame index being displayed (`int`)."""
return self._currentFrame
@property
def percentageComplete(self):
"""Percentage of the video completed (`float`)."""
return self.getPercentageComplete()
@property
def duration(self):
"""Duration of the loaded video in seconds (`float`). Not valid unless
the video has been started.
"""
if not self.isNotStarted:
return self._player.get_length()
return 0.0
@property
def loopCount(self):
"""Number of loops completed since playback started (`int`). This value
is reset when either `stop` or `loadMovie` is called.
"""
return self._loopCount
@property
def fps(self):
"""Movie frames per second (`float`)."""
return self.getFPS()
[docs] def getFPS(self):
"""Movie frames per second.
Returns
-------
float
Nominal number of frames to be displayed per second.
"""
return self._frameRate
@property
def frameTime(self):
"""Current frame time in seconds (`float`)."""
return self.getCurrentFrameTime()
[docs] def getCurrentFrameTime(self):
"""Get the time that the movie file specified the current video frame as
having.
Returns
-------
float
Current video time in seconds.
"""
if not self._player:
return 0.0
return self._player.get_time() / 1000.0
[docs] def getPercentageComplete(self):
"""Provides a value between 0.0 and 100.0, indicating the amount of the
movie that has been already played.
"""
return self._player.get_position() * 100.0
@property
def videoSize(self):
"""Size of the video `(w, h)` in pixels (`tuple`). Returns `(0, 0)` if
no video is loaded."""
if self._stream is not None:
return self._videoWidth, self._videoHeight
return 0, 0
# --------------------------------------------------------------------------
# Drawing methods and properties
#
@property
def interpolate(self):
"""Enable linear interpolation (`bool').
If `True` linear filtering will be applied to the video making the image
less pixelated if scaled. You may leave this off if the native size of
the video is used.
"""
return self._interpolate
@interpolate.setter
def interpolate(self, value):
self._interpolate = value
self._texFilterNeedsUpdate = True
[docs] def setFlipHoriz(self, newVal=True, log=True):
"""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')
[docs] def setFlipVert(self, newVal=True, log=True):
"""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 = not newVal
logAttrib(self, log, 'flipVert')
[docs] def _drawRectangle(self):
"""Draw the frame to the window. This is called by the `draw()` method.
"""
# make sure that textures are on and GL_TEXTURE0 is activ
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glActiveTexture(GL.GL_TEXTURE0)
# 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
array = (GL.GLfloat * 32)(
1, 1, # texture coords
vertsPix[0, 0], vertsPix[0, 1], 0., # vertex
0, 1,
vertsPix[1, 0], vertsPix[1, 1], 0.,
0, 0,
vertsPix[2, 0], vertsPix[2, 1], 0.,
1, 0,
vertsPix[3, 0], vertsPix[3, 1], 0.,
)
GL.glPushAttrib(GL.GL_ENABLE_BIT)
GL.glBindTexture(GL.GL_TEXTURE_2D, self._textureId)
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()
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glEnable(GL.GL_TEXTURE_2D)
[docs] def draw(self, win=None):
"""Draw the current frame to a particular
:class:`~psychopy.visual.Window` (or 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.
Parameters
----------
win : :class:`~psychopy.visual.Window` or `None`
Window the video is being drawn to. If `None`, the window specified
at initialization will be used instead.
Returns
-------
bool
`True` if the frame was updated this draw call.
"""
if self.isNotStarted and self._autoStart:
self.play()
elif self._streamEnded and self.loop:
self._loopCount += 1
self._streamEnded = False
self.replay()
elif self.isFinished:
self.stop()
return False
self._selectWindow(self.win if win is None else win)
# check if we need to pull a new frame this round
if self._currentFrame == self._frameCounter:
# update the texture, getting the most recent frame
self.updateTexture()
# draw the frame to the window
self._drawRectangle()
return False # frame does not need an update this round
# Below not called if the frame hasn't advanced yet ------
# update the current frame
self._currentFrame = self._frameCounter
# update the texture, getting the most recent frame
self.updateTexture()
# draw the frame to the window
self._drawRectangle()
# token gesture for existing code, we handle this logic internally now
return True
[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:
if hasattr(self, '_player'):
# false if crashed before creating instance
self._releaseVLCInstance()
except (ImportError, ModuleNotFoundError, TypeError):
pass # has probably been garbage-collected already
# ------------------------------------------------------------------------------
# Callback functions for `libvlc`
#
# WARNING: Due to a limitation of libvlc, you cannot call the API from within
# the callbacks below. Doing so will result in a deadlock that stalls the
# application. This applies to any method in the `VloMovieStim` class being
# called within a callback too. They cannot have any libvlc calls anywhere in
# the call stack.
#
@vlc.CallbackDecorators.VideoLockCb
def vlcLockCallback(user_data, planes):
"""Callback invoked when VLC has new texture data.
"""
# Need to catch errors caused if a NULL object is passed. Not sure why this
# happens but it might be due to the callback being invoked before the
# movie stim class is fully realized. This may happen since the VLC library
# operates in its own thread and may be processing frames before we are
# ready to use them. This `try-except` structure is present in all callback
# functions here that pass `user_data` which is a C pointer to the stim
# object. Right now, these functions just return when we get a NULL object.
#
try:
cls = ctypes.cast(
user_data, ctypes.POINTER(ctypes.py_object)).contents.value
except (ValueError, TypeError):
return
cls._pixelLock.acquire()
# tell VLC to take the data and stuff it into the buffer
planes[0] = ctypes.cast(cls._framePixelBuffer, ctypes.c_void_p)
@vlc.CallbackDecorators.VideoUnlockCb
def vlcUnlockCallback(user_data, picture, planes):
"""Called when VLC releases the frame draw buffer.
"""
try:
cls = ctypes.cast(
user_data, ctypes.POINTER(ctypes.py_object)).contents.value
except (ValueError, TypeError):
return
cls._pixelLock.release()
@vlc.CallbackDecorators.VideoDisplayCb
def vlcDisplayCallback(user_data, picture):
"""Callback used by VLC when its ready to display a new frame.
"""
try:
cls = ctypes.cast(
user_data, ctypes.POINTER(ctypes.py_object)).contents.value
except (ValueError, TypeError):
return
cls._frameCounter += 1
@vlc.CallbackDecorators.LogCb
def vlcLogCallback(user_data, level, ctx, fmt, args):
"""Callback for logging messages emitted by VLC. Processed here and
converted to PsychoPy logging format.
"""
# if level == vlc.DEBUG:
# logging.debug()
pass # suppress messages from VLC, look scary but can be mostly ignored
def vlcMediaEventCallback(event, user_data, player):
"""Callback used by VLC for handling media player events.
"""
if not user_data():
return
cls = user_data() # ref to movie class
event = event.type
if event == vlc.EventType.MediaPlayerEndReached:
cls._onEos()
if __name__ == "__main__":
pass