Source code for csengine.csmediafile
import subprocess
from csengine.utility import DebugUtility, OSUtility
from csengine import globals
from threading import Thread, Event
import multitimer
import copy
import numpy as np
[docs]
class csmediafile:
filename = None
#Processes
videoProcess = None
audioProcess = None
#Threads
videoReader = None
audioReader = None
videoStdErrReader = None
audioStdErrReader = None
#Pipes - not in use anymore
audioPipe = None
videoPipe = None
#Events
videoSyncEvent = None
videoReadyEvent = None
audioReadyEvent = None
pauseEvent = None
#Timers
videoSyncTimer = None
#Audio Output
outputStream = None
pa = None
audioCallback = None
#Flags
hasAudio = False
hasVideo = False
enableAudio = True
enableVideo = True
shutdown = False
isPaused = False
isPlaying = False
CHUNK_BYTES = 1024
EMPTY_AUDIO = np.zeros((5,), dtype=np.int8).tobytes()
#Media Parameters
width = 0
height = 0
framerate = 0
totalFrames = 0
totalSamples = 0
#duration
durationStr = ""
h = 0
min = 0
sec = 0
msec = 0
#Can be used to sync up the Audio with the Video - Dispose an amount of Video Frames before starting the stream
DISPOSE_FRAMES = 0
#Can be used to sync up the Audio with the Video - Send a certain amount of Empty frames before starting the Audio
DELAY_CHUNKS = 0#12
arraySize = 0 #The size of one frame for read operations - calculated based on the frame size
#This parameter is calculated automatically - if the loaded Video size is bigger than 640 in width - it will then be scaled down to 640
scale = ""
#Internal parameters
audioVideoSyncFrames = 0
currentAudioFrames = 0
syncCounter = 0
videoFrameCount = 0
#the frame that is availavle and is being updated in order to use it
currentFrame = None
def __init__(self, filename):
if OSUtility.GetPlatformID() == 'MAC':
self.EXC_STR = "ffmpeg/ffmpeg"
else:
self.EXC_STR = "ffmpeg\\ffmpeg.exe"
self.filename = filename
self.getMediaInformation()
#setup video ready event
self.videoReadyEvent = Event()
self.videoReadyEvent.clear()
#setup Audio Ready Event
self.audioReadyEvent = Event()
self.audioReadyEvent.clear()
#setup sync event
self.videoSyncEvent = Event()
self.videoSyncEvent.clear()
#setup Pause Event
self.pauseEvent = Event()
self.pauseEvent.set()
#returns the actual position of the Playback as percent
[docs]
def getVideoPosition(self):
try:
if self.hasVideo:
return int((self.videoFrameCount / self.totalFrames)*100)
if self.hasAudio:
return int((self.currentAudioFrames/self.totalSamples)*50)
except:
return 0
return 0
[docs]
def getDurationMsec(self):
return int(((((self.h * 60) + self.min) * 60) + self.sec) * 1000 + self.msec)
[docs]
def calcDurationString(self, msec):
h = int(msec//(1000*60*60))
msec -= h*(1000*60*60)
if len(str(h)) == 1:
h = str("0" + str(h))
min = int(msec//(1000*60))
msec -= min * (1000 * 60)
if len(str(min)) == 1:
min = str("0" + str(min))
sec = int(msec//1000)
msec -= sec*1000
if len(str(sec)) == 1:
sec = str("0" + str(sec))
msec = str(int(msec))
while len(msec) < 3:
msec = "0" + msec
ret = str(h) + ":" + str(min) + ":" + str(sec) + "." + str(msec)
# DebugUtility.Debug("calculated time String: " + ret)
return ret
[docs]
def stop(self):
if self.shutdown:
return
self.shutdown = True
if self.videoSyncTimer:
self.videoSyncTimer.stop()
self.videoSyncTimer = None
#allow the VideoReaderThread to Exit
self.videoSyncEvent.set()
self.audioReadyEvent.set()
self.videoReadyEvent.set()
self.pauseEvent.set()
# DebugUtility.Debug("Waiting for VideoThread to stop")
try:
self.videoReader.join()
except Exception:
# DebugUtility.Debug("except VideoThread to stop")
pass
# DebugUtility.Debug("Waiting for AudioThread to stop")
try:
self.audioReader.join()
except Exception:
# DebugUtility.Debug("except AudioThread to stop")
pass
# DebugUtility.Debug("Threads stopped")
self.currentFrame = 0
self.currentAudioFrames = 0
#make sure AudioReaderThread Exits - close pipe
# try:
# self.audioPipe.close()
# globals.AUDIO_THREAD.disconnectPort(self.audioPipe)
# except Exception:
# pass
#
#kill video transcoder ffmpeg subprocess (and childrenn)
#TODO: kill the process on Mac as well
try:
subprocess.Popen("TASKKILL /F /PID {pid} /T".format(pid=self.videoProcess.pid))
except Exception:
pass
self.videoProcess = None
# kill Audio transcoder ffmpeg subprocess (and childrenn)
try:
subprocess.Popen("TASKKILL /F /PID {pid} /T".format(pid=self.audioProcess.pid))
except Exception:
pass
self.audioProcess = None
self.isPlaying = False
[docs]
def pause(self):
self.pauseEvent.clear()
self.isPaused = True
if self.videoSyncTimer:
self.videoSyncTimer.stop()
[docs]
def readAudioStderr(self):
while not self.shutdown:
try:
line = self.audioProcess.stderr.readline()
if line:
#We might not want to print it into our logs but we defenitely need to read it!
DebugUtility.Debug("FFMPEGA:" + str(line))
pass
except:
break
[docs]
def readVideoStderr(self):
while not self.shutdown:
try:
line = self.videoProcess.stderr.readline()
if line:
# We might not want to print it into our Debugs but we defenitely need to read it!
DebugUtility.Debug("FFMPEGV:" + str(line))
except:
break
[docs]
def readVideo(self):
try:
buf = self.videoProcess.stdout.read(self.arraySize)
except:
return
self.audioReadyEvent.wait()
#import time
#time.sleep(0.5)
self.videoReadyEvent.set()
while not self.shutdown:
#self.readerEvent.wait()
#self.readerEvent.clear()
try:
buf = self.videoProcess.stdout.read(self.arraySize)
except:
break
if not buf:
break
while len(buf) < self.arraySize:
lenToRead = self.arraySize - len(buf)
try:
buf += self.videoProcess.stdout.read(lenToRead)
except:
break
#Write the buffer that we have just received into our output buffer
self.currentFrame = buf
self.videoFrameCount += 1
#synchronisation with the AudioStream!!
self.videoSyncEvent.wait()
self.videoSyncEvent.clear()
self.syncCounter -= 1
if self.syncCounter > 0:
self.videoSyncEvent.set()
self.stop()
self.currentFrame = None
# DebugUtility.Debug("EXIT from Video Reader!!!!")
[docs]
def readAudio(self):
if not self.audioCallback:
return
audioSyncCount = 0
try:
buf = self.audioProcess.stdout.read(2048)
except:
return
self.audioReadyEvent.set()
self.videoReadyEvent.wait()
for i in range(self.DELAY_CHUNKS):
self.audioCallback(self.EMPTY_AUDIO)
self.videoSyncEvent.set()
self.syncCounter += 1
while not self.shutdown:
try:
buf = self.audioProcess.stdout.read(2048)
if buf:
self.audioCallback(buf)
else:
break
self.currentAudioFrames += len(buf)
#in case Playback is Paused
self.pauseEvent.wait()
#check if we will trigger a new video frame based on the amount of transmitted data in the audio stream
#this is the essential mechanism which syncs audio and video!
if self.currentAudioFrames > self.audioVideoSyncFrames:
if audioSyncCount < self.DISPOSE_FRAMES:
audioSyncCount +=1
else:
self.videoSyncEvent.set()
self.syncCounter += 1
self.currentAudioFrames -= self.audioVideoSyncFrames
except:
break
self.stop()
# DebugUtility.Debug("EXIT from Audio Reader!!!!")
[docs]
def playFrom(self, msec):
#get the duration string
start = self.calcDurationString(msec)
#setup ffmpeg seek parameters
seekstring = " -ss " + start.split(".")[0] + " -to "+ str(int(self.getDurationMsec()//1000))
#calculate start position
self.videoFrameCount = int((msec / 1000) * self.framerate)
#start the file from the given position
self.play(seekstring)
[docs]
def play(self, seek = ""):
self.pauseEvent.set()
if self.isPaused:
self.isPaused = False
if self.videoSyncTimer:
self.videoSyncTimer.start()
return
if self.isPlaying:
return
self.isPaused = False
self.shutdown = False
self.syncCounter = 0
if seek == "":
self.videoFrameCount = 0
#if both Audio and Video are available make sure to sync them on startup
if self.hasAudio and self.enableAudio and self.hasVideo and self.enableVideo:
self.videoReadyEvent.clear()
self.audioReadyEvent.clear()
else:
self.videoReadyEvent.set()
self.audioReadyEvent.set()
if self.hasAudio and self.enableAudio:
#self.audioPipe = globals.AUDIO_THREAD.createAudioOutput()
import time
#time.sleep(0.5)
#Audio Streaming Subprocess
self.audioProcess = subprocess.Popen(self.EXC_STR #run ffmpeg executable
+ seek #insert seek parameter in case we do not want to start the video from the beginning
+ " -i \"" + str(self.filename) + "\"" #set input filename to load
+ " -vn" #strip video from stream
#+ " -vsync 1"
+ " -r 24"
+ " -map 0" #use first available audio stream
+ " -c:a pcm_s16le" #define encoder codec - use only left channel(we want mono)
+ " -f s16le" #define audio output format - pcm16le(for windows)
+ " -af \"aresample=44100\"" #resample audio stream if input is not alread. 44.1 kSamples
+ " -ar 44100" #define output Audio Format - 44.1 kSamples
+ " -ac 1" #set output format to mono
+ " pipe: ", #set Output - We want it to be a pipe
#+ self.audioPipe.pipename, #Output pipe to write to
shell=True,
stderr=subprocess.PIPE, stdout=subprocess.PIPE)
Thread(target=self.readAudioStderr, daemon=True).start()
self.audioReader = Thread(target=self.readAudio, daemon=True)
self.audioReader.start()
#Video Streaming Subprocess
if self.hasVideo:
self.videoProcess = subprocess.Popen(self.EXC_STR #run ffmpeg executable
+ " -i \"" + str(self.filename) + "\"" #set input filename to load
+ " -an" # strip video from stream
+ " -preset ultrafast" #use the fastest codec option available - realtime playing is more important than quality (only makes a different if video is being scaled anyway)
+ seek # insert seek parameter in case we do not want to start the video from the beginning
+ " -map 0" #use input file 0 and stream 0 (if multiple streams available)
+ " -c:v rawvideo" #specify video encoder - rawvideo
+ self.scale #insert scale if available
+ " -f rawvideo" # specify output format - rawvideo
+ " -pix_fmt rgb24" # set the pixel fmt to rgb24
+ " pipe: ", #set Output - We want it to be a pipe
#+ self.videoPipe.pipename, #Output pipe to write to
shell=True,
stderr=subprocess.PIPE, stdout=subprocess.PIPE)
#time.sleep(1)
Thread(target=self.readVideoStderr,daemon=True).start()
self.videoReader = Thread(target=self.readVideo, daemon=True)
self.videoReader.start()
#Setup Video Sync timer in case there is no available Audio Stream
if self.hasVideo and self.enableVideo:
if not self.hasAudio or not self.enableAudio:
self.videoSyncTimer = multitimer.MultiTimer(interval=(1/self.framerate), function=self.videoSyncEvent.set)
self.videoSyncTimer.start()
#Thread(target=self.testThread).start()
self.isPlaying = True
return
[docs]
def testThread(self):
import time
while not self.shutdown:
# DebugUtility.Debug("FRAME DIFF: " + str(self.syncCounter))
time.sleep(1)
#open the file with ffmpeg and get the required Media Information
[docs]
def getMediaInformation(self):
self.infoProcess = subprocess.Popen(self.EXC_STR + " -i \"" + str(self.filename) + "\"", stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True, bufsize=10 ** 8)
ret = " "
chatter = b""
while len(ret) > 0:
ret = self.infoProcess.stderr.read(1024)
chatter += ret
combined = chatter.decode("utf-8").split("\n")
resolutionFound = False
framerateFound = False
#"-ss 00:00:23.000 - to 60"
width = 0
height = 0
# get resolution
for line in combined:
if "Duration: " in line:
durationLine = line.replace(","," ").split(" ")
durDiscovered = False
for frag in durationLine:
if durDiscovered:
self.duration = frag.replace(" ", "").replace("\n", "").replace("\r", "")
try:
comps = self.duration.split(":")
self.h = int(comps[0])
self.min = int(comps[1])
fract = comps[2].split(".")
self.sec = int(fract[0])
if len(fract) > 1:
msec = fract[1]
while len(msec) < 3:
msec += "0"
self.msec = int(msec)
except:
return False
# DebugUtility.Debug("Found Duration: " + str(self.duration) + " h: " + str(self.h) + " min: "+ str(self.min) + " sec: " + str(self.sec) + " msec: " + str(self.msec))
break
if "Duration:" in frag:
durDiscovered = True
# DebugUtility.Debug(line)
if "Stream" in line:
if "Video" in line:
self.hasVideo = True
if not framerateFound:
stringsplit = line.replace(" ", ",").split(",")
lastParam = ""
for param in stringsplit:
if "fps" in param:
try:
self.framerate = float(lastParam)
# DebugUtility.Debug("found framerate: " + str(self.framerate))
framerateFound = True
except Exception:
pass
if "tbr" in param:
try:
self.framerate = float(lastParam)
# DebugUtility.Debug("found framerate: " + str(self.framerate))
framerateFound = True
except Exception:
pass
lastParam = param
if not resolutionFound:
stringsplit = line.replace(" ", ",").split(",")
for param in stringsplit:
resol = param.replace(" ", "")
resArray = resol.split("x")
if len(resArray) != 2:
continue
else:
try:
width = int(resArray[0])
height = int(resArray[1])
resolutionFound = True
break
except:
continue
if "Audio" in line:
self.hasAudio = True
if width != 0 and height != 0:
# DebugUtility.Debug("Found Resolution : " + str(width) + " " + str(height))
self.width = width
self.height = height
else:
# DebugUtility.Debug("Video Resolution not found")
self.hasVideo = False
#calculate audio-video sync sample size(based on audio sample rate - 44.1 KSamples)
try:
self.audioVideoSyncFrames = int((44100 * 2) // self.framerate)
## DebugUtility.Debug("SYNC FRAMES: " + str(self.audioVideoSyncFrames))
#calculate total number of frames for status information
self.totalFrames = int((self.getDurationMsec() / 1000) * self.framerate)
#video is bigger than the screen - scaling down to 640 width
self.scale = ""
if width > 640:
self.height = int(round((640 / width) * height))
self.width = 640
self.scale = " -vf scale=" + str(self.width)+":"+str(self.height)
self.arraySize = self.width * self.height * 3
except:
self.hasVideo = False
self.totalSamples = int((self.getDurationMsec() / 1000) * 44100)