# Python 3 compatibility
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
# MoviePy
try:
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.tools import cvsecs
import numpy as np
except ImportError as e:
print("""Error importing dependencies:
{0}
This module depends on the following packages
- MoviePy
- ImageIO
- Numpy
Please make sure that they are installed.""".format(e))
# Other modules
import os
import time
import threading
import logging
logger = logging.getLogger(__name__)
try:
# Python 3
from queue import Queue, Full
except:
# Python 2
from Queue import Queue, Full
queue_length = 3
from mediadecoder.states import *
from mediadecoder.timer import Timer
from mediadecoder.soundrenderers._base import SoundRenderer
[docs]class Decoder(object):
""" This class loads a video file that can be played. It can
be passed a callback function to which decoded video frames should be passed.
"""
[docs] def __init__(self, mediafile=None, videorenderfunc=None, play_audio=True):
"""
Constructor.
Parameters
----------
mediafile : str, optional
The path to the mediafile to be loaded (default: None)
videorenderfunc : callable (default: None)
Callback function that takes care of the actual
Rendering of the videoframe.\
The specified renderfunc should be able to accept the following
arguments:
- frame (numpy.ndarray): the videoframe to be rendered
play_audio : bool, optional
Whether audio of the clip should be played.
"""
# Create an internal timer
self.clock = Timer()
# Load a video file if specified, but allow users to do this later
# by initializing all variables to None
if not self.load_media(mediafile, play_audio):
self.reset()
# Set callback function if set
self.set_videoframerender_callback(videorenderfunc)
# Store instance variables
self.play_audio = play_audio
@property
def frame_interval(self):
""" Duration in seconds of a single frame. """
return self.clock.frame_interval
@property
def current_frame_no(self):
""" Current frame_no of video. """
return self.clock.current_frame
@property
def current_videoframe(self):
""" Representation of current video frame as a numpy array. """
return self.__current_videoframe
@property
def current_playtime(self):
""" Clocks current runtime in seconds. """
return self.clock.time
@property
def loop(self):
""" Indicates whether the playback should loop. """
return self._loop
@loop.setter
def loop(self, value):
""" Indicates whether the playback should loop.
Parameters
----------
value : bool
True if playback should loop, False if not.
"""
if not type(value) == bool:
raise TypeError("can only be True or False")
self._loop = value
[docs] def reset(self):
""" Resets the player and discards loaded data. """
self.clip = None
self.loaded_file = None
self.fps = None
self.duration = None
self.status = UNINITIALIZED
self.clock.reset()
[docs] def set_videoframerender_callback(self, func):
""" Sets the function to call when a new frame is available.
This function is passed the frame (in the form of a numpy.ndarray) and
should take care of the rendering.
Parameters
----------
func : callable
The function to pass the new frame to once it becomes available.
"""
# Check if renderfunc is indeed a function
if not func is None and not callable(func):
raise TypeError("The object passed for videorenderfunc is not a function")
self.__videorenderfunc = func
[docs] def set_audiorenderer(self, renderer):
""" Sets the SoundRenderer object. This should take care of processing
the audioframes set in audioqueue.
Parameters
----------
renderer : soundrenderers.SoundRenderer
A subclass of soundrenderers.SoundRenderer that takes care of the
audio rendering.
Raises
------
RuntimeError
If no information about the audiostream is available. This could be
because no video has been loaded yet, or because no embedded
audiostream could be detected in the video, or play_sound was set
to False.
"""
if not hasattr(self, 'audioqueue') or self.audioqueue is None:
raise RuntimeError("No video has been loaded, or no audiostream "
"was detected.")
if not isinstance(renderer, SoundRenderer):
raise TypeError("Invalid renderer object. Not a subclass of "
"SoundRenderer")
self.soundrenderer = renderer
self.soundrenderer.queue = self.audioqueue
[docs] def play(self):
""" Start the playback of the video.
The playback loop is run in a separate thread, so this function returns
immediately. This allows one to implement things such as event handling
loops (e.g. check for key presses) elsewhere.
"""
### First do some status checks
# Make sure a file is loaded
if self.status == UNINITIALIZED or self.clip is None:
raise RuntimeError("Player uninitialized or no file loaded")
# Check if playback has already finished (rewind needs to be called first)
if self.status == EOS:
logger.debug("End of stream has already been reached")
return
# Check if playback hasn't already been started (and thus if play()
# has not been called before from another thread for instance)
if self.status in [PLAYING,PAUSED]:
logger.warning("Video already started")
return
### If all is in order start the general playing loop
if self.status == READY:
self.status = PLAYING
self.last_frame_no = 0
if not hasattr(self,"renderloop") or not self.renderloop.isAlive():
if self.audioformat:
# Chop the total stream into separate audio chunks that are the
# lenght of a video frame (this way the index of each chunk
# corresponds to the video frame it belongs to.)
self.__calculate_audio_frames()
# Start audio handling thread. This thread places audioframes
# into a sound buffer, untill this buffer is full.
self.audioframe_handler = threading.Thread(
target=self.__audiorender_thread)
self.audioframe_handler.start()
# Start main rendering loop.
self.renderloop = threading.Thread(target=self.__render)
self.renderloop.start()
else:
logger.warning("Rendering thread already running!")
[docs] def pause(self):
""" Pauses or resumes the video and/or audio stream. """
# Change playback status only if current status is PLAYING or PAUSED
# (and not READY).
logger.debug("Pausing playback")
if self.status == PAUSED:
# Recalculate audio stream position to make sure it is not out of
# sync with the video
self.__calculate_audio_frames()
self.status = PLAYING
self.clock.pause()
elif self.status == PLAYING:
self.status = PAUSED
self.clock.pause()
[docs] def stop(self):
""" Stops the video stream and resets the clock. """
logger.debug("Stopping playback")
# Stop the clock
self.clock.stop()
# Set plauyer status to ready
self.status = READY
[docs] def seek(self, value):
""" Seek to the specified time.
Parameters
----------
value : str or int
The time to seek to. Can be any of the following formats:
>>> 15.4 -> 15.4 # seconds
>>> (1,21.5) -> 81.5 # (min,sec)
>>> (1,1,2) -> 3662 # (hr, min, sec)
>>> '01:01:33.5' -> 3693.5 #(hr,min,sec)
>>> '01:01:33.045' -> 3693.045
>>> '01:01:33,5' #comma works too
"""
# Pause the stream
self.pause()
self.clock.time = value
logger.debug("Seeking to {} seconds; frame {}".format(self.clock.time,
self.clock.current_frame))
if self.audioformat:
self.__calculate_audio_frames()
# Resume the stream
self.pause()
[docs] def rewind(self):
""" Rewinds the video to the beginning.
Convenience function simply calling seek(0). """
self.seek(0)
def __calculate_audio_frames(self):
""" Aligns audio with video.
This should be called for instance after a seeking operation or resuming
from a pause. """
if self.audioformat is None:
return
start_frame = self.clock.current_frame
totalsize = int(self.clip.audio.fps*self.clip.audio.duration)
self.audio_times = list(range(0, totalsize,
self.audioformat['buffersize'])) + [totalsize]
# Remove audio segments up to the starting frame
del(self.audio_times[0:start_frame])
def __render(self):
""" Main render loop.
Checks clock if new video and audio frames need to be rendered.
If so, it passes the frames to functions that take care
of rendering these frames. """
# Render first frame
self.__render_videoframe()
# Start videoclock with start of this thread
self.clock.start()
logger.debug("Started rendering loop.")
# Main rendering loop
while self.status in [PLAYING,PAUSED]:
current_frame_no = self.clock.current_frame
# Check if end of clip has been reached
if self.clock.time >= self.duration:
logger.debug("End of stream reached at {}".format(self.clock.time))
if self.loop:
logger.debug("Looping: restarting stream")
# Seek to the start
self.seek(0)
else:
# End of stream has been reached
self.status = EOS
break
if self.last_frame_no != current_frame_no:
# A new frame is available. Get it from te stream
self.__render_videoframe()
self.last_frame_no = current_frame_no
# Sleeping is a good idea to give the other threads some breathing
# space to do their work.
time.sleep(0.005)
# Stop the clock.
self.clock.stop()
logger.debug("Rendering stopped.")
def __render_videoframe(self):
""" Retrieves a new videoframe from the stream.
Sets the frame as the __current_video_frame and passes it on to
__videorenderfunc() if it is set. """
new_videoframe = self.clip.get_frame(self.clock.time)
# Pass it to the callback function if this is set
if callable(self.__videorenderfunc):
self.__videorenderfunc(new_videoframe)
# Set current_frame to current frame (...)
self.__current_videoframe = new_videoframe
def __audiorender_thread(self):
""" Thread that takes care of the audio rendering. Do not call directly,
but only as the target of a thread. """
new_audioframe = None
logger.debug("Started audio rendering thread.")
while self.status in [PLAYING,PAUSED]:
# Retrieve audiochunk
if self.status == PLAYING and new_audioframe is None:
# Get a new frame from the audiostream, skip to the next one
# if the current one gives a problem
try:
start = self.audio_times.pop(0)
stop = self.audio_times[0]
except IndexError:
logger.debug("Audio times could not be obtained")
time.sleep(0.02)
continue
# Get the frame numbers to extract from the audio stream.
chunk = (1.0/self.audioformat['fps'])*np.arange(start, stop)
try:
# Extract the frames from the audio stream. Does not always,
# succeed (e.g. with bad streams missing frames), so make
# sure this doesn't crash the whole program.
new_audioframe = self.clip.audio.to_soundarray(
tt = chunk,
buffersize = self.frame_interval*self.clip.audio.fps,
quantize=True
)
except OSError as e:
logger.warning("Sound decoding error: {}".format(e))
new_audioframe = None
# Put audioframe in buffer/queue for soundrenderer to pick up. If
# the queue is full, try again after a timeout (this allows to check
# if the status is still PLAYING after a pause.)
if not new_audioframe is None:
try:
self.audioqueue.put(new_audioframe, timeout=.05)
new_audioframe = None
except Full:
pass
logger.debug("Stopped audio rendering thread.")
def __repr__(self):
""" Create a string representation for when print() is called. """
return "Decoder [file loaded: {0}]".format(self.loaded_file)