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()
[docs] def getCurrentFrame(self): return self.currentFrame
#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)