#!/usr/bin/env python# -*- coding: utf-8 -*-"""A stimulus class for playing movies (mpeg, avi, etc...) in PsychoPy using alocal 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.#importosimportsysimportthreadingimportctypesimportweakreffrompsychopyimportcore,loggingfrompsychopy.tools.attributetoolsimportlogAttrib,setAttributefrompsychopy.tools.filetoolsimportpathToStringfrompsychopy.visual.basevisualimportBaseVisualStim,ContainerMixinfrompsychopy.constantsimportFINISHED,NOT_STARTED,PAUSED,PLAYING,STOPPEDimportnumpyimportpygletpyglet.options['debug_gl']=FalseGL=pyglet.gltry:# check if the lib can be loadedimportvlchaveVLC=TrueexceptExceptionaserr:haveVLC=False# store the error but only raise it if theif"wrong architecture"instr(err):msg=("Failed to import `vlc` module required by `vlcmoviestim`.\n""You're using %i-bit python. Is your VLC install the same?"%64ifsys.maxsize==2**64else32)_vlcImportErr=OSError(msg)else:_vlcImportErr=err# flip time, and time since last movie frame flip will be printedreportNDroppedFrames=10
[docs]classVlcMovieStim(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 libifnothaveVLC: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 pygletifwin.winType!='pyglet':logging.error('Movie stimuli can only be used with a pyglet window')core.quit()# drawing stuffself.flipVert=flipVertself.flipHoriz=flipHorizself.pos=numpy.asarray(pos,float)# original size to keep BaseVisualStim happyself._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=sizeisNoneifnotself._useFrameSizeFromVideo:self.size=numpy.asarray(size,float)self.depth=depthself.opacity=float(opacity)# playback stuffself._filename=pathToString(filename)self._volume=volumeself._noAudio=noAudio# cannot be changedself._currentFrame=-1self._loopCount=0self.loop=loop# video pixel and texture buffer variables, setup laterself.interpolate=interpolate# use setterself._textureId=GL.GLuint()self._pixbuffId=GL.GLuint()# VLC related attributesself._instance=Noneself._player=Noneself._manager=Noneself._stream=Noneself._videoWidth=0self._videoHeight=0self._frameRate=0.0self._vlcInitialized=Falseself._pixelLock=threading.Lock()# semaphore for VLC pixel transferself._framePixelBuffer=None# buffer pointer to draw toself._videoFrameBufferSize=Noneself._streamEnded=False# spawn a VLC instance for this class instance# self._createVLCInstance()# load a movie if providedifself._filename:self.loadMovie(self._filename)self.setVolume(volume)self.nDroppedFrames=0self._autoStart=autoStartself.ori=ori# set autoLog (now that params have been initialised)self.autoLog=autoLogifautoLog:logging.exp("Created {} = {}".format(self.name,self))@propertydeffilename(self):"""File name for the loaded video (`str`)."""returnself._filename@filename.setterdeffilename(self,value):self.loadMovie(value)@propertydefautoStart(self):"""Start playback when `.draw()` is called (`bool`)."""returnself._autoStart@autoStart.setterdefautoStart(self,value):self._autoStart=bool(value)
[docs]defsetMovie(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]defloadMovie(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 playerself._openMedia()self.status=NOT_STARTEDlogAttrib(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.ifself._instanceisnotNone: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"ifself._noAudioelse"","--sout-keep","--quiet"])self._instance=vlc.Instance(params)# used to capture log messages from VLCself._instance.log_set(vlcLogCallback,None)# create a new player object, reusable by by just changing the streamself._player=self._instance.media_player_new()# setup the event managerself._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=Truelogging.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=Falseifself._playerisnotNone:self._player.stop()# shutdown the managerself._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 informationself._filename=Noneself._videoWidth=self._videoHeight=0self._frameCounter=self._loopCount=0self._frameRate=0.0self._framePixelBuffer=Noneifself._streamisnotNone:self._stream.release()self._streamEnded=Falseself._stream=Noneifself._instanceisnotNone:self._instance.release()self._instance=Noneself.status=STOPPED
[docs]def_openMedia(self,uri=None):"""Internal method that opens a new stream using `filename`. This will close the previous stream. Raises an error if a VLC instance is not available. """# if None, use `filename`uri=self.filenameifuriisNoneelseuri# create a fresh VLC instanceself._createVLCInstance()# raise error if there is no VLC instanceifself._instanceisNone:errmsg="Cannot open a stream without a VLC instance started."logging.fatal(errmsg,obj=self)logging.flush()# check if we have a player, stop it if soifself._playerisNone:errmsg="Cannot open a stream without a VLC media player."logging.fatal(errmsg,obj=self)logging.flush()else:self._player.stop()# stop any playback# check if the file is valid and readableifnotos.access(uri,os.R_OK):raiseRuntimeError('Error: %s file not readable'%uri)# close the previous VLC resources if neededifself._streamisnotNone:self._stream.release()# open the file and create a new streamtry:self._stream=self._instance.media_new(uri)exceptNameError:raiseImportError('NameError: %s vs LibVLC %s'%(vlc.__version__,vlc.libvlc_get_version()))# player objectself._player.set_media(self._stream)# set media related attributesself._stream.parse()videoWidth,videoHeight=self._player.video_get_size()self._videoWidth=videoWidthself._videoHeight=videoHeight# set the video sizeifself._useFrameSizeFromVideo:self.size=self._origSize=numpy.array((self._videoWidth,self._videoHeight),float)self._frameRate=self._player.get_fps()self._frameCounter=self._loopCount=0self._streamEnded=False# uncomment if not using direct GPU write, might be more thread-safeself._framePixelBuffer=(ctypes.c_ubyte*self._videoWidth*self._videoHeight*4)()# duration unavailable until startedduration=self._player.get_length()logging.info("Video is %ix%i, duration %s, fps %s"%(self._videoWidth,self._videoHeight,duration,self._frameRate))logging.flush()# We assume we can use the RGBA format hereself._player.video_set_format("RGBA",self._videoWidth,self._videoHeight,self._videoWidth<<2)# setup texture buffers before binding the callbacksself._setupTextureBuffers()# Set callbacks since we have the resources to write to.thisInstance=ctypes.cast(ctypes.pointer(ctypes.py_object(self)),ctypes.c_void_p)# we need to increment the ref countctypes.pythonapi.Py_IncRef(ctypes.py_object(thisInstance))self._player.video_set_callbacks(vlcLockCallback,vlcUnlockCallback,vlcDisplayCallback,thisInstance)
[docs]def_closeMedia(self):"""Internal method to release the presently loaded stream (if any). """self._vlcInitialized=Falseifself._playerisnotNone:# stop any playbackself._player.stop()self._player.set_media(None)ifself._streamisNone:return# nopself._stream.release()self._stream=self._player=Noneself._useFrameSizeFromVideo=Trueself._releaseVLCInstance()# unregister callbacks before freeing buffersself._freeBuffers()GL.glFlush()
[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=Trueifnotself.loop:self.status=FINISHEDifself.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 createdifself._pixbuffId.value>0:GL.glDeleteBuffers(1,self._pixbuffId)self._pixbuffId=GL.GLuint()# delete the old texture if presentifself._textureId.value>0:GL.glDeleteTextures(1,self._textureId)self._textureId=GL.GLuint()exceptTypeError:# can happen when unloading or shutting downpass
[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 -> GLGL.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 pixels0,GL.GL_RGBA,GL.GL_UNSIGNED_BYTE,None)GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT,1)# needs to be 1# setup texture filteringifself.interpolate:texFilter=GL.GL_LINEARelse:texFilter=GL.GL_NEARESTGL.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
@propertydefframeTexture(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. """returnself._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 bufferGL.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 OpenGLGL.glEnable(GL.GL_TEXTURE_2D)# copy the PBO to the textureGL.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 neededifself._texFilterNeedsUpdate:ifself.interpolate:texFilter=GL.GL_LINEARelse:texFilter=GL.GL_NEARESTGL.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 PBOGL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER,0)GL.glBindTexture(GL.GL_TEXTURE_2D,0)GL.glDisable(GL.GL_TEXTURE_2D)
[docs]defupdateTexture(self):"""Update the video texture buffer to the most recent video frame. """withself._pixelLock:self._pixelTransfer()
# --------------------------------------------------------------------------# Video playback controls and status#@propertydefisPlaying(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.#returnself.status==PLAYING@propertydefisNotStarted(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."""returnself.status==NOT_STARTED@propertydefisStopped(self):"""`True` if the video is stopped (`bool`)."""returnself.status==STOPPED@propertydefisPaused(self):"""`True` if the video is presently paused (`bool`)."""returnself.status==PAUSED@propertydefisFinished(self):"""`True` if the video is finished (`bool`)."""# why is this the same as STOPPED?returnself.status==FINISHED
[docs]defplay(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. """ifself.isNotStarted:# video has not been played yetiflogandself.autoLog:self.win.logOnFlip("Set %s playing"%self.name,level=logging.EXP,obj=self)self._player.play()elifself.isPaused:self.win.logOnFlip("Resuming playback at position {:.4f}".format(self._player.get_time()/1000.0),level=logging.EXP,obj=self)ifself._player.will_play():self._player.play()self.status=PLAYINGreturnself._currentFrame
[docs]defpause(self,log=True):"""Pause the current point in the movie. Parameters ---------- log : bool Log the pause event. """ifself.isPlaying:self.status=PAUSEDplayer=self._playerifplayerandplayer.can_pause():player.pause()iflogandself.autoLog:self.win.logOnFlip("Set %s paused"%self.name,level=logging.EXP,obj=self)returnTrueiflogandself.autoLog:self.win.logOnFlip("Failed Set %s paused"%self.name,level=logging.EXP,obj=self)returnFalse
[docs]defstop(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. """ifself._playerisNone:returnself.status=STOPPEDiflogandself.autoLog:self.win.logOnFlip("Set %s stopped"%self.name,level=logging.EXP,obj=self)self._releaseVLCInstance()
[docs]defseek(self,timestamp,log=True):"""Seek to a particular timestamp in the movie. Parameters ---------- timestamp : float Time in seconds. log : bool Log the seek event. """ifself.isPlayingorself.isPaused:player=self._playerifplayerandplayer.is_seekable():# pause while seekingplayer.set_time(int(timestamp*1000.0))iflog:logAttrib(self,log,'seek',timestamp)
[docs]defrewind(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 seekingreturnself.getCurrentFrameTime()
[docs]deffastForward(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))returnself.getCurrentFrameTime()
[docs]defreplay(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._filenameself.stop()self.loadMovie(lastMovieFile)ifautoPlay:self.play()
# --------------------------------------------------------------------------# Volume controls#@propertydefvolume(self):"""Audio track volume (`int` or `float`). See `setVolume` for more information about valid values. """returnself.getVolume()@volume.setterdefvolume(self,value):self.setVolume(value)
[docs]defsetVolume(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. """ifself._player:if0.0<=volume<=1.0andisinstance(volume,float):v=int(volume*100)else:v=int(volume)self._volume=vifself._player:self._player.audio_set_volume(v)
[docs]defgetVolume(self):"""Returns the current movie audio volume. Returns ------- int Volume level, 0 is no audio, 100 is max audio volume. """ifself._player:self._volume=self._player.audio_get_volume()returnself._volume
[docs]defincreaseVolume(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() """ifnotself._player:return0self.setVolume(min(max(self.getVolume()+int(amount),0),100))returnself._volume
[docs]defdecreaseVolume(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() """ifnotself._player:return0self.setVolume(min(max(self.getVolume()-int(amount),0),100))returnself._volume
# --------------------------------------------------------------------------# Video and playback information#@propertydefframeIndex(self):"""Current frame index being displayed (`int`)."""returnself._currentFrame
[docs]defgetCurrentFrameNumber(self):"""Get the current movie frame number (`int`), same as `frameIndex`. """returnself._frameCounter
@propertydefpercentageComplete(self):"""Percentage of the video completed (`float`)."""returnself.getPercentageComplete()@propertydefduration(self):"""Duration of the loaded video in seconds (`float`). Not valid unless the video has been started. """ifnotself.isNotStarted:returnself._player.get_length()return0.0@propertydefloopCount(self):"""Number of loops completed since playback started (`int`). This value is reset when either `stop` or `loadMovie` is called. """returnself._loopCount@propertydeffps(self):"""Movie frames per second (`float`)."""returnself.getFPS()
[docs]defgetFPS(self):"""Movie frames per second. Returns ------- float Nominal number of frames to be displayed per second. """returnself._frameRate
@propertydefframeTime(self):"""Current frame time in seconds (`float`)."""returnself.getCurrentFrameTime()
[docs]defgetCurrentFrameTime(self):"""Get the time that the movie file specified the current video frame as having. Returns ------- float Current video time in seconds. """ifnotself._player:return0.0returnself._player.get_time()/1000.0
[docs]defgetPercentageComplete(self):"""Provides a value between 0.0 and 100.0, indicating the amount of the movie that has been already played. """returnself._player.get_position()*100.0
@propertydefvideoSize(self):"""Size of the video `(w, h)` in pixels (`tuple`). Returns `(0, 0)` if no video is loaded."""ifself._streamisnotNone:returnself._videoWidth,self._videoHeightreturn0,0# --------------------------------------------------------------------------# Drawing methods and properties#@propertydefinterpolate(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. """returnself._interpolate@interpolate.setterdefinterpolate(self,value):self._interpolate=valueself._texFilterNeedsUpdate=True
[docs]defsetFlipHoriz(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=newVallogAttrib(self,log,'flipHoriz')
[docs]defsetFlipVert(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=notnewVallogAttrib(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 activGL.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 rotatevertsPix=self.verticesPixarray=(GL.GLfloat*32)(1,1,# texture coordsvertsPix[0,0],vertsPix[0,1],0.,# vertex0,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 arrayGL.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]defdraw(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. """ifself.isNotStartedandself._autoStart:self.play()elifself._streamEndedandself.loop:self._loopCount+=1self._streamEnded=Falseself.replay()elifself.isFinished:self.stop()returnFalseself._selectWindow(self.winifwinisNoneelsewin)# check if we need to pull a new frame this roundifself._currentFrame==self._frameCounter:# update the texture, getting the most recent frameself.updateTexture()# draw the frame to the windowself._drawRectangle()returnFalse# frame does not need an update this round# Below not called if the frame hasn't advanced yet ------# update the current frameself._currentFrame=self._frameCounter# update the texture, getting the most recent frameself.updateTexture()# draw the frame to the windowself._drawRectangle()# token gesture for existing code, we handle this logic internally nowreturnTrue
[docs]defsetAutoDraw(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 """ifval:self.play(log=False)# set to play in case stoppedelse:self.pause(log=False)# add to drawing list and update statussetAttribute(self,'autoDraw',val,log)
def__del__(self):try:ifhasattr(self,'_player'):# false if crashed before creating instanceself._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.VideoLockCbdefvlcLockCallback(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.valueexcept(ValueError,TypeError):returncls._pixelLock.acquire()# tell VLC to take the data and stuff it into the bufferplanes[0]=ctypes.cast(cls._framePixelBuffer,ctypes.c_void_p)@vlc.CallbackDecorators.VideoUnlockCbdefvlcUnlockCallback(user_data,picture,planes):"""Called when VLC releases the frame draw buffer. """try:cls=ctypes.cast(user_data,ctypes.POINTER(ctypes.py_object)).contents.valueexcept(ValueError,TypeError):returncls._pixelLock.release()@vlc.CallbackDecorators.VideoDisplayCbdefvlcDisplayCallback(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.valueexcept(ValueError,TypeError):returncls._frameCounter+=1@vlc.CallbackDecorators.LogCbdefvlcLogCallback(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 ignoreddefvlcMediaEventCallback(event,user_data,player):"""Callback used by VLC for handling media player events. """ifnotuser_data():returncls=user_data()# ref to movie classevent=event.typeifevent==vlc.EventType.MediaPlayerEndReached:cls._onEos()if__name__=="__main__":pass