class Video(object): """Read video file and draw it to the screen. Parameters ---------- ec : instance of expyfun.ExperimentController file_name : str the video file path pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If scale is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, and may result in letterboxing or pillarboxing). center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. visible : bool Whether to show the video when initialized. Can be toggled later using `Video.set_visible` method. Notes ----- This is a somewhat pared-down implementation of video playback. Looping is not available, and the audio stream from the video file is discarded. Timing of individual frames is relegated to the pyglet media player's internal clock. Recommended for use only in paradigms where the relative timing of audio and video are unimportant (e.g., if the video is merely entertainment for the participant during a passive auditory task). """ def __init__(self, ec, file_name, pos=(0, 0), units='norm', scale=1., center=True, visible=True): from pyglet.media import load, Player self._ec = ec # On Windows, the default is unaccelerated WMF, which is terribly slow. decoder = None if _new_pyglet(): try: from pyglet.media.codecs.ffmpeg import FFmpegDecoder decoder = FFmpegDecoder() except Exception as exc: warnings.warn( 'FFmpeg decoder could not be instantiated, decoding ' f'performance could be compromised:\n{exc}') self._source = load(file_name, decoder=decoder) self._player = Player() with warnings.catch_warnings(record=True): # deprecated eos_action self._player.queue(self._source) self._player._audio_player = None frame_rate = self.frame_rate if frame_rate is None: logger.warning('Frame rate could not be determined') frame_rate = 60. self._dt = 1. / frame_rate self._playing = False self._finished = False self._pos = pos self._units = units self._center = center self.set_scale(scale) # also calls set_pos self._visible = visible self._eos_fun = self._eos_new if _new_pyglet() else self._eos_old self._program = _create_program(ec, tex_vert, tex_frag) gl.glUseProgram(self._program) self._buffers = dict() for key in ('position', 'texcoord'): self._buffers[key] = gl.GLuint(0) gl.glGenBuffers(1, pointer(self._buffers[key])) w, h = self.source_width, self.source_height tex = np.array([(0, h), (w, h), (w, 0), (0, 0)], np.float32) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._buffers['texcoord']) gl.glBufferData(gl.GL_ARRAY_BUFFER, tex.nbytes, tex.tobytes(), gl.GL_DYNAMIC_DRAW) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glUseProgram(0) def play(self, auto_draw=True): """Play video from current position. Parameters ---------- auto_draw : bool If True, add ``self.draw`` to ``ec.on_every_flip``. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``play()`` was called. """ if not self._playing: if auto_draw: self._ec.call_on_every_flip(self.draw) self._player.play() self._playing = True else: warnings.warn('ExperimentController.video.play() called when ' 'already playing.') return self._ec.get_time() def pause(self): """Halt video playback. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``pause()`` was called. """ if self._playing: try: idx = self._ec.on_every_flip_functions.index(self.draw) except ValueError: # not auto_draw pass else: self._ec.on_every_flip_functions.pop(idx) self._player.pause() self._playing = False else: warnings.warn('ExperimentController.video.pause() called when ' 'already paused.') return self._ec.get_time() def _delete(self): """Halt video playback and remove player.""" if self._playing: self.pause() self._player.delete() def set_scale(self, scale=1.): """Set video scale. Parameters ---------- scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If scale is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, which may result in letterboxing). """ if isinstance(scale, string_types): _scale = self._ec.window_size_pix / np.array( (self.source_width, self.source_height), dtype=float) if scale == 'fit': scale = _scale.min() elif scale == 'fill': scale = _scale.max() self._scale = float(scale) # allows [1, 1., '1']; others: ValueError if self._scale <= 0: raise ValueError('Video scale factor must be strictly positive.') self.set_pos(self._pos, self._units, self._center) def set_pos(self, pos, units='norm', center=True): """Set video position. Parameters ---------- pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. """ pos = np.array(pos, float) if pos.size != 2: raise ValueError('pos must be a 2-element array') pos = np.reshape(pos, (2, 1)) pix = self._ec._convert_units(pos, units, 'pix').ravel() offset = np.array((self.width, self.height)) // 2 if center else 0 self._pos = pos self._actual_pos = pix - offset self._pos_unit = units self._pos_centered = center def _draw(self): tex = self._player.get_texture() gl.glUseProgram(self._program) gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(tex.target, tex.id) gl.glBindVertexArray(0) x, y = self._actual_pos w = self.source_width * self._scale h = self.source_height * self._scale pos = np.array([(x, y), (x + w, y), (x + w, y + h), (x, y + h)], np.float32) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._buffers['position']) gl.glBufferData(gl.GL_ARRAY_BUFFER, pos.nbytes, pos.tobytes(), gl.GL_DYNAMIC_DRAW) loc_pos = gl.glGetAttribLocation(self._program, b'a_position') gl.glEnableVertexAttribArray(loc_pos) gl.glVertexAttribPointer(loc_pos, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, 0) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._buffers['texcoord']) loc_tex = gl.glGetAttribLocation(self._program, b'a_texcoord') gl.glEnableVertexAttribArray(loc_tex) gl.glVertexAttribPointer(loc_tex, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, 0) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glDrawArrays(gl.GL_QUADS, 0, 4) gl.glDisableVertexAttribArray(loc_pos) gl.glDisableVertexAttribArray(loc_tex) gl.glUseProgram(0) gl.glBindTexture(tex.target, 0) def draw(self): """Draw the video texture to the screen buffer.""" self._player.update_texture() # detect end-of-stream to prevent pyglet from hanging: if not self._eos: if self._visible: self._draw() else: self._finished = True self.pause() self._ec.check_force_quit() def set_visible(self, show, flip=False): """Show/hide the video frame. Parameters ---------- show : bool Show or hide. flip : bool If True, flip after showing or hiding. """ if show: self._visible = True self._draw() else: self._visible = False self._ec.flip() if flip: self._ec.flip() # PROPERTIES @property def _eos(self): return self._eos_fun() def _eos_old(self): return (self._player._last_video_timestamp is not None and self._player._last_video_timestamp == self._source.get_next_video_timestamp()) def _eos_new(self): ts = self._source.get_next_video_timestamp() dur = self._source._duration return ts is None or ts >= dur @property def playing(self): return self._playing @property def finished(self): return self._finished @property def position(self): return np.squeeze(self._pos) @property def scale(self): return self._scale @property def duration(self): return self._source.duration @property def frame_rate(self): return self._source.video_format.frame_rate @property def dt(self): return self._dt @property def time(self): return self._player.time @property def width(self): return self.source_width * self._scale @property def height(self): return self.source_height * self._scale @property def source_width(self): return self._source.video_format.width @property def source_height(self): return self._source.video_format.height @property def time_offset(self): return self._ec.get_time() - self._player.time
class Video(object): """Read video file and draw it to the screen Parameters ---------- ec : instance of expyfun.ExperimentController file_name : str the video file path pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If `scale` is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, and may result in letterboxing or pillarboxing). center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. visible : bool Whether to show the video when initialized. Can be toggled later using ``set_visible`` method. Returns ------- None Notes ----- This is a somewhat pared-down implementation of video playback. Looping is not available, and the audio stream from the video file is discarded. Timing of individual frames is relegated to the pyglet media player's internal clock. Recommended for use only in paradigms where the relative timing of audio and video are unimportant (e.g., if the video is merely entertainment for the participant during a passive auditory task). """ def __init__(self, ec, file_name, pos=(0, 0), units='norm', scale=1., center=True, visible=True): from pyglet.media import load, Player self._ec = ec self._source = load(file_name) self._player = Player() with warnings.catch_warnings(record=True): # deprecated eos_action self._player.queue(self._source) self._player._audio_player = None frame_rate = self.frame_rate if frame_rate is None: logger.warning('Frame rate could not be determined') frame_rate = 60. self._dt = 1. / frame_rate self._texture = None self._playing = False self._finished = False self._pos = pos self._units = units self._center = center self.set_scale(scale) # also calls set_pos self._visible = visible def play(self): """Play video from current position. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``play()`` was called. """ if not self._playing: self._ec.call_on_every_flip(self.draw) self._player.play() self._playing = True else: warnings.warn('ExperimentController.video.play() called when ' 'already playing.') return self._ec.get_time() def pause(self): """Halt video playback. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``pause()`` was called. """ if self._playing: idx = self._ec.on_every_flip_functions.index(self.draw) self._ec.on_every_flip_functions.pop(idx) self._player.pause() self._playing = False else: warnings.warn('ExperimentController.video.pause() called when ' 'already paused.') return self._ec.get_time() def _delete(self): """Halt video playback and remove player.""" if self._playing: self.pause() self._player.delete() def _scale_texture(self): if self._texture: self._texture.width = self.source_width * self._scale self._texture.height = self.source_height * self._scale def set_scale(self, scale=1.): """Set video scale. Parameters ---------- scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If `scale` is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, which may result in letterboxing). """ if isinstance(scale, string_types): _scale = self._ec.window_size_pix / np.array( (self.source_width, self.source_height), dtype=float) if scale == 'fit': scale = _scale.min() elif scale == 'fill': scale = _scale.max() self._scale = float(scale) # allows [1, 1., '1']; others: ValueError if self._scale <= 0: raise ValueError('Video scale factor must be strictly positive.') self._scale_texture() self.set_pos(self._pos, self._units, self._center) def set_pos(self, pos, units='norm', center=True): """Set video position. Parameters ---------- pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. """ pos = np.array(pos, float) if pos.size != 2: raise ValueError('pos must be a 2-element array') pos = np.reshape(pos, (2, 1)) pix = self._ec._convert_units(pos, units, 'pix').ravel() offset = np.array((self.width, self.height)) // 2 if center else 0 self._pos = pos self._actual_pos = pix - offset self._pos_unit = units self._pos_centered = center def _draw(self): self._texture = self._player.get_texture() self._scale_texture() self._texture.blit(*self._actual_pos) def draw(self): """Draw the video texture to the screen buffer.""" self._player.update_texture() # detect end-of-stream to prevent pyglet from hanging: if not self._eos: if self._visible: self._draw() else: self._finished = True self.pause() self._ec.check_force_quit() def set_visible(self, show, flip=False): """Show/hide the video frame.""" if show: self._visible = True self._draw() else: self._visible = False self._ec.flip() if flip: self._ec.flip() # PROPERTIES @property def _eos(self): return (self._player._last_video_timestamp is not None and self._player._last_video_timestamp == self._source.get_next_video_timestamp()) @property def playing(self): return self._playing @property def finished(self): return self._finished @property def position(self): return np.squeeze(self._pos) @property def scale(self): return self._scale @property def duration(self): return self._source.duration @property def frame_rate(self): return self._source.video_format.frame_rate @property def dt(self): return self._dt @property def time(self): return self._player.time @property def width(self): return self.source_width * self._scale @property def height(self): return self.source_height * self._scale @property def source_width(self): return self._source.video_format.width @property def source_height(self): return self._source.video_format.height @property def time_offset(self): return self._ec.get_time() - self._player.time
class Video(object): """Read video file and draw it to the screen Parameters ---------- ec : instance of expyfun.ExperimentController file_name : str the video file path pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If `scale` is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, and may result in letterboxing or pillarboxing). center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. visible : bool Whether to show the video when initialized. Can be toggled later using ``set_visible`` method. Returns ------- None Notes ----- This is a somewhat pared-down implementation of video playback. Looping is not available, and the audio stream from the video file is discarded. Timing of individual frames is relegated to the pyglet media player's internal clock. Recommended for use only in paradigms where the relative timing of audio and video are unimportant (e.g., if the video is merely entertainment for the participant during a passive auditory task). """ def __init__(self, ec, file_name, pos=(0, 0), units='norm', scale=1., center=True, visible=True): from pyglet.media import load, Player self._ec = ec self._source = load(file_name) self._player = Player() self._player.queue(self._source) self._player._audio_player = None frame_rate = self.frame_rate if frame_rate is None: logger.warning('Frame rate could not be determined') frame_rate = 60. self._dt = 1. / frame_rate self._texture = None self._playing = False self._finished = False self._pos = pos self._units = units self._center = center self.set_scale(scale) # also calls set_pos self._visible = visible def play(self): """Play video from current position. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``play()`` was called. """ if not self._playing: self._ec.call_on_every_flip(self.draw) self._player.play() self._playing = True else: warnings.warn('ExperimentController.video.play() called when ' 'already playing.') return self._ec.get_time() def pause(self): """Halt video playback. Returns ------- time : float The timestamp (on the parent ``ExperimentController`` timeline) at which ``pause()`` was called. """ if self._playing: idx = self._ec.on_every_flip_functions.index(self.draw) self._ec.on_every_flip_functions.pop(idx) self._player.pause() self._playing = False else: warnings.warn('ExperimentController.video.pause() called when ' 'already paused.') return self._ec.get_time() def _delete(self): """Halt video playback and remove player.""" if self._playing: self.pause() self._player.delete() def _scale_texture(self): if self._texture: self._texture.width = self.source_width * self._scale self._texture.height = self.source_height * self._scale def set_scale(self, scale=1.): """Set video scale. Parameters ---------- scale : float | str The scale factor. 1 is native size (pixel-to-pixel), 2 is twice as large, etc. If `scale` is a string, it must be either ``'fill'`` (which ensures the entire ``ExperimentController`` window is covered by the video, at the expense of some parts of the video potentially being offscreen), or ``'fit'`` (which scales maximally while ensuring none of the video is offscreen, which may result in letterboxing). """ if isinstance(scale, string_types): _scale = self._ec.window_size_pix / np.array((self.source_width, self.source_height), dtype=float) if scale == 'fit': scale = _scale.min() elif scale == 'fill': scale = _scale.max() self._scale = float(scale) # allows [1, 1., '1']; others: ValueError if self._scale <= 0: raise ValueError('Video scale factor must be strictly positive.') self._scale_texture() self.set_pos(self._pos, self._units, self._center) def set_pos(self, pos, units='norm', center=True): """Set video position. Parameters ---------- pos : array-like 2-element array-like with X, Y elements. units : str Units to use for the position. See ``check_units`` for options. center : bool If ``False``, the elements of ``pos`` specify the position of the lower left corner of the video frame; otherwise they position the center of the frame. """ pos = np.array(pos, float) if pos.size != 2: raise ValueError('pos must be a 2-element array') pos = np.reshape(pos, (2, 1)) pix = self._ec._convert_units(pos, units, 'pix').ravel() offset = np.array((self.width, self.height)) // 2 if center else 0 self._pos = pos self._actual_pos = pix - offset self._pos_unit = units self._pos_centered = center def _draw(self): self._texture = self._player.get_texture() self._scale_texture() self._texture.blit(*self._actual_pos) def draw(self): """Draw the video texture to the screen buffer.""" self._player.update_texture() # detect end-of-stream to prevent pyglet from hanging: if not self._eos: if self._visible: self._draw() else: self._finished = True self.pause() self._ec.check_force_quit() def set_visible(self, show, flip=False): """Show/hide the video frame.""" if show: self._visible = True self._draw() else: self._visible = False self._ec.flip() if flip: self._ec.flip() # PROPERTIES @property def _eos(self): return (self._player._last_video_timestamp is not None and self._player._last_video_timestamp == self._source.get_next_video_timestamp()) @property def playing(self): return self._playing @property def finished(self): return self._finished @property def position(self): return np.squeeze(self._pos) @property def scale(self): return self._scale @property def duration(self): return self._source.duration @property def frame_rate(self): return self._source.video_format.frame_rate @property def dt(self): return self._dt @property def time(self): return self._player.time @property def width(self): return self.source_width * self._scale @property def height(self): return self.source_height * self._scale @property def source_width(self): return self._source.video_format.width @property def source_height(self): return self._source.video_format.height @property def time_offset(self): return self._ec.get_time() - self._player.time