import sys, thread, time, traceback
import pymedia.muxer as muxer
import pymedia.audio.acodec as acodec
import pymedia.video.vcodec as vcodec
import pymedia.audio.sound as sound
SEEK_IN_PROGRESS= -2
PAUSE_SLEEP= 0.003
FILE_CHUNK= 300000
EXITING_FLAG=-1
########################################################################3
# Simple video player
class Player:
"""
Player is a class that plays all audio/video formats supported by pymedia
It provides very simple interface and many features such as:
- seeking
- extracting metadata
- multiple sound cards support
- different sources for video rendering
- error handling
- volume operations
Here is the simple way of calling Player:
player= pymedia.Player()
player.start()
player.startPlayback( '<YOUR_MP3_FILE>' )
while player.isPlaying():
time.sleep( 0.01 )
Player supports the following callback:
class Callback:
# called _before_ the audio frame is rendered
# return your audio data after you process the audio frame
# or return none for using the default sound processing
def onAudioReady( self, afr ):
pass
# Called when the video frame is about to be rendered
def onVideoReady( self, vfr ):
pass
Use the callback like below:
c= Callback()
player= pymedia.Player( c )
"""
# ------------------------------------
def __init__( self, callback= None ):
"""
Create new Player instance. In case if you want to play video you have to supply the callback class instance
which will get video data and will be able to display it properly ( onVideoReady() call ).
For audio data the playback is done through the pymedia.sound.Output object.
Before playing audio the onAudioReady( afr ) callback is called so you can modify audio data.
Just return None if you do not wish to modify that data or return a sound data as string in case if you do.
Remember that sampling frequency and number of channels should remain the same as in afr object.
"""
self.frameNum= -1
self.exitFlag= 1
self.ct= None
self.pictureSize= None
self.paused= 0
self.snd= None
self.aDelta= 0
self.aBitRate= 0
self.vBitRate= 0
self.seek= -1
self.vc= None
self.ac= None
self.ovlTime= 0
self.playingFile= None
self.endPos= None
self.loops= 0
self.volume= 0xffff
self.startPos= 0
self.fileSize= 0
self.initADelta= -1
# Set length to 1 sec by default
self.length= -1
# By default the first audio card
self.audioCard= 0
self.callback= callback
self.metaData= {}
self.fileFormat= 'mp3'
self.aPTS= self.aindex= self.vindex= -1
self.clearError()
self.maxBufSize= -1
# ------------------------------------
def _resetAudio( self ):
# No delta for audio so far
self.snd= self.resampler= None
self.aBitRate= self.aSampleRate= self.aChannels= self.aDelta= 0
self.aDecodedFrames= []
if self.ac:
self.ac.reset()
# ------------------------------------
def _initAudio( self, params ):
try:
# Reset audio stream
self._resetAudio()
# Initializing audio codec
self.ac= acodec.Decoder( params )
self.aindex= -1
except:
self.err.append( sys.exc_info() )
# ------------------------------------
def _resetVideo( self ):
# Init all used vars first
self.decodeTime= self.vBitRate= self.frameNum= \
self.sndDelay= self.hurry= self.videoPTS= \
self.lastPTS= self.frRate= self.vDelay= 0
self.seek= -1
self.pictureSize= None
if self.initADelta!= -1:
self.seekADelta= self.initADelta
# Zeroing out decoded pics queue
self.decodedFrames= []
self.rawFrames= []
if self.vc:
self.vc.reset()
# ------------------------------------
def _initVideo( self, params ):
# There is no overlay created yet
try:
# Set the initial sound delay to 0 for now
# It defines initial offset from video in the beginning of the stream
self.initADelta= -1
self.vindex= -1
self._resetVideo()
self.seekADelta= 0
# Setting up the HW video codec
self.vc= pymedia.video.ext_codecs.Decoder( params )
except:
try:
# Fall back to SW video codec
self.vc= vcodec.Decoder( params )
except:
pass
# ------------------------------------
def _getStreamLength( self, format, dm, f, fr ):
# Get file size if possible
pos= f.tell()
f.seek( 0, 2 )
self.fileSize= f.tell()
f.seek( pos, 0 )
# Demux frames from the beginning and from the end and get the PTS diff
if not dm.hasHeader():
startPTS= -1
for d in fr:
if d[ 3 ]> 0:
startPTS= d[ 3 ] / 90
break
# Seek to the end and get the PTS from the end of the file
if startPTS> 0:
pos= f.tell()
f.seek( 0, 0 )
dm1= muxer.Demuxer( format )
s= f.read( FILE_CHUNK )
r= dm1.parse( s )
endPTS= startPTS
for d in fr:
if d[ 3 ]> 0:
endPTS= d[ 3 ] / 90
f.seek( pos, 0 )
self.length= endPTS- startPTS
else:
lStreams= filter( lambda x: x and ( x[ 'length' ]> 0 ), dm.streams )
if len( lStreams ):
self.length= max( [ x[ 'length' ] for x in lStreams ] )
else:
self.length= -1
# Check file size against length and bitrates
#if self.length* ( self.getBitRate()/ 8 )< self.fileSize:
# self.length= self.fileSize/ ( self.getBitRate()/ 8 )
# ------------------------------------
def _getVStreamParams( self, vindex, vparams, r ):
# Decode one video frame and 1 audio frame to get stream data
vDec= None
for d in r:
try:
# Demux file into streams
if d[ 0 ]== vindex:
if not vDec:
vDec= vcodec.Decoder( vparams )
vfr= vDec.decode( d[ 1 ] )
if vfr and vfr.data:
if vfr.aspect_ratio> .0:
self.pictureSize= ( int( vfr.size[ 1 ]* vfr.aspect_ratio ), vfr.size[ 1 ] )
else:
self.pictureSize= vfr.size
self.vBitRate= vfr.bitrate
break
except:
self.err.append( sys.exc_info() )
break
# ------------------------------------
def _processVideoFrame( self, d, forced= False ):
# See if we should show video frame now
if not forced:
self.rawFrames.append( d )
if len( self.decodedFrames )== 0:
if self._decodeVideoFrame( forced )== -1:
return
# See if we have our frame inline with the sound
self._processVideo( forced )
# ------------------------------------
def _decodeVideoFrame( self, forced= False ):
# Decode the video frame
if self.snd== None and self.seek!= SEEK_IN_PROGRESS and self.aindex!= -1 and not forced:
return -1
if len( self.decodedFrames )> vcodec.MAX_BUFFERS- 4:
# Cannot decode video frame because of too many decoded frames already
return 0
while len( self.rawFrames ):
d= self.rawFrames.pop( 0 )
vfr= self.vc.decode( d[ 1 ] )
if vfr:
if self.seek== SEEK_IN_PROGRESS and not forced:
if vfr.data:
self.seek= -1
else:
# We decode video frame after seek, but no I frame so far
return 0
# If frame has data in it, put it in to the queue along with PTS
self.decodedFrames.append( ( vfr, self.videoPTS ) )
# Set up the video bitrate for the informational purpose
if self.vBitRate== 0:
self.vBitRate= vfr.bitrate
# Handle the PTS
rate= float( vfr.rate_base )/ vfr.rate
if d[ 3 ]> 0 and self.lastPTS< d[3]:
# Get the first lowest PTS( we do not have DTS :( )
self.lastPTS= d[3]
self.videoPTS= float( d[ 3 ] ) / 90000
#print 'VPTS:', self.videoPTS, vfr.pict_type, self.getSndLeft()
else:
# We cannot accept PTS, just calculate it
self.videoPTS+= rate
return 0
# No more raw frames to decode
return -2
# ------------------------------------
def _processVideo( self, forced= False ):
while 1:
if len( self.decodedFrames )== 0:
return
vfr, videoPTS= self.decodedFrames[ 0 ]
self.vDelay= videoPTS- self.seekADelta- self._getPTS()
frRate= float( vfr.rate_base )/ vfr.rate
res= self._decodeVideoFrame()
#print '<<', res, self.vDelay, self.frameNum, videoPTS, self._getPTS(), len( self.decodedFrames ), len( self.rawFrames ), self._getSndLeft(), self.aindex, self.isPaused()
#if res== -1 or ( self.snd and self.getSndLeft()< frRate ) or ( res== -2 and self.vDelay> 0 and self.getSndLeft()< self.vDelay and not forced ):
if res== -1 or ( res== -2 and self.vDelay> 0 and self._getSndLeft()< self.vDelay and not forced ):
return
# If audio queue is empty and we still have video frames, add 1 audio frame per every video frame
if self.vDelay> 0 and self._getSndLeft()< 0.01 and self.aindex!= -1 and len( self.aDecodedFrames )== 0 and self._getPTS()> 0:
if self.vDelay> frRate:
self.vDelay= frRate
#print 'Appending dummy audio...', self.vDelay, frRate
self._appendDummyAudio( self.vDelay )
time.sleep( self.vDelay )
self.vDelay= 0
# If delay
#print '!!', self.vDelay, self.frameNum, videoPTS, self._getPTS(), len( self.decodedFrames ), len( self.rawFrames ), self._getSndLeft(), self.seekADelta
if self.vDelay> 0 and self._getSndLeft()> self.vDelay:
time.sleep( self.vDelay / 6 )
#print 'Delay', self.vDelay, self.getSndLeft()
self.vDelay= 0
if self.vDelay< frRate / 4 or forced:
# Remove frame from the queue
del( self.decodedFrames[ 0 ] )
# Get delay for seeking
if self.frameNum== 0 and self.initADelta== -1:
self.initADelta= self._getSndLeft()
# Increase number of frames
self.frameNum+= 1
# Skip frame if no data inside, but assume it was a valid frame though
if vfr.data:
try: self.callback.onVideoReady( vfr )
except: pass
self.vDelay= frRate
# ------------------------------------
def _appendDummyAudio( self, length ):
# Get PCM length of the audio frame
l= self.aSampleRate* self.aChannels* 2* length
self.aDecodedFrames.append( ( '\0'* int( l ), self.aSampleRate, self.aChannels ) )
self._processAudio()
# ------------------------------------
def _processAudioFrame( self, d ):
# Decode audio frame
afr= self.ac.decode( d[ 1 ] )
if afr:
# See if we have to set up the sound
if self.snd== None:
self.aBitRate= afr.bitrate
self.aSampleRate= afr.sample_rate
self.aChannels= afr.channels
try:
# Hardcoded S16 ( 16 bit signed ) for now
#print 'Opening sound', afr.sample_rate, afr.channels, sound.AFMT_S16_LE, self.audioCard
self.snd= sound.Output( afr.sample_rate, afr.channels, sound.AFMT_S16_LE, self.audioCard )
self.resampler= None
except:
try:
# Create a resampler when no multichannel sound is available
self.resampler= sound.Resampler( (afr.sample_rate,afr.channels), (afr.sample_rate,2) )
# Fallback to 2 channels
#print 'Falling back to', afr.sample_rate, 2, sound.AFMT_S16_LE, self.audioCard
self.snd= sound.Output( afr.sample_rate, 2, sound.AFMT_S16_LE, self.audioCard )
except:
self.err.append( sys.exc_info() )
raise
# Calc max buf size for better audio handling
if self.maxBufSize== -1:
self.maxBufSize= self.snd.getSpace()
# Handle the PTS accordingly
snd= self.snd
if d[ 3 ]> 0 and self.aDelta== 0 and snd:
# set correction factor in case of PTS presence
self.aDelta= ( float( d[ 3 ] ) / 90000 )- snd.getPosition()- snd.getLeft()
# Play the raw data if we have it
if len( afr.data )> 0:
# Split the audio data if the size of data chunk larger than the buffer size
data= afr.data
if len( data )> self.maxBufSize:
data= str( data )
while len( data )> self.maxBufSize:
chunk= data[ : self.maxBufSize ]
data= data[ self.maxBufSize: ]
self.aDecodedFrames.append( ( chunk, afr.sample_rate, afr.channels ) )
self.aDecodedFrames.append( ( data, afr.sample_rate, afr.channels ) )
self._processAudio()
# ------------------------------------
def _processAudio( self ):
snd= self.snd
while len( self.aDecodedFrames ) and snd:
# See if we can play sound chunk without clashing with the video frame
if len( self.aDecodedFrames[ 0 ][ 0 ] )> snd.getSpace() and self.vindex!= -1:
break
#print 'player SOUND LEFT:', self.snd.getLeft(), self.snd.getSpace(), self.isPlaying()
if self.isPlaying():
data, sampleRate, channnels= self.aDecodedFrames.pop(0)
if self.callback:
try:
data1= self.callback.onAudioReady( data, sampleRate, channnels )
if data1:
data= data1
except:
pass
# See if we need to resample the audio data
if self.resampler:
data= self.resampler.resample( data )
snd.play( data )
# ------------------------------------
def start( self ):
"""
Start player object. It starts internal loop only, no physical playback is started.
You have to call start only once for the player instance.
If you wish to play multiple files just call startPlayback() subsequently.
"""
if self.ct:
raise 'cannot run another copy of vplayer'
self.exitFlag= 0
self.pictureSize= None
self.ct= thread.start_new_thread( self._readerLoop, () )
# ------------------------------------
def stop( self ):
"""
Stop player object. It stops internmal loop and any playing file.
Once the internal loop is stopped the further playback is not possible until you issue start() again.
"""
self.stopPlayback()
if self.callback:
try: self.callback.removeOverlay()
except: pass
# Turn the flag to exist the main thread
self.exitFlag= EXITING_FLAG
# ------------------------------------
def startPlayback( self, file, format= 'mp3', paused= False ):
"""
Starts playback of the file passed as string or file like object.
Player should already be started otherwise this call has no effect.
If any file is already playing it will stop the playback and start playing the file you pass.
'paused' parameter can specifiy if the playback should not start until unpausePlayback() is called.
'paused' parameter is helpfull when you want to start your playback exactly at a certain time avoiding any delays
caused by the file opening or codec initilizations.
"""
self.stopPlayback()
# Set the new file for playing
self.paused= paused
self.fileFormat= format
self.playingFile= file
try:
self.setVolume( vars.volume )
except:
pass
# ------------------------------------
def stopPlayback( self ):
"""
Stops file playback. If media file is not playing currently, it does nothing.
If file was paused it will unpause it first.
"""
self.playingFile= None
self.unpausePlayback()
if self.snd:
self.snd.stop()
self.snd= None
# --------------------------------------------------------
def pausePlayback( self ):
""" Pause playing the current file """
if self.isPlaying():
self.paused= 1
if self.snd:
self.snd.pause()
# --------------------------------------------------------
def unpausePlayback( self ):
""" Resume playing the current file """
if self.isPlaying():
if self.snd:
self.snd.unpause()
self.paused= 0
# ------------------------------------
def isPaused( self ):
""" Returns whether playback is paused """
return self.paused
# ------------------------------------
def seekTo( self, secs ):
"""
Seeks the file playback position to a given number of seconds from the beginning.
Seek may position not exactly as specified especially in video files.
It will look for a first key frame and start playing from that position.
In some files key frames could resides up to 10 seconds apart.
"""
while self.seek>= 0:
time.sleep( 0.01 )
if secs< 0:
secs= 0
self.seek= secs
# ------------------------------------
def isPlaying( self ):
""" Returns whether playback is active """
return self.playingFile!= None and self.isRunning()
# ------------------------------------
def isRunning( self ):
"""
Returns whether Player object can do the playback.
It will return False after you issue stop()
"""
return self.exitFlag in ( EXITING_FLAG, 0 )
# ------------------------------------
def getError( self ):
"""
Return error list if any
"""
return self.err
# ------------------------------------
def clearError( self ):
"""
Clear all errors
"""
self.err= []
# ------------------------------------
def setLoops( self, loops ):
"""
Set number of loops the player will play current file
"""
self.loops= loops
# ------------------------------------
def setStartTime( self, timePosSec ):
"""
Set start time for the media track to start playing.
Whenever file is played it will start from the timePosSec position in the file.
"""
self.startPos= timePosSec
# ------------------------------------
def setEndTime( self, timePos ):
"""
Set end time for the media track.
Whenever file is reached the endTime it will stop playing.
"""
self.endPos= timePos
# ------------------------------------
def setVolume( self, volume ):
"""
Set volume for the media track to play
volume = [ 0..65535 ]
"""
self.volume= volume
# Asume the very first is the the Master volume
conns= sound.Mixer().getControls()
if len( conns ):
conns[ 0 ][ 'control' ].setValue( self.volume )
# ------------------------------------
def getVolume( self ):
"""
Get volume for the playing media track 0..65535
"""
# Asume the very first is the the Master volume
conns= sound.Mixer().getControls()
if len( conns ):
return conns[ 0 ][ 'control' ].getValue()
return 0
# ------------------------------------
def getPictureSize( self ):
"""
Whenever the file containing video is played, this function returns the actual picture size for the
video part. It may be None if no video is found in the media file.
Note: picture size may be unknown for up to 1 second after you call startPlayback() !
"""
return self.pictureSize
# ------------------------------------
def getLength( self ):
"""
Get length of the media file in seconds
Note: length may be unknown for up to 1 second after you call startPlayback() !
"""
return self.length
# ------------------------------------
def getMetaData( self ):
"""
Get meta data from the media file as dictionary
Note: metadata may be unknown for up to 1 second after you call startPlayback() !
"""
return self.metaData
# ------------------------------------
def getSampleRate( self ):
"""
Get sample rate of the playing file
"""
return self.sampleRate
# ------------------------------------
def getABitRate( self ):
"""
Get bitrate for the audio stream if present
"""
if self.aindex!= -1:
return self.aBitRate
return 0
# ------------------------------------
def getBitRate( self ):
"""
Get bitrate for the full stream
"""
return self.getABitRate()+ self.vBitRate
# ------------------------------------
def getPosition( self ):
"""
Returns current position for the media
"""
if self.isPlaying():
return self._getPTS()
return 0
# ------------------------------------
def _hasQueue( self ):
queue= 0
if self.aindex!= -1:
queue+= len( self.aDecodedFrames )
if self.vindex!= -1:
queue+= len( self.rawFrames )+ len( self.decodedFrames )
return queue> 0
# ------------------------------------
def _getPTS( self ):
if self.aindex== -1:
if not self.aPTS:
self.aPTS= time.time()
return time.time()- self.aPTS
if self.snd== None:
return 0
p= self.snd.getPosition()
return p+ self.aDelta
# ------------------------------------
def _getSndLeft( self ):
if self.snd== None:
if self.aindex== -1:
return 1
else:
return 0
return self.snd.getLeft()
# ------------------------------------
def _readerLoop( self ):
f= None
try:
while self.exitFlag== 0:
if self.playingFile== None:
time.sleep( 0.01 )
continue
self.length= self.frameNum= -1
# Initialize demuxer and read small portion of the file to have more info on the format
self.clearError()
if type( self.playingFile ) in ( str, unicode ):
try:
f= open( self.playingFile, 'rb' )
format= self.playingFile.split( '.' )[ -1 ].lower()
except:
traceback.print_exc()
self.err.append( sys.exc_info() )
self.playingFile= None
continue
else:
format= self.fileFormat
f= self.playingFile
try:
dm= muxer.Demuxer( format )
s= f.read( FILE_CHUNK )
r= dm.parse( s )
except:
traceback.print_exc()
self.err.append( sys.exc_info() )
self.playingFile= None
continue
try: self.metaData= dm.getHeaderInfo()
except: self.metaData= {}
# This seek sets the seeking position already at the desired offset from the beginning
if self.startPos:
self.seekTo( self.startPos )
# Setup video( only first matching stream will be used )
self.clearError()
self.vindex= -1
streams= filter( lambda x: x, dm.streams )
for st in streams:
if st and st[ 'type' ]== muxer.CODEC_TYPE_VIDEO:
self._initVideo( st )
self.vindex= list( streams ).index( st )
break
# Setup audio( only first matching stream will be used )
self.aindex= -1
self.aPTS= None
for st in streams:
if st and st[ 'type' ]== muxer.CODEC_TYPE_AUDIO:
self._initAudio( st )
self.aindex= list( streams ).index( st )
break
# Open current file for playing
currentFile= self.playingFile
if self.vindex>= 0:
self._getVStreamParams( self.vindex, streams[ self.vindex ], r )
self._getStreamLength( format, dm, f, r )
# Play until no exit flag, not eof, no errs and file still the same
while len(s) and len( self.err )== 0 and \
self.exitFlag== 0 and self.playingFile and len( streams ) and \
self.playingFile== currentFile:
if self.isPaused():
time.sleep( PAUSE_SLEEP )
continue
for d in r:
if self.playingFile!= currentFile:
break
# Seeking stuff
if self.seek>= 0:
# Find the file position first
if self.length> 0 and self.fileSize> 0:
#print self.seek, self.length, self.fileSize, ( float( self.seek ) / self.length )* self.fileSize
f.seek( ( float( self.seek ) / self.length )* self.fileSize, 0 )
else:
f.seek( self.seek* self.getBitRate()/ 8, 0 )
#print self.seek, self.getBitRate(), f.tell()
#print 'seek to', self.seek, f.tell()
seek= self.seek
self.aDecodedFrames= []
if self.ac:
self.ac.reset()
self.snd.stop()
self.rawFrames= []
self.decodedFrames= []
if self.vc:
self.vc.reset()
dm.reset()
if self.vindex== -1:
# Seek immediately if only audio stream is available
self.seek= -1
self.aDelta= seek
else:
# Wait for a key video frame to arrive
self.seek= SEEK_IN_PROGRESS
break
# See if we reached the end position of the video clip
if self.endPos and self._getPTS()* 1000> self.endPos:
# Seek at the end and close the reading loop instantly
f.seek( 0, 2 )
break
try:
# Update length if not set already
if self.getLength()== -1 and self.getBitRate()> 0:
# Check file size against length and bitrates
self.length= self.fileSize/ ( self.getBitRate()/ 8 )
# Demux file into streams
if d[ 0 ]== self.vindex:
# Process video frame
seek= self.seek
self._processVideoFrame( d )
if self.seek!= SEEK_IN_PROGRESS and seek== SEEK_IN_PROGRESS:
# If key frame was found, change the time position
self.videoPTS= ( float( f.tell() )/ self.fileSize )* self.length
self.aDelta= self.videoPTS+ self.initADelta
#print '---->position', f.tell(), self.aDelta
elif d[ 0 ]== self.aindex and self.seek!= SEEK_IN_PROGRESS:
# Decode and play audio frame
self._processAudioFrame( d )
except:
traceback.print_exc()
self.err.append( sys.exc_info() )
self.playingFile= None
break
# Read next encoded chunk and demux it
try:
s= f.read( 512 )
r= dm.parse( s )
except:
traceback.print_exc()
self.err.append( sys.exc_info() )
self.playingFile= None
continue
if f: f.close()
# Close current file when error detected
if len( self.err ):
self.stopPlayback()
# Wait until all frames are played
while self._hasQueue()> 0 and self.isPlaying():
self._processVideoFrame( None, True )
self._processAudio()
while self.aindex!= -1 and self._getSndLeft()> 0 and self.isPlaying():
time.sleep( 0.01 )
self._resetAudio()
self._resetVideo()
if self.loops> 0:
self.loops-= 1
continue
# Report the file end
try: f= self.callback.onPlaybackEnd
except: f= None
if f:
f( self )
else:
self.playingFile= None
except:
traceback.print_exc()
self.exitFlag= 1
|