def _profileFromFilename(self, filename): result = re.match(self.profilepattern, filename) if result: debug.log("Profile=%s (filename=%s, pattern=%s)" % (result.group(1), filename, self.profilepattern)) return result.group(1) else: exit("Error: Can't extract profile from filename=%s (pattern=%s)" % (filename, self.profilepattern))
def _getStartNumberFromFilename(self, filename): result = re.match(self.numberpattern, filename) if result: debug.log("StartNumber=%s (filename=%s, pattern=%s)" % (result.group(1), filename, self.numberpattern)) return result.group(1) debug.log( "Warning: Can't extract start number from filename %s, using 0 (pattern=%s)" % (filename, self.numberpattern)) return '0'
def tsremux(tsfile, outdir, filename, starttime): audiofile = '%s/audio-%s' % (outdir, filename) videofile = '%s/video-%s' % (outdir, filename) debug.log("Remuxing %s to %s and %s" % (tsfile, audiofile, videofile)) tmpaudio = tempfile.NamedTemporaryFile(dir='/tmp/', suffix='.mp4') tmpvideo = tempfile.NamedTemporaryFile(dir='/tmp/', suffix='.mp4') FFMpegCommand(tsfile, tmpaudio.name, '-y -bsf:a aac_adtstoasc -acodec copy -vn') FFMpegCommand(tsfile, tmpvideo.name, '-y -vcodec copy -an') Mp4Fragment(tmpaudio.name, audiofile, starttime) Mp4Fragment(tmpvideo.name, videofile, starttime)
def tsremux(tsfile, outdir, filename, starttime): audiofile = '%s/audio-%s' % (outdir, filename) videofile = '%s/video-%s' % (outdir, filename) debug.log("Remuxing %s to %s and %s" % (tsfile, audiofile, videofile)) tmpaudio = tempfile.NamedTemporaryFile(dir='/tmp/', suffix='.mp4') tmpvideo = tempfile.NamedTemporaryFile(dir='/tmp/', suffix='.mp4') FFMpegCommand(tsfile, tmpaudio.name, '-y -bsf:a aac_adtstoasc -acodec copy -vn') FFMpegCommand(tsfile, tmpvideo.name, '-y -vcodec copy -an') Mp4Fragment(tmpaudio.name, audiofile, starttime) Mp4Fragment(tmpvideo.name, videofile, starttime)
def save(self): obj = {} obj['timebase'] = self.timebase if self.prevSplitTS != None: obj['prevsplit'] = self.prevSplitTS if self.nextSplitTS != None: obj['nextsplit'] = self.nextSplitTS with open(self.filename, 'w+') as f: f.seek(0) f.write(json.dumps(obj, indent=4)) f.truncate() debug.log('Saved context %s to %s' % (obj, self.filename))
def restore(self): debug.log('Restoring context from %s' % self.filename) if os.path.isfile(self.filename): with open(self.filename, 'r+') as f: data = f.read() obj = json.loads(data) self.timebase = obj['timebase'] if 'prevsplit' in obj: self.prevSplitTS = obj['prevsplit'] if 'nextsplit' in obj: self.nextSplitTS = obj['nextsplit'] debug.log('Context: %s' % self)
def runcmd(cmd, name): debug.log('COMMAND: %s' % cmd) try: FNULL = open(os.devnull, 'w') if debug.doDebug: return subprocess.call(cmd) else: return subprocess.call(cmd, stdout=FNULL, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: message = "binary tool failed with error %d" % e.returncode raise Exception(message) except OSError as e: raise Exception('Command %s not found, ensure that it is in your path' % name)
def runcmd(cmd, name): debug.log('COMMAND: %s' % cmd) try: FNULL = open(os.devnull, 'w') if debug.doDebug: return subprocess.call(cmd) else: return subprocess.call(cmd, stdout=FNULL, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: message = "binary tool failed with error %d" % e.returncode raise Exception(message) except OSError as e: raise Exception( 'Command %s not found, ensure that it is in your path' % name)
def _parseMaster(self, variant): debug.log("Parsing master playlist") for playlist in variant.playlists: stream = playlist.stream_info (video_codec, audio_codec) = stream.codecs.split(',') profile = self._profileFromFilename(playlist.uri) profilemetadata = { 'profile': profile, 'videocodec': video_codec, 'audiocodec': audio_codec, 'stream': stream } self.profiles.append(profilemetadata) self._initiatePeriod(self.getPeriod(self.currentPeriodIdx), self.profiles)
def load(self): self.context.restore() debug.log("Loading playlist: ", self.playlistlocator) m3u8_obj = m3u8.load(self.playlistlocator) if m3u8_obj.is_variant: if m3u8_obj.playlist_type == "VOD": raise Exception("VOD playlists not yet supported") self._parseMaster(m3u8_obj) else: raise Exception( "Can only create DASH manifest from an HLS master playlist") p = m3u8_obj.playlists[0] debug.log("Loading playlist: ", self.baseurl + p.uri) self._parsePlaylist(m3u8.load(self.baseurl + p.uri)) for per in self.getAllPeriods(): debug.log("Audio: ", per.as_audio) debug.log("Video: ", per.as_video) self.context.save()
def _parsePlaylist(self, playlist): debug.log("Splicing enabled=%s" % self.splitperiod) self.maxSegmentDuration = playlist.target_duration isFirstInPeriod = True isFirst = True doSplit = False eventid = 1 offset = 0.0 state = 'initial' isFirstSplit = True lastnumber = None periodid = 'UNDEF' for seg in playlist.segments: if state == 'initial': if seg.cue_out == True: state = 'insidecue' else: state = 'outsidecue' elif state == 'outsidecue': if seg.cue_out == True: state = 'insidecue' if not isFirst: doSplit = True elif state == 'insidecue': if seg.cue_out == False: state = 'outsidecue' if not isFirst: doSplit = True #debug.log("[%s][P%d]: %s" % (state, self.currentPeriodIdx, seg.uri)) if self.splitperiod and doSplit: debug.log("-- Split period before %s" % seg.uri) self.currentPeriodIdx = self.currentPeriodIdx + 1 newperiod = Period("P%s" % self._getStartNumberFromFilename(seg.uri)) self._initiatePeriod(newperiod, self.profiles) self.appendPeriod(newperiod) isFirstInPeriod = True doSplit = False duration = float(seg.duration) videoseg = MPDRepresentation.Segment(duration, isFirstInPeriod) audioseg = MPDRepresentation.Segment(duration, isFirstInPeriod) period = self.getPeriod(self.currentPeriodIdx) period.getAdaptationSetVideo().addSegment(videoseg) period.getAdaptationSetAudio().addSegment(audioseg) period.increaseDuration(duration) offset += duration if isFirstInPeriod: # Add EventStream to place SCTE35 metadata debug.log("SCTE35:%s (%s, %s)" % (seg.scte35, seg.cue_out, state)) if state == 'insidecue' and seg.cue_out == True: period.addSCTE35Splice(eventid, seg.scte35_duration, seg.scte35) eventid = eventid + 1 # Obtain the start time for the first segment in this period firstStartTimeInPeriod = self._getStartTimeFromFile( seg.base_uri + seg.uri) firstStartTimeInPeriodTicks = int( float(firstStartTimeInPeriod) * self.context.getTimeBase()) # Determine the period ID if isFirst == True: debug.log('firstStartTimeInPeriod=%d, prevsplit=%d' % (firstStartTimeInPeriodTicks, self.context.getPrevSplit())) # Store the first segment start time in this manifest # to be able to calculate the MPD availability start time self.firstSegmentStartTime = firstStartTimeInPeriod # As this is the very first period and we then need to determine # whether the first segment belongs to a period created # in previous manifest so the correct period id is set if self.context.getPrevSplit() == 0: # We have no information of previous split so use # the start time in this period as period id # and save it for later use self.context.setPrevSplit(firstStartTimeInPeriod) periodid = self.context.getPrevSplit() elif firstStartTimeInPeriodTicks < 0: # Start time for a segment can actually be negative. No # good way to handle it but as long as it is increasing # it will eventually be back to normal periodid = firstStartTimeInPeriodTicks self.context.setPrevSplit(firstStartTimeInPeriod) elif firstStartTimeInPeriodTicks >= self.context.getPrevSplit( ): if self.context.getNextSplit() == 0: periodid = self.context.getPrevSplit() elif firstStartTimeInPeriodTicks < self.context.getNextSplit( ): # Start time for the first segment in this period # is still before the next split and we should use # period id belonging to previous manifest periodid = self.context.getPrevSplit() else: # Start time for the first segment in this period # belongs to a new period unless this is the only period # in this manifest and is actually the last split if self.context.getNextSplit( ) < self.context.getPrevSplit(): period = self.context.getPrevSplit() else: periodid = firstStartTimeInPeriodTicks self.context.setPrevSplit( firstStartTimeInPeriod) elif firstStartTimeInPeriodTicks < self.context.getPrevSplit( ): # If start time of first segment is smaller than ts of # last split a segment time stamp reset / overflow must have # occured periodid = firstStartTimeInPeriodTicks self.context.setPrevSplit(firstStartTimeInPeriod) else: # Start time for the first segment in this period after a split # is the period id periodid = firstStartTimeInPeriodTicks if isFirstSplit == True: # Save the segment start time for the first split self.context.setNextSplit(firstStartTimeInPeriod) isFirstSplit = False period.setPeriodId(periodid) # Set period start time periodstartsec = float(periodid / self.context.getTimeBase()) period.setPeriodStart(periodstartsec) # Set segment start time and start number for the video and audio segments videoseg.setStartTime(firstStartTimeInPeriod) audioseg.setStartTime(firstStartTimeInPeriod) as_audio = period.getAdaptationSetAudio() as_video = period.getAdaptationSetVideo() as_video.setStartNumber( self._getStartNumberFromFilename(seg.uri)) as_video.setStartTime(periodstartsec) as_audio.setStartNumber( self._getStartNumberFromFilename(seg.uri)) as_audio.setStartTime(periodstartsec) isFirstInPeriod = False isFirst = False if self.context.getNextSplit() < self.context.getPrevSplit(): # No new split in this manifest, last split is the current one self.context.resetNextSplit() allperiods = self.getAllPeriods() lastperiod = allperiods[len(allperiods) - 1] lastperiod.setAsLastPeriod()