def insert_segmentbase(period, presentation_time_offset): "Insert SegmentBase element." segmentbase_elem = ElementTree.Element(add_ns('SegmentBase')) if presentation_time_offset != 0: segmentbase_elem.set('presentationTimeOffset', str(presentation_time_offset)) period.insert(0, segmentbase_elem)
def insert_service_description(self, mpd, pos): sd_elem = ElementTree.Element(add_ns('ServiceDescription')) sd_elem.set("id", "0") sd_elem.text = "\n" lat_elem = ElementTree.Element(add_ns('Latency')) lat_elem.set("min", "2000") lat_elem.set("max", "6000") lat_elem.set("target", "4000") lat_elem.set("referenceId", "0") lat_elem.tail = "\n" sd_elem.insert(0, lat_elem) pr_elem = ElementTree.Element(add_ns('PlaybackRate')) pr_elem.set("min", "0.96") pr_elem.set("max", "1.04") pr_elem.tail = "\n" sd_elem.insert(1, pr_elem) sd_elem.tail = "\n" mpd.insert(pos, sd_elem)
def generate_s_elem(self, start_time, duration, repeat): "Generate the S elements for the SegmentTimeline." s_elem = ElementTree.Element(add_ns('S')) if start_time is not None: s_elem.set("t", str(start_time)) s_elem.set("d", str(duration)) if repeat > 0: s_elem.set('r', str(repeat)) s_elem.tail = "\n" return s_elem
def insert_producer_reference(self, ad_set, pos): prt_elem = ElementTree.Element(add_ns('ProducerReferenceTime')) prt_elem.set("id", "0") prt_elem.set("type", "encoder") prt_elem.set("wallClockTime", "1970-01-01T00:00:00") prt_elem.set("presentationTime", "0") utc_elem = self.create_descriptor_elem( 'UTCTiming', 'urn:mpeg:dash:utc:http-iso:2014', UTC_TIMING_HTTP_SERVER) prt_elem.insert(0, utc_elem) prt_elem.text = "\n" prt_elem.tail = "\n" ad_set.insert(pos, prt_elem)
def insert_baseurl(self, mpd, pos, new_baseurl, new_ato, new_atc): "Create and insert a new <BaseURL> element." baseurl_elem = ElementTree.Element(add_ns('BaseURL')) baseurl_elem.text = new_baseurl baseurl_elem.tail = "\n" if float(new_ato) == -1: self.insert_ato(baseurl_elem, 'INF') elif float( new_ato) > 0: # don't add this attribute when the value is 0 self.insert_ato(baseurl_elem, new_ato) if new_atc in ('False', 'false', '0'): baseurl_elem.set('availabilityTimeComplete', new_atc) mpd.insert(pos, baseurl_elem)
def create_descriptor_elem(self, name, scheme_id_uri, value=None, elem_id=None, messageData=None): "Create an element of DescriptorType." elem = ElementTree.Element(add_ns(name)) elem.set("schemeIdUri", scheme_id_uri) if value: elem.set("value", value) if elem_id: elem.set("id", elem_id) if messageData: elem.set("messageData", messageData) elem.tail = "\n" return elem
def process_mpd(self, mpd, mpd_data): """Process the root element (MPD)""" assert mpd.tag == add_ns('MPD') mpd.set('type', mpd_data.get('type', 'dynamic')) if self.scte35_present: old_profiles = mpd.get('profiles') if not old_profiles.find(scte35.PROFILE) >= 0: new_profiles = old_profiles + "," + scte35.PROFILE mpd.set('profiles', new_profiles) if self.segtimelineloss: old_profiles = mpd.get('profiles') if old_profiles.find("dash-if-simple") >= 0: new_profiles = old_profiles.replace("dash-if-simple", "dash-if-main") mpd.set('profiles', new_profiles) if 'add_profiles' in mpd_data: profiles = mpd.get('profiles').split(",") for prof in mpd_data['add_profiles']: if prof not in profiles: profiles.append(prof) mpd.set('profiles', ",".join(profiles)) key_list = [ 'availabilityStartTime', 'availabilityEndTime', 'timeShiftBufferDepth', 'minimumUpdatePeriod', 'maxSegmentDuration', 'mediaPresentationDuration', 'suggestedPresentationDelay' ] if mpd_data.get('type', 'dynamic') == 'static': key_list.remove('minimumUpdatePeriod') if (mpd_data.get('type', 'dynamic') == 'static' or mpd_data.get('mediaPresentationDuration')): key_list.remove('timeShiftBufferDepth') set_values_from_dict(mpd, key_list, mpd_data) if 'mediaPresentationDuration' in mpd.attrib and 'mediaPresentationDuration' not in mpd_data: del mpd.attrib['mediaPresentationDuration'] mpd.set('publishTime', make_timestamp(self.mpd_proc_cfg['now']) ) # TODO Correlate time with change in MPD mpd.set('id', 'Config part of url maybe?') if self.segtimeline or self.segtimeline_nr: if 'maxSegmentDuration' in mpd.attrib: del mpd.attrib['maxSegmentDuration'] if mpd_data.get('type', 'dynamic') != 'static': mpd.set('minimumUpdatePeriod', "PT0S")
def update_periods(self, mpd, period_data, offset_at_period_level, ll_data): "Update periods to provide appropriate values." # pylint: disable = too-many-statements def set_attribs(elem, keys, data): "Set element attributes from data." for key in keys: if key in data: if key == "presentationTimeOffset" and str( data[key]) == "0": # Remove default value if key in elem: del elem[key] continue elem.set(key, str(data[key])) def remove_attribs(elem, keys): "Remove attributes from elem." for key in keys: if key in elem.attrib: del elem.attrib[key] def insert_segmentbase(period, presentation_time_offset): "Insert SegmentBase element." segmentbase_elem = ElementTree.Element(add_ns('SegmentBase')) if presentation_time_offset != 0: segmentbase_elem.set('presentationTimeOffset', str(presentation_time_offset)) period.insert(0, segmentbase_elem) def create_inband_scte35stream_elem(): "Create an InbandEventStream element for SCTE-35." return self.create_descriptor_elem("InbandEventStream", scte35.SCHEME_ID_URI, value=str(scte35.PID)) def create_inband_stream_elem(): """Create an InbandEventStream element for signalling emsg in Rep when encoder fails to generate new segments IOP 4.11.4.3 scenario.""" return self.create_descriptor_elem("InbandEventStream", "urn:mpeg:dash:event:2012", value=str(1)) def create_inline_mpdcallback_elem(BaseURLSegmented): "Create an EventStream element for MPD Callback." return self.create_descriptor_elem( "EventStream", "urn:mpeg:dash:event:callback:2015", value=str(1), elem_id=None, messageData=BaseURLSegmented) if self.segtimeline or self.segtimeline_nr: segtimeline_generators = {} for content_type in ('video', 'audio'): segtimeline_generators[ content_type] = SegmentTimeLineGenerator( self.cfg.media_data[content_type], self.cfg) periods = mpd.findall(add_ns('Period')) BaseURL = mpd.findall(add_ns('BaseURL')) if len(BaseURL) > 0: BaseURLParts = BaseURL[0].text.split('/') if len(BaseURLParts) > 3: BaseURLSegmented = BaseURLParts[0] + '//' + BaseURLParts[ 2] + '/' + BaseURLParts[3] + '/mpdcallback/' # From the Base URL last_period_id = '-1' for (period, pdata) in zip(periods, period_data): set_attribs(period, ('id', 'start'), pdata) if 'etpDuration' in pdata: period.set('duration', "PT%dS" % pdata['etpDuration']) if 'periodDuration' in pdata: period.set('duration', pdata['periodDuration']) segmenttemplate_attribs = ['startNumber'] pto = pdata['presentationTimeOffset'] if pto: if offset_at_period_level: insert_segmentbase(period, pto) else: segmenttemplate_attribs.append('presentationTimeOffset') if 'mpdCallback' in pdata: # Add the mpdCallback element only if the flag is raised. mpdcallback_elem = create_inline_mpdcallback_elem( BaseURLSegmented) period.insert(0, mpdcallback_elem) adaptation_sets = period.findall(add_ns('AdaptationSet')) for ad_set in adaptation_sets: ad_pos = 0 content_type = ad_set.get('contentType') if self.emsg_last_seg: inband_event_elem = create_inband_stream_elem() ad_set.insert(0, inband_event_elem) if content_type == 'video' and self.scte35_present: scte35_elem = create_inband_scte35stream_elem() ad_set.insert(0, scte35_elem) ad_pos += 1 if self.continuous and last_period_id != '-1': supplementalprop_elem = self.create_descriptor_elem( "SupplementalProperty", "urn:mpeg:dash:period_continuity:2014", last_period_id) ad_set.insert(ad_pos, supplementalprop_elem) if ll_data: self.insert_producer_reference(ad_set, ad_pos) seg_templates = ad_set.findall(add_ns('SegmentTemplate')) for seg_template in seg_templates: set_attribs(seg_template, segmenttemplate_attribs, pdata) if ll_data: set_attribs(seg_template, ('availabilityTimeOffset', 'availabilityTimeComplete'), ll_data) if pdata.get('startNumber') == '-1': # Default to 1 remove_attribs(seg_template, ['startNumber']) if self.segtimeline or self.segtimeline_nr: # add SegmentTimeline block in SegmentTemplate with timescale and window. segtime_gen = segtimeline_generators[content_type] now = self.mpd_proc_cfg['now'] tsbd = self.cfg.timeshift_buffer_depth_in_s ast = self.cfg.availability_start_time_in_s start_time = max(ast + pdata['start_s'], now - tsbd) if 'period_duration_s' in pdata: end_time = min( ast + pdata['start_s'] + pdata['period_duration_s'], now) else: end_time = now start_time -= self.cfg.availability_start_time_in_s end_time -= self.cfg.availability_start_time_in_s use_closest = False if self.cfg.stop_time and self.cfg.timeoffset == 0: start_time = self.cfg.start_time end_time = min(now, self.cfg.stop_time) use_closest = True seg_timeline = segtime_gen.create_segtimeline( start_time, end_time, use_closest) remove_attribs(seg_template, ['duration']) seg_template.set( 'timescale', str(self.cfg.media_data[content_type] ['timescale'])) if pto != "0" and not offset_at_period_level: # rescale presentationTimeOffset based on the local timescale seg_template.set( 'presentationTimeOffset', str( int(pto) * int(self.cfg.media_data[content_type] ['timescale']))) media_template = seg_template.attrib['media'] if self.segtimeline: media_template = media_template.replace( '$Number$', 't$Time$') remove_attribs(seg_template, ['startNumber']) elif self.segtimeline_nr: # Set number to the first number listed set_attribs( seg_template, ('startNumber', ), {'startNumber': segtime_gen.start_number}) seg_template.set('media', media_template) seg_template.text = "\n" seg_template.insert(0, seg_timeline) last_period_id = pdata.get('id')
def insert_location(self, mpd, pos, location_url): location_elem = ElementTree.Element(add_ns('Location')) location_elem.text = location_url location_elem.tail = "\n" mpd.insert(pos, location_elem)
def process_mpd_children(self, mpd, data, period_data, ll_data): """Process the children of the MPD element. They should be in order ProgramInformation, BaseURL, Location, ServiceDescription, Period, UTCTiming, Metrics.""" ato = 0 atc = 'true' if 'availabilityTimeOffset' in data: ato = data['availabilityTimeOffset'] if 'availabilityTimeComplete' in data: atc = data['availabilityTimeComplete'] children = list(mpd) pos = 0 for child in children: if child.tag != add_ns('ProgramInformation'): break pos += 1 next_child = list(mpd)[pos] set_baseurl = SET_BASEURL if self.cfg and self.cfg.add_location: set_baseurl = False # Cannot have both BASEURL and Location if next_child.tag == add_ns('BaseURL'): if 'BaseURL' not in data or not set_baseurl: self.root.remove(next_child) else: self.modify_baseurl(next_child, data['BaseURL']) pos += 1 elif ('BaseURL' in data) and set_baseurl: if 'urls' in data and data[ 'urls']: # check if we have to set multiple URLs url_header, url_body = data['BaseURL'].split('//') url_parts = url_body.split('/') i = -1 for part in url_parts: i += 1 if part.find("_") < 0: # Not a configuration continue cfg_parts = part.split("_", 1) key, _ = cfg_parts if key == "baseurl": url_parts[i] = "" # Remove all the baseurl elements url_parts = [p for p in url_parts if p is not None] for url in data['urls']: url_parts.insert(-1, "baseurl_" + url) self.insert_baseurl( mpd, pos, url_header + "//" + "/".join(url_parts) + "/", ato, atc) del url_parts[-2] pos += 1 else: self.insert_baseurl(mpd, pos, data['BaseURL'], ato, atc) pos += 1 if self.cfg and self.cfg.add_location and self.full_url is not None: loc_url = re.sub(r"/startrel_[-\d]+", "/start_%d" % self.cfg.start_time, self.full_url) loc_url = re.sub(r"/stoprel_[-\d]+", "/stop_%d" % self.cfg.stop_time, loc_url) self.insert_location(mpd, pos, loc_url) pos += 1 if ll_data: self.insert_service_description(mpd, pos) pos += 1 children = list(mpd) for ch_nr in range(pos, len(children)): if children[ch_nr].tag == add_ns("Period"): period = list(mpd)[pos] pos = ch_nr break else: raise MpdModifierError("No period found.") for i in range(1, len(period_data)): new_period = copy.deepcopy(period) mpd.insert(pos + i, new_period) self.insert_utc_timings(mpd, pos + len(period_data)) self.update_periods(mpd, period_data, data['periodOffset'] >= 0, ll_data)
def create_segtimeline(self, start_time, end_time, use_closest=False): "Create and insert a new <SegmentTimeline> element and S entries." seg_timeline = ElementTree.Element(add_ns('SegmentTimeline')) seg_timeline.text = "\n" seg_timeline.tail = "\n" start = start_time * self.timescale end = end_time * self.timescale # The start segment is the latest one that starts before or at start # The end segment is the latest one that ends before or at end. (end_index, end_repeats, end_wraps) = self.find_latest_starting_before(end) if end_index is None: raise SegmentTimeLineGeneratorError("No end_index for %d %d. Before AST" % (start_time, end_time)) end_tics = self.get_seg_endtime(end_wraps, end_index, end_repeats) # print("end_time %d %d" % (end, end_tics)) while end_tics > end: if end_repeats > 0: end_repeats -= 1 # Just move one segment back in the repeat elif end_index > 0: end_index -= 1 end_repeats = self.segtimedata[end_index].repeats else: end_wraps -= 1 end_index = len(self.segtimedata) - 1 end_repeats = self.segtimedata[end_index].repeats if (end_wraps < 0): return (None, None, None) end_tics = self.get_seg_endtime(end_wraps, end_index, end_repeats) # print "end_time2 %d %d %d" % (end, end_tics, (end-end_tics)/(self.timescale*1.0)) # print "end time %d %d %d" % (end_index, end_repeats, end_wraps) if use_closest: result = self.find_closest_start(start) else: result = self.find_latest_starting_before(start) (start_index, start_repeats, start_wraps) = result # print("start %d %d %d" % (start_index, start_repeats, start_wraps)) start_tics = self.get_seg_starttime(start_wraps, start_index, start_repeats) start_tics_end = self.get_seg_starttime(end_wraps, end_index, end_repeats) if (start_tics_end < start_tics): return seg_timeline # Empty timeline in this case # print("start time %d %d %d" % (start_tics, start, start - start_tics)) repeat_index = end_index nr_wraps = end_wraps # Create the S elements in backwards order while repeat_index != start_index or nr_wraps != start_wraps: seg_data = self.segtimedata[repeat_index] # print(repeat_index, start_index, nr_wraps, start_wraps) if repeat_index == end_index: s_elem = self.generate_s_elem(None, seg_data.duration, end_repeats) else: s_elem = self.generate_s_elem(None, seg_data.duration, seg_data.repeats) seg_timeline.insert(0, s_elem) repeat_index -= 1 if repeat_index < 0: nr_wraps -= 1 repeat_index = len(self.segtimedata) - 1 # Now at first entry corresponding to start_index and start_wraps seg_data = self.segtimedata[start_index] seg_start_time = self.get_seg_starttime(nr_wraps, start_index, start_repeats) if start_index != end_index: nr_repeats = seg_data.repeats - start_repeats elif len(self.segtimedata) == 1 and end_repeats < start_repeats: nr_repeats = (self.segtimedata[0].repeats + end_repeats - start_repeats) else: # There was only one entry which was repeated nr_repeats = end_repeats - start_repeats s_elem = self.generate_s_elem(seg_start_time, seg_data.duration, nr_repeats) seg_timeline.insert(0, s_elem) self.start_number = self.get_seg_number(nr_wraps, start_index, start_repeats) return seg_timeline