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 modify_baseurl(self, baseurl_elem, new_baseurl):
"Modify the text of an existing BaseURL"
baseurl_elem.text = new_baseurl
def insert_ato(self, baseurl_elem, new_ato):
"Add availabilityTimeOffset to BaseURL element"
baseurl_elem.set('availabilityTimeOffset', new_ato)
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 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 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 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 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 name == "EventStream" and messageData:
eventElem = ElementTree.Element(add_ns("Event"))
eventElem.set("messageData", messageData)
elem.append(eventElem)
elem.tail = "\n"
return elem
def insert_utc_timings(self, mpd, start_pos):
"""Insert UTCTiming elements right after program information in order given by self.utc_timing_methods.
The version of DASH should also be updated, but that is not done yet."""
pos = start_pos
for utc_method in self.utc_timing_methods:
if utc_method == "direct":
direct_time = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time()))
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:direct:2014', direct_time)
elif utc_method == "head":
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:http-head:2014',
self.utc_head_url)
elif utc_method == "ntp":
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:ntp:2014',
UTC_TIMING_NTP_SERVER)
elif utc_method == "sntp":
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:sntp:2014',
UTC_TIMING_SNTP_SERVER)
elif utc_method == "httpxsdate":
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:http-xsdate:2014',
UTC_TIMING_HTTP_SERVER)
elif utc_method == "httpiso":
time_elem = self.create_descriptor_elem('UTCTiming', 'urn:mpeg:dash:utc:http-iso:2014',
UTC_TIMING_HTTP_SERVER)
else: # Unknown or un-implemented UTCTiming method
raise MpdModifierError("Unknown UTCTiming method: %s" % utc_method)
mpd.insert(pos, time_elem)
pos += 1
return pos
def get_full_xml(self, clean=True):
"Get a string of all XML cleaned (no ns0 namespace)"
ofh = StringIO()
self.tree.write(ofh, encoding="unicode")
value = ofh.getvalue()
if clean:
value = value.replace("ns0:", "").replace("xmlns:ns0=", "xmlns=")
xml_intro = '\n'
return xml_intro + value
================================================
FILE: dashlivesim/dashlib/scte35.py
================================================
"""SCTE-35 splice cues in emsg format.
Follows the DASH-IF guidelines."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from dashlivesim.dashlib import emsg
# The scheme_id_uri is a bit unsure. There is also a binary format, which may be preferred (...:2014:bin)
SCHEME_ID_URI = "urn:scte:scte35:2013:xml"
PID = 999
PROFILE = "http://dashif.org/guidelines/adin/app"
PTS_MOD = 2**33
def make_xml_bool(value):
"Return a true or false string."
return value and "true" or "false"
class Scte35Error(Exception):
"Error in SCTE-35 context."
def create_scte35_insert_message(pts_adjustment, tier, splice_event_id, splice_event_cancel_indicator,
out_of_network_indicator, unique_program_id, avail_num, avails_expected,
splice_immediate_flag, pts_time, auto_return, duration):
"Create the emsg message data for an SCTE-35 Insert Event."
# pylint: disable=too-many-arguments, too-many-locals
splice_insert_attribs = {'spliceEventId': splice_event_id,
'spliceEventCancelIndicator': make_xml_bool(splice_event_cancel_indicator),
'outOfNetworkIndicator': make_xml_bool(out_of_network_indicator),
'uniqueProgramId': unique_program_id,
'availNum': avail_num,
'availsExpected': avails_expected,
'spliceImmediateFlag': make_xml_bool(splice_immediate_flag)}
splice_insert_attr_keys = ('spliceEventId', 'spliceEventCancelIndicator', 'outOfNetworkIndicator',
'uniqueProgramId', 'availNum', 'availsExpected', 'spliceImmediateFlag')
splice_event_cancel_indicator = make_xml_bool(splice_event_cancel_indicator)
splice_immediate_flag = make_xml_bool(splice_immediate_flag)
lines = []
lines.append('' % (pts_adjustment, tier))
if splice_event_cancel_indicator == "false":
attributes = " ".join(['%s="%s"' % (k, splice_insert_attribs[k]) for k in splice_insert_attr_keys])
lines.append('' % attributes)
if splice_immediate_flag == "false":
lines.append('' % pts_time)
if duration:
lines.append('' % (make_xml_bool(auto_return), duration))
else:
lines.append(''
% (splice_event_id, splice_event_cancel_indicator))
lines.append("")
lines.append("")
return "\n".join(lines)
class Scte35Emsg(emsg.Emsg):
"Class providing an SCTE-35 Insert EMSG box."
def __init__(self, timescale, presentation_time_offset, presentation_time, duration, message_id, splice_id):
# pylint: disable=too-many-locals
if timescale != 90000:
raise Scte35Error("Only supports timescale=90000")
presentation_time_delta = presentation_time - presentation_time_offset
pts_adjustment = 0
tier = 4095
splice_event_id = splice_id
splice_event_cancel_indicator = False
out_of_network_indicator = False
unique_program_id = 0
avail_num = 0
avails_expected = 0
splice_immediate_flag = False
pts_time = presentation_time % PTS_MOD
auto_return = True
message_data = create_scte35_insert_message(pts_adjustment, tier, splice_event_id,
splice_event_cancel_indicator, out_of_network_indicator,
unique_program_id, avail_num, avails_expected,
splice_immediate_flag, pts_time, auto_return, duration)
emsg.Emsg.__init__(self, SCHEME_ID_URI, PID, timescale, presentation_time_delta, duration,
message_id, message_data)
def create_scte35_emsg(timescale, presentation_time_offset, presentation_time, duration, message_id, splice_id):
"Create the Emsg DASH box for SCTE35 splice_insert."
scte35emsg = Scte35Emsg(timescale, presentation_time_offset, presentation_time, duration, message_id, splice_id)
return scte35emsg.get_box()
================================================
FILE: dashlivesim/dashlib/segmentmuxer.py
================================================
"""Segment Muxer. Can multiplex DASH init and media segments (of some kinds).
"""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from dashlivesim.dashlib.mp4filter import MP4Filter
from dashlivesim.dashlib.structops import uint32_to_str, str_to_uint32
class InitSegmentStructure(MP4Filter):
"""Holds the structure of an initsegment.
Stores ftyp, mvhd, trex, and trak box data."""
def __init__(self, filename=None, data=None):
MP4Filter.__init__(self, filename, data)
self.top_level_boxes_to_parse = [b'ftyp', b'moov']
self.composite_boxes_to_parse = [b'moov', b'mvex']
self._ftyp = None
self._mvhd = None
self._trex = None
self._trak = None
def process_ftyp(self, data):
"Get a handle to ftyp."
self._ftyp = data
return data
def process_mvhd(self, data):
"Get a handle to mvhd."
self._mvhd = data
return data
def process_trex(self, data):
"Get a handle to trex."
self._trex = data
return data
def process_trak(self, data):
"Get a handle to trak."
self._trak = data
return data
@property
def ftyp(self):
"Get the ftyp box."
return self._ftyp
@property
def trak(self):
"Get the trak box."
return self._trak
@property
def mvhd(self):
"Get the mvhd box."
return self._mvhd
@property
def trex(self):
"Get the trex box."
return self._trex
class MultiplexInits(object):
"Takes two init segments and multiplexes them. The ftyp and mvhd is taken from the first."
# pylint: disable=too-few-public-methods
def __init__(self, filename1=None, filename2=None, data1=None, data2=None):
self.istruct1 = InitSegmentStructure(filename1, data1)
self.istruct1.filter()
self.istruct2 = InitSegmentStructure(filename2, data2)
self.istruct2.filter()
def construct_muxed(self):
"Construct a multiplexed init segment."
data = []
data.append(self.istruct1.ftyp)
mvex_size = 8 + len(self.istruct1.trex) + len(self.istruct2.trex)
moov_size = 8 + len(self.istruct1.mvhd) + mvex_size + len(self.istruct1.trak) + len(self.istruct2.trak)
data.append(uint32_to_str(moov_size))
data.append(b'moov')
data.append(self.istruct1.mvhd)
data.append(uint32_to_str(mvex_size))
data.append(b'mvex')
data.append(self.istruct1.trex)
data.append(self.istruct2.trex)
data.append(self.istruct1.trak)
data.append(self.istruct2.trak)
return b"".join(data)
class MediaSegmentStructure(MP4Filter):
"Holds the box structure of a media segment."
# pylint: disable=too-many-instance-attributes
def __init__(self, filename=None, data=None):
MP4Filter.__init__(self, filename, data)
self.top_level_boxes_to_parse = [b'styp', b'moof', b'mdat']
self.trun_data_offset = None
self.trun_data_offset_in_traf = None
self.traf_start = None
self.styp = None
self.mfhd = None
self.traf = None
self.moof = None
self.mdat = None
def parse_trun(self, data, pos):
"Parse trun box and find position of data_offset."
flags = str_to_uint32(data[8:12]) & 0xffffff
data_offset_present = flags & 1
if data_offset_present:
self.trun_data_offset = str_to_uint32(data[16:20])
self.trun_data_offset_in_traf = pos + 16 - self.traf_start
def filter_box(self, boxtype, data, file_pos, path=b""):
"Filter box or tree of boxes recursively."
if boxtype == b"styp":
self.styp = data
elif boxtype == b"moof":
self.moof = data
elif boxtype == b"mdat":
self.mdat = data
elif boxtype == b"mfhd":
self.mfhd = data
elif boxtype == b"traf":
self.traf = data
self.traf_start = file_pos
elif boxtype == b"trun":
self.parse_trun(data, file_pos)
if path == b"":
path = boxtype
else:
path = b"%s.%s" % (path, boxtype)
output = b""
if path in (b"moof", b"moof.traf"): # Go deeper
output += data[:8]
pos = 8
while pos < len(data):
size, boxtype = self.check_box(data[pos:pos+8])
output += self.filter_box(boxtype, data[pos:pos+size], file_pos + len(output), path)
pos += size
else:
output = data
return output
class MultiplexMediaSegments(object):
"""Takes two media segments and multiplexes them like [mdat1][moof1][mdat2][moof2].
The styp and is taken from the first."""
def __init__(self, filename1=None, filename2=None, data1=None, data2=None):
self.mstruct1 = MediaSegmentStructure(filename1, data1)
self.mstruct1.filter()
self.mstruct2 = MediaSegmentStructure(filename2, data2)
self.mstruct2.filter()
def mux_on_fragment_level(self):
"Multiplex on frgment level."
data = []
data.append(self.mstruct1.styp)
data.append(self.mstruct1.moof)
data.append(self.mstruct1.mdat)
data.append(self.mstruct2.moof)
data.append(self.mstruct2.mdat)
return b"".join(data)
def mux_on_sample_level(self):
"Mux media samples into one mdata. This is done by simple concatenation."
def get_traf_with_mod_offset(mstruct, delta_offset):
"Get a traf box but with modified offset."
if mstruct.trun_data_offset is None:
return mstruct.traf
new_data_offset = mstruct.trun_data_offset + delta_offset
traf = mstruct.traf
offset = mstruct.trun_data_offset_in_traf
return traf[:offset] + uint32_to_str(new_data_offset) + traf[offset+4:]
delta_offset1 = len(self.mstruct2.traf)
delta_offset2 = len(self.mstruct1.traf) + len(self.mstruct1.mdat) - 8
traf1 = get_traf_with_mod_offset(self.mstruct1, delta_offset1)
traf2 = get_traf_with_mod_offset(self.mstruct2, delta_offset2)
moof_size = 8 + len(self.mstruct1.mfhd) + len(self.mstruct1.traf) + len(self.mstruct2.traf)
mdat_size = len(self.mstruct1.mdat) + len(self.mstruct2.mdat) - 8
data = []
data.append(self.mstruct1.styp)
data.append(uint32_to_str(moof_size))
data.append(b'moof')
data.append(self.mstruct1.mfhd)
data.append(traf1)
data.append(traf2)
data.append(uint32_to_str(mdat_size))
data.append(b'mdat')
data.append(self.mstruct1.mdat[8:])
data.append(self.mstruct2.mdat[8:])
return b"".join(data)
================================================
FILE: dashlivesim/dashlib/segtimeline.py
================================================
"""SegmentTimeLine XML entry generator."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
from struct import unpack
from xml.etree import ElementTree
import bisect
from dashlivesim.dashlib.configprocessor import SEGTIMEFORMAT, SegTimeEntry
from dashlivesim.dashlib.dash_namespace import add_ns
class SegmentTimeLineGeneratorError(Exception):
"Something strange happened."
pass
class SegmentTimeLineGenerator(object):
"Generate SegmentTimeline Object with times relative to availabilityStartTime."
def __init__(self, media_data, cfg):
self.timescale = media_data['timescale']
self.cfg = cfg
try:
dat_file = media_data['dat_file']
except KeyError as e:
print("Error for %s: %s" % (media_data, e))
dat_file_path = os.path.join(self.cfg.vod_cfg_dir, dat_file)
self.segtimedata = [] # Tuples corresponding to SegTimeEntry
with open(dat_file_path, "rb") as ifh:
data = ifh.read(12)
while data:
ste = SegTimeEntry(*unpack(SEGTIMEFORMAT, data))
self.segtimedata.append(ste)
data = ifh.read(12)
self.interval_starts = [std.start_time for std in self.segtimedata]
self.wrap_duration = cfg.vod_wrap_seconds * self.timescale
self.nr_segments_per_wrap = cfg.vod_nr_segments_in_loop
self.first_segment_number = cfg.vod_first_segment_in_loop
self.start_number = None
def create_segtimeline(self, start_time, end_time, use_closest=False):
"Create and insert a new 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
def get_seg_starttime(self, nr_wraps, index, repeats):
"Get the segment starttime given repeats."
seg_data = self.segtimedata[index]
return nr_wraps*self.wrap_duration + seg_data.start_time + repeats*seg_data.duration
def get_seg_number(self, nr_wraps, index, repeats):
"Get the segment number given repeats."
seg_data = self.segtimedata[index]
return (nr_wraps*self.nr_segments_per_wrap + seg_data.start_nr +
repeats - self.first_segment_number)
def get_seg_endtime(self, nr_wraps, index, repeats):
"Get the end of a segment."
seg_data = self.segtimedata[index]
return nr_wraps*self.wrap_duration + seg_data.start_time + (repeats+1)*seg_data.duration
def find_latest_starting_before(self, act_time):
"Find the latest segment starting before act_time."
nr_wraps, rel_time = divmod(act_time, self.wrap_duration)
if nr_wraps < 0:
return (None, None, None) # This is before AST
index = bisect.bisect(self.interval_starts, rel_time) - 1
seg_data = self.segtimedata[index]
repeats = 0
accumulated_end_time = seg_data.start_time + seg_data.duration
while accumulated_end_time < rel_time:
accumulated_end_time += seg_data.duration
repeats += 1
return index, repeats, nr_wraps
def find_closest_start(self, act_time):
nr_wraps, rel_time = divmod(act_time, self.wrap_duration)
if nr_wraps < 0:
return (None, None, None) # This is before AST
index = bisect.bisect(self.interval_starts, rel_time) - 1
seg_data = self.segtimedata[index]
repeats = 0
start = seg_data.start_time
if abs(rel_time - start) <= (seg_data.duration // 2):
return index, repeats, nr_wraps
while repeats < seg_data.repeats:
repeats += 1
start += seg_data.duration
if abs(rel_time - start) <= (seg_data.duration // 2):
return index, repeats, nr_wraps
index += 1
if index >= len(self.interval_starts):
index = 0
nr_wraps += 1
return index, self.segtimedata[index].repeats, nr_wraps
def find_closest_end(self, act_time):
"Find "
nr_wraps, rel_time = divmod(act_time, self.wrap_duration)
if nr_wraps < 0:
return (None, None, None) # This is before AST
index = bisect.bisect(self.interval_starts, rel_time) - 1
seg_data = self.segtimedata[index]
repeats = 0
start = seg_data.start_time
if abs(act_time, start) < (seg_data.duration // 2):
return index, repeats, nr_wraps
while repeats < seg_data.repeats:
repeats += 1
start += seg_data.duration
if abs(act_time, start) < (seg_data.duration // 2):
return index, repeats, nr_wraps
index += 1
if index >= self.interval_starts:
index = 0
nr_wraps += 1
return index, self.segtimedata[index].repeats, nr_wraps
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
================================================
FILE: dashlivesim/dashlib/sessionid.py
================================================
"""Session IDs to allow for tracing sessions."""
import random
MAX_NUMBER = 2**32 - 1
def generate_session_id():
"Generate a session ID as hex string."
return "%08x" % random.randint(0, MAX_NUMBER)
================================================
FILE: dashlivesim/dashlib/stpp_generator/__init__.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: dashlivesim/dashlib/stpp_generator/make_stpp_segments.py
================================================
"""Generator of TTML/stpp mp4 media segments with fixed duration according to a template."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
from argparse import ArgumentParser
from jinja2 import Template
from stpp_creator import StppInitFilter, create_media_segment, TTML_TEMPLATE
BODY_TEMPLATE = '''
{% for p in paragraph %}
{{p.text}}
{% endfor %}
'''
"""
"""
TTML_XML = TTML_TEMPLATE.format(BODY_TEMPLATE)
class SegmentCreator(object):
"Creator of both init and media segments."
def __init__(self, number_of_segments, segment_duration, resolution, segment_name_format,
language, trackid, output_path):
self.number_of_segments = number_of_segments
self.segment_duration = segment_duration
self.segment_name_format = segment_name_format
self.language = language
self.trackid = trackid
self.output_path = output_path
self.resolution = resolution
# pylint: disable=too-many-locals
def create_segments(self):
"Create init and media segments."
print("Creating: %dx%d"%(self.number_of_segments, self.segment_duration))
# Create output directory if it doesn't exist
if not os.path.exists(self.output_path):
os.mkdir(self.output_path)
# Create init segment
init_segment_path = os.path.join(self.output_path, "init.mp4")
with open(init_segment_path, 'wb') as iof:
initfilter = StppInitFilter(self.language, self.trackid, self.resolution)
initseg = initfilter.filter()
iof.write(initseg)
# Create jinja2 template
ttml_template = Template(TTML_XML.strip())
# Create media segments
time = 0
for seg_nr in range(1, self.number_of_segments + 1):
media_segment_path = (os.path.join(self.output_path, self.segment_name_format)+".m4s") % (seg_nr)
r1_start_time = self.create_time_string(time)
r1_end_time = self.create_time_string(time + self.segment_duration)
r1_text = "Segment: %d" % (seg_nr)
# Create paragraph info
pars = []
for rel_time in range(0, self.segment_duration, self.resolution):
start_time = self.create_time_string(time + rel_time)
end_time = self.create_time_string(time + rel_time + self.resolution)
id_str = "sub%05d" % (time+rel_time)
text = '%s : %s' % (self.language, start_time)
pars.append({'begin':start_time, 'end':end_time, 'id':id_str, 'text':text})
ttml_data = ttml_template.render(paragraph=pars, r1_id="%010d" % (time), r1_begin=r1_start_time,
r1_end=r1_end_time, r1_text=r1_text, lang=self.language)
#print ttml_data.encode('utf-8')
with open(media_segment_path, "wb") as mof:
output = create_media_segment(self.trackid, seg_nr, self.segment_duration, time,
ttml_data.encode('utf-8'))
mof.write(output)
time += self.segment_duration
#pylint: disable=no-self-use
def create_time_string(self, time_ms):
"Create time string from number of milliseconds."
hours, time_ms = divmod(time_ms, 3600000)
minutes, time_ms = divmod(time_ms, 60000)
seconds, milliseconds = divmod(time_ms, 1000)
return "%02d:%02d:%02d.%03d"%(hours, minutes, seconds, milliseconds)
def main():
"Parse command line and run script."
parser = ArgumentParser()
parser.add_argument("-d", "--segment_duration", dest="segment_duration", type=int,
help="duration of segment in ms", required=True)
parser.add_argument("-n", "--number_of_segments", dest="number_of_segments", type=int,
help="number of segments to generate", required=True)
parser.add_argument("-o", "--output_path", dest="output_path", type=str,
help="path to output folder", required=True)
parser.add_argument("-f", "--segment_name_format", dest="segment_name_format", type=str,
help="format of media segment w/o extension (default %%d).", default="%d")
parser.add_argument("-r", "--resolution", dest="resolution", type=int,
help="subtitle time stamp resolution (default = 1000)", default=1000)
parser.add_argument("-l", "--language", dest="language", type=str, help="language (3 letters, default eng)",
default="eng")
parser.add_argument("-t", "--trackid", dest="trackid", type=int, help="trackID (default 3)", default=3)
args = parser.parse_args()
output_path = os.path.abspath(args.output_path)
seg_creator = SegmentCreator(args.number_of_segments, args.segment_duration, args.resolution,
args.segment_name_format, args.language, args.trackid, output_path)
seg_creator.create_segments()
if __name__ == "__main__":
main()
================================================
FILE: dashlivesim/dashlib/stpp_generator/stpp_creator.py
================================================
"""Generate TTML init and media segments.
Start from template (which has timescale=1000)."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from dashlivesim.dashlib.mp4filter import MP4Filter
from dashlivesim.dashlib.structops import uint32_to_str, str_to_uint32, uint64_to_str
TTML_MEDIA_TMPL = '\x00\x00\x00\x18stypmsdh\x00\x00\x00\x00msdhdash\x00\x00\x00`moof\x00\x00\x00\x10mfhd\x00\x00\
\x00\x00\x00\x00\x00\x01\x00\x00\x00Htraf\x00\x00\x00\x18tfhd\x00\x02\x00\x18\x00\x00\x00\x03\x00\x00\x03\xe8\x00\
\x00\x00\t\x00\x00\x00\x14tfdt\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14trun\x00\x00\x00\
\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x00\x08mdat'
TIMESCALE = 1000 # This is the units for tfdt time and durations.
TRACK_ID = 3
# The init has sample_time_scale and trackID according to the values above
TTML_INIT = '\x00\x00\x00\x18ftypiso6\x00\x00\x00\x01isomdash\x00\x00\x02\x9dmoov\x00\x00\x00lmvhd\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00(mvex\x00\x00\x00 trex\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x01trak\x00\x00\x00\\tkhd\x00\x00\x00\x07\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x9dmdia\x00\x00\x00 mdhd\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe8\x00\x00\x00\x00\x15\xc7\x00\x00\x00\x00\x00-hdlr\x00\x00\x00\x00\
\x00\x00\x00\x00subt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00DASH-IF TTML\x00\x00\x00\x01Hminf\x00\x00\x00\x0c\
sthd\x00\x00\x00\x00\x00\x00\x00$dinf\x00\x00\x00\x1cdref\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0curl \x00\x00\
\x00\x01\x00\x00\x01\x10stbl\x00\x00\x00\xc4stsd\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb4stpp\x00\x00\x00\x00\
\x00\x00\x00\x01http://www.w3.org/ns/ttml#parameter http://www.w3.org/ns/ttml http://www.w3.org/ns/ttml#styling http:\
//www.w3.org/ns/ttml#metadata urn:ebu:metadata urn:ebu:style\x00\x00\x00\x00\x00\x00\x10stts\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x10stsc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14stsz\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x10stco\x00\x00\x00\x00\x00\x00\x00\x00'
# EBU-TT-D sample
TTML_TEMPLATE = '''
DASH-IF Live Simulator
urn:ebu:distribution:2014-01
30
{0}
'''
BODY_TEMPLATE = '''
The time is 00:00:00
The time is 00:00:01
'''
TTML_XML = TTML_TEMPLATE.format(BODY_TEMPLATE)
class StppSegmentCreatorError(Exception):
"Error in TtmlSegmentGenerator."
class StppMediaFilter(MP4Filter):
"Generate a TTML media segment with the limitation that there can only be one sample ()."
def __init__(self, track_id, sequence_nr, sample_duration, tfdt_time, ttml_data):
MP4Filter.__init__(self, data=TTML_MEDIA_TMPL)
self.track_id = track_id
self.sequence_nr = sequence_nr
self.default_sample_duration = sample_duration
self.tfdt_time = tfdt_time
self.ttml_data = ttml_data
self.top_level_boxes_to_parse = [b'styp', b'moof', b'mdat', b'sidx']
self.composite_boxes_to_parse = [b'moof', b'traf']
# pylint: disable=unused-argument, no-self-use
def process_sidx(self, data):
"SIDX not supported."
raise StppSegmentCreatorError("SIDX presence not supported")
def process_mfhd(self, data):
"Set sequence number."
return data[:12] + uint32_to_str(self.sequence_nr)
def process_tfhd(self, data):
"Process a tfhd box and set trackID, defaultSampleDuration and defaultSampleSize"
tf_flags = str_to_uint32(data[8:12]) & 0xffffff
assert tf_flags == 0x020018, "Can only handle certain tf_flags combinations"
output = data[:12]
output += uint32_to_str(self.track_id)
output += uint32_to_str(self.default_sample_duration)
output += uint32_to_str(len(self.ttml_data))
return output
def process_tfdt(self, data):
"Process a tfdt box and set the baseMediaDecodeTime."
version = ord(data[8])
assert version == 1, "Can only handle tfdt version 1 (64-bit tfdt)."
output = data[:12]
output += uint64_to_str(self.tfdt_time)
return output
def process_mdat(self, data):
"Make an mdat box with the right size to contain the one-and-only ttml sample."
size = len(self.ttml_data) + 8
return uint32_to_str(size) + b'mdat' + self.ttml_data
class StppInitFilter(MP4Filter):
"Generate a TTML init segment from template by changing some values."
def __init__(self, lang="eng", track_id=TRACK_ID, timescale=TIMESCALE, creation_modfication_time=None,
hdlr_name=None):
"Filter to create an appropriate init segment."
MP4Filter.__init__(self, data=TTML_INIT)
self.lang = lang
self.track_id = track_id
self.timescale = timescale
self.creation_modfication_time = creation_modfication_time # Measured from 1904-01-01 in seconds
self.handler_name = hdlr_name
self.top_level_boxes_to_parse = [b'moov']
self.composite_boxes_to_parse = [b'moov', b'trak', b'mdia', b'minf', b'dinf']
def process_mvhd(self, data):
"Set nextTrackId and time and movie timescale."
output = self._insert_timing_data(data)
pos = len(output)
output += data[pos:-4]
output += uint32_to_str(self.track_id + 1) # next_track_ID
return output
def process_tkhd(self, data):
"Set trackID and time."
version = data[8]
output = data[:12]
if version == 1:
if self.creation_modfication_time:
output += uint64_to_str(self.creation_modfication_time)
output += uint64_to_str(self.creation_modfication_time)
else:
output += data[12:28]
output += uint32_to_str(self.track_id)
output += uint32_to_str(0)
output += uint64_to_str(0) # duration
pos = 44
else:
if self.creation_modfication_time:
output += uint32_to_str(self.creation_modfication_time)
output += uint32_to_str(self.creation_modfication_time)
else:
output += data[12:20]
output += uint32_to_str(self.track_id)
output += uint32_to_str(0)
output += uint32_to_str(0) # duration
pos = 32
output += data[pos:]
return output
def process_mdhd(self, data):
"Set the timescale for the trak, language and time."
def get_char_bits(char):
"Each character in the language is smaller case and offset at 0x60."
return ord(char) - 96
output = self._insert_timing_data(data)
assert len(self.lang) == 3
lang = self.lang
lang_bits = (get_char_bits(lang[0]) << 10) + (get_char_bits(lang[1]) << 5) + get_char_bits(lang[2])
output += uint32_to_str(lang_bits << 16)
return output
def process_hdlr(self, data):
"Set handler name, if desired."
hdlr = data[16:20]
hdlr_name = data[32:-1] # Actually UTF-8 encoded
print("Found hdlr %s: %s" % (hdlr, hdlr_name))
if self.handler_name:
output = uint32_to_str(len(self.handler_name) + 33) + data[4:32] + self.handler_name + '\x00'
print("Wrote hdlr %s" % self.handler_name)
else:
output = data
return output
def _insert_timing_data(self, data):
"Help function to insert timestamps and timescale in similar boxes."
version = data[8]
output = data[:12]
if version == 1:
if self.creation_modfication_time:
output += uint64_to_str(self.creation_modfication_time)
output += uint64_to_str(self.creation_modfication_time)
else:
output += data[12:28]
output += uint32_to_str(self.timescale)
output += uint64_to_str(0) # duration
else:
if self.creation_modfication_time:
output += uint32_to_str(self.creation_modfication_time)
output += uint32_to_str(self.creation_modfication_time)
else:
output += data[12:20]
output += uint32_to_str(self.timescale)
output += uint32_to_str(0)
return output
def create_media_segment(track_id, sequence_nr, sample_duration, tfdt_time, ttml_data):
"Create a media segment."
ttml_seg = StppMediaFilter(track_id, sequence_nr, sample_duration, tfdt_time, ttml_data)
return ttml_seg.filter()
def create_init_segment(lang="eng", track_id=TRACK_ID, timescale=TIMESCALE, creation_modfication_time=None,
hdlr_name=None):
"Create an init segment."
init_seg = StppInitFilter(lang, track_id, timescale, creation_modfication_time, hdlr_name)
return init_seg.filter()
================================================
FILE: dashlivesim/dashlib/structops.py
================================================
"""Simple struct operations to pack and unpack numbers to strings."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from struct import pack, unpack
def str_to_uint32(string4):
"4-character string to unsigned int32."
return unpack(">I", string4)[0]
def str_to_sint32(string4):
"4-character string to signed int32."
return unpack(">i", string4)[0]
def str_to_uint64(string8):
"8-character string to unsigned int64."
return unpack(">Q", string8)[0]
def uint32_to_str(uint32):
"Unsigned int32 to string."
return pack(">I", uint32)
def sint32_to_str(sint32):
"Signed int32 to string."
return pack(">i", sint32)
def uint64_to_str(uint64):
"Unsigned int64 to string."
return pack(">Q", uint64)
================================================
FILE: dashlivesim/dashlib/timeformatconversions.py
================================================
"""Helper functions for time conversions."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import time
import re
RE_DURATION = re.compile(r"PT((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?")
class TimeFormatConversionError(Exception):
"Generic timeformatconversion error."
def iso_duration_to_seconds(duration):
"Convert a time duration in ISO 8601 format to seconds (only integer parts)."
match_obj = RE_DURATION.match(duration)
if not match_obj:
raise TimeFormatConversionError("%s does not match a duration" % duration)
secs = 0
if match_obj.group("hours"):
secs += int(match_obj.group("hours"))*3600
if match_obj.group("minutes"):
secs += int(match_obj.group("minutes"))*60
if match_obj.group("seconds"):
secs += int(match_obj.group("seconds"))
return secs
def seconds_to_iso_duration(nr_secs):
"Make interval string in format PT... from time in seconds."
days, rest = divmod(nr_secs, 3600*24)
hours, rest = divmod(rest, 3600)
minutes, seconds = divmod(rest, 60)
period = "P"
if days > 0:
period += "%dD" % days
period += "T"
if hours > 0:
period += "%dH" % hours
if minutes > 0:
period += "%dM" % minutes
if seconds > 0 or period == "PT":
period += "%dS" % seconds
return period
def make_timestamp(time_in_s):
"Return timestamp as string."
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time_in_s))
================================================
FILE: dashlivesim/dashlib/ttml_timing_offset.py
================================================
"""Add an offset in seconds to TTML timing elements."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re
import time
TIME_PATTERN_S = re.compile(rb'(?P(begin|end))="(?P\d\d):(?P\d\d):(?P\d\d)')
CONTENT_PATTERN_S = re.compile(rb'(?P\w+) : (?P\d\d):(?P\d\d):(?P\d\d)(\.\d+)?')
CONTENT_PATTERN_SEGMENT = re.compile(rb'(?PSegment # )(?P\d+)')
def adjust_ttml_content(xml_str, offset_in_s, output_seg_nr):
"Add offset in seconds to begin and end elements in xml bytestring."
def replace(match_obj):
"Match and replace time for begin and end attributes."
matches = match_obj.groupdict()
attr = matches['attr']
hours = int(matches['hours'])
minutes = int(matches['minutes'])
seconds = int(matches['seconds'])
total_seconds = seconds + 60 * minutes + 3600 * hours + offset_in_s
hours, seconds = divmod(total_seconds, 3600)
minutes, seconds = divmod(seconds, 60)
return b'%s="%02d:%02d:%02d' % (attr, hours, minutes, seconds)
def replace_content(match_obj):
"Match and replace time with UTC time in lang time pattern."
matches = match_obj.groupdict()
hours = int(matches['hours'])
minutes = int(matches['minutes'])
seconds = int(matches['seconds'])
total_seconds = seconds + 60 * minutes + 3600 * hours + offset_in_s
time_str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(total_seconds)).encode('utf-8')
return b'%s : UTC = %s' % (matches['lang'], time_str)
def replace_segment_nr(match_obj):
"Match and replace segment number."
matches = match_obj.groupdict()
return b'%s%d' % (matches['intro'], output_seg_nr)
xml_str = re.sub(TIME_PATTERN_S, replace, xml_str)
xml_str = re.sub(CONTENT_PATTERN_S, replace_content, xml_str)
xml_str = re.sub(CONTENT_PATTERN_SEGMENT, replace_segment_nr, xml_str)
return xml_str
================================================
FILE: dashlivesim/mod_wsgi/__init__.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: dashlivesim/mod_wsgi/mod_dashlivesim.py
================================================
"WSGI Module for dash-live-source-simulator"
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Note that VOD_CONF_DIR and CONTENT_ROOT directories must be set in environment
# For Apache mod_wsgi, this is done using setEnv
import traceback
from os.path import splitext
from urllib.parse import urlparse, parse_qs
from time import time, sleep
from dashlivesim.dashlib import dash_proxy, sessionid, mpd_proxy
from dashlivesim.dashlib.dash_proxy import ChunkedSegment
from dashlivesim import SERVER_AGENT
MAX_SESSION_LENGTH = 0 # If non-zero, limit sessions via redirect
# Helper for HTTP responses
# pylint: disable=dangerous-default-value
status_string = {
200: 'OK',
206: 'Partial Content',
302: 'Found',
404: 'Not Found',
410: 'Gone'
}
def start_reply(status_code, response, length=-1, headers={}):
"Start reply by writing headers reply."
status = "%d %s" % (status_code, status_string[status_code])
# Add default headers to all requests
headers['Accept-Ranges'] = 'bytes'
headers['Pragma'] = 'no-cache'
headers['Cache-Control'] = 'no-cache'
headers['Expires'] = '-1'
headers['DASH-Live-Simulator'] = SERVER_AGENT
headers['Access-Control-Allow-Headers'] = 'origin,range,accept-encoding,referer'
headers['Access-Control-Allow-Methods'] = 'GET,HEAD,OPTIONS'
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Expose-Headers'] = 'Server,range,Content-Length,Content-Range,Date'
if length >= 0:
headers['Content-Length'] = str(length)
if 'Content-Type' not in headers:
headers['Content-Type'] = 'text/plain'
response(status, list(headers.items()))
def full_reply(status_code, response, body=b"", headers={}):
"A full reply including body and content-length."
start_reply(status_code, response, len(body), headers)
return [body]
# pylint: disable=too-many-branches, too-many-locals
def application(environment, start_response):
"WSGI Entrypoint"
hostname = environment['HTTP_HOST']
url = urlparse(environment['REQUEST_URI'])
vod_conf_dir = environment['VOD_CONF_DIR']
content_root = environment['CONTENT_ROOT']
is_https = environment.get('wsgi.url_scheme', False) and environment['wsgi.url_scheme'] == 'https'
path_parts = url.path.split('/')
ext = splitext(path_parts[-1])[1]
query = url.query if url.query else environment.get('QUERY_STRING', '')
args = parse_qs(query)
now = time()
body = None
if MAX_SESSION_LENGTH: # Redirect and do limit sessions in time
# Check if there is a sts_xxx parameter.
start_time = None
for part in path_parts:
if part.startswith('sts_'):
try:
start_time = int(part[4:])
except Exception:
pass
if ext == ".mpd" and start_time is None:
new_url = 'https://' if is_https else 'http://'
start_part = "sts_%d" % int(now)
session_id_path = "sid_%s" % sessionid.generate_session_id()
path_parts = (path_parts[:2] + [start_part] +
[session_id_path] + path_parts[2:])
new_url += hostname + '/'.join(path_parts)
if query:
new_url += '?' + query
body = b""
start_reply(302, start_response, len(body), {'Location': new_url})
body = b""
elif start_time is None:
body = b'No start_time in non-manifest request'
start_reply(404, start_response, len(body))
elif now > start_time + MAX_SESSION_LENGTH:
msg = "Maximum session length %ds passed" % MAX_SESSION_LENGTH
body = msg.encode('utf-8')
start_reply(410, start_response, len(body))
elif start_time > now + 5: # Give some margin
body = b'start_time is in future'
start_reply(404, start_response, len(body))
if body is not None:
yield body
else:
range_line = None
if 'HTTP_RANGE' in environment:
range_line = environment['HTTP_RANGE']
success = True
mimetype = get_mime_type(ext)
status_code = 200
payload_in = None
chunk = chunk_out = False
try:
dashProv = dash_proxy.createProvider(hostname, path_parts[1:], args,
vod_conf_dir, content_root, now,
None, is_https)
cfg = dashProv.cfg
ext = cfg.ext
if ext == ".m4s":
if cfg.chunk_duration_in_s is not None and cfg.chunk_duration_in_s > 0:
chunk = True
response = dash_proxy.get_media(dashProv, chunk)
if isinstance(response, ChunkedSegment):
chunk_out = True
elif ext in (".mpd", ".period"):
response = mpd_proxy.get_mpd(dashProv)
elif ext == ".mp4":
response = dash_proxy.get_init(dashProv)
elif ext == ".jpg":
response = dash_proxy.get_media(dashProv)
if isinstance(response, bytes) or isinstance(response, str) or chunk_out:
if isinstance(response, str):
response = response.encode('utf-8')
payload_in = response
if not payload_in:
success = False
else:
if not response['ok']:
success = False
payload_in = response['pl']
# pylint: disable=broad-except
except Exception as exc:
success = False
traceback.print_exc()
payload_in = "DASH Proxy Error: {0}\n URL={1}".format(exc, url)
if not success:
if not payload_in:
payload_in = "Not found (now)"
status_code = 404
mimetype = "text/plain"
if isinstance(payload_in, str):
payload_in = payload_in.encode('utf-8')
payload_out = payload_in
# Setup response headers
headers = {'Content-Type': mimetype}
if status_code != 404:
if range_line and not chunk_out:
payload_out, range_out = handle_byte_range(payload_in, range_line)
if range_out != "": # OK
headers['Content-Range'] = range_out
status_code = 206
else: # Bad range, drop it
print("mod_dash_handler: Bad range {0}".format(range_line))
if not chunk_out:
start_reply(status_code, start_response, len(payload_out), headers)
yield payload_out
else:
start_reply(status_code, start_response, -1, headers)
now_float = now
seg_start = payload_out.seg_start
chunk_dur = cfg.chunk_duration_in_s
margin = 0.1 # Make available 100ms before the formal time
for i, chunk in enumerate(payload_out.chunks, start=1):
now_float = time() # Update time
chunk_availability_time = seg_start + i * chunk_dur - margin
time_until_available = chunk_availability_time - now_float
# print("%d time_until_available %.3f" % (i, time_until_available))
if time_until_available > 0:
# print("Sleeping for %.3f" % time_until_available)
sleep(time_until_available)
yield chunk
def get_mime_type(ext):
"Get mime-type depending on extension."
if ext == ".mpd":
return "application/dash+xml"
elif ext == ".m4s":
return "video/iso.segment"
elif ext == ".mp4":
return "video/mp4"
return "text/plain"
def handle_byte_range(payload, range_line):
"""Handle byte range and return data and range-header value.
If range is strange, return empty string."""
range_parts = range_line.split("=")[-1]
ranges = range_parts.split(",")
if len(ranges) > 1:
return (payload, None)
length = len(payload)
range_interval = ranges[0]
range_start, range_end = range_interval.split("-")
bad_range = False
if range_start == "" and range_end != "":
# This is the rangeStart lasts bytes
range_start = length - int(range_end)
range_end = length - 1
elif range_start != "":
range_start = int(range_start)
if range_end != "":
range_end = min(int(range_end), length-1)
else:
range_end = length-1
else:
bad_range = True
if range_end < range_start:
bad_range = True
if bad_range:
return (payload, "")
ranged_payload = payload[range_start: range_end+1]
range_response = "bytes %d-%d/%d" % (range_start, range_end, len(payload))
return (ranged_payload, range_response)
def main():
"Local stand-alone wsgi server for testing."
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("-d", "--config_dir", dest="vod_conf_dir", type=str,
help="configuration root directory", required=True)
parser.add_argument("-c", "--content_dir", dest="content_dir", type=str,
help="content root directory", required=True)
parser.add_argument("--host", dest="host", type=str, help="IPv4 host", default="0.0.0.0")
parser.add_argument("--port", dest="port", type=int, help="IPv4 port", default=8059)
args = parser.parse_args()
def application_wrapper(env, resp):
"Wrapper around application for local webserver."
env['REQUEST_URI'] = env['PATH_INFO'] # Set REQUEST_URI from PATH_INFO
env['VOD_CONF_DIR'] = args.vod_conf_dir
env['CONTENT_ROOT'] = args.content_dir
return application(env, resp)
def run_local_webserver(wrapper, host, port):
"Local webserver."
from wsgiref.simple_server import make_server
print('Waiting for requests at "{0}:{1}"'.format(host, port))
httpd = make_server(host, port, wrapper)
httpd.serve_forever()
run_local_webserver(application_wrapper, args.host, args.port)
if __name__ == '__main__':
main()
================================================
FILE: dashlivesim/pylintrc
================================================
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=E1608,W1627,E1601,E1603,E1602,E1605,E1604,E1607,E1606,W1621,W1620,W1623,W1622,W1625,W1624,W1609,W1608,W1607,W1606,W1605,W1604,W1603,W1602,W1601,W1639,W1640,I0021,W1638,I0020,W1618,W1619,W1630,W1626,W1637,W1634,W1635,W1610,W1611,W1612,W1613,W1614,W1615,W1616,W1617,W1632,W1633,W0704,W1628,W1629,W1636
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,input
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )??$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=8
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
================================================
FILE: dashlivesim/tests/__init__.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: dashlivesim/tests/dash_test_util.py
================================================
"""Utilities for testing."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from os import unlink, makedirs
from os.path import join, abspath, dirname, exists
thisDir = abspath(dirname(__file__))
VOD_CONFIG_DIR = join(thisDir, "vod_cfg")
CONTENT_ROOT = thisDir
OUT_DIR = join(thisDir, "out_test")
def rm_outfile(filename):
"Remove file from OUT_DIR if it exists."
path = join(OUT_DIR, filename)
if exists(path):
unlink(path)
def write_data_to_outfile(data, filename):
"Write data to a file in OUT_DIR."
if not exists(OUT_DIR):
makedirs(OUT_DIR)
ofh = open(join(OUT_DIR, filename), "wb")
ofh.write(data)
ofh.close()
def findAllIndexes(needle, haystack):
"""Find the index for the beginning of each occurrence of ``needle`` in ``haystack``. Overlaps are allowed."""
indexes = []
last_index = haystack.find(needle)
while -1 != last_index:
indexes.append(last_index)
last_index = haystack.find(needle, last_index + 1)
return indexes
================================================
FILE: dashlivesim/tests/test_adinsertion.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
import xml.etree.ElementTree as ET
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.dashlib import mpdprocessor
class TestXlinkPeriod(unittest.TestCase):
def setUp(self):
self.old_set_baseurl_value = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = True
def tearDown(self):
mpdprocessor.SET_BASEURL = self.old_set_baseurl_value
def testMpdPeriodReplaced(self):
"Check whether before every xlink period, duration attribute has been inserted."
collect_result = []
urlParts = ['livesim', 'periods_60', 'xlink_30', 'insertad_1', 'testpic_2s', 'Manifest.mpd']
dp = dash_proxy.DashProvider("10.4.247.98", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10000)
d = mpd_proxy.get_mpd(dp)
xml = ET.fromstring(d)
# Make the string as a xml document.
# In the following, we will check if for every period before every xlink period, duration attribute has been
# added or not.
prev_child = []
for child in xml.findall('{urn:mpeg:dash:schema:mpd:2011}Period'): # Collect all period elements first
if '{http://www.w3.org/1999/xlink}actuate' in child.attrib:
# If the period element has the duration attribute.
collect_result.append('duration' in prev_child.attrib) # Then collect its period id in this
prev_child = child
# Ideally, at the periods should have a duration attribute, if no then the test fails.
self.assertFalse(False in collect_result)
================================================
FILE: dashlivesim/tests/test_availabilitytimeoffset.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT, rm_outfile, write_data_to_outfile
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.dashlib import mpdprocessor
def isMediaSegment(data):
"Check if response is a segment."
return isinstance(data, bytes) and data[4:8] == b'styp'
class TestMPDwithATO(unittest.TestCase):
"Test of MPDs with availability offset. Note that BASEURL must be set."
def setUp(self):
self.oldBaseUrlState = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = True
def tearDown(self):
mpdprocessor.SET_BASEURL = self.oldBaseUrlState
def testMpdGeneration(self):
"Check if availabilityTimeOffset is added correctly to the MPD file."
testOutputFile = "ato.mpd"
rm_outfile(testOutputFile)
urlParts = ['livesim', 'ato_30', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
write_data_to_outfile(d.encode('utf-8'), testOutputFile)
self.assertEqual(d.find('availabilityTimeOffset="30')-d.find('http://streamtest.eu/pdash/testpic/") > 0)
def testMPDwithChangedAST(self):
"Put AST to 1200s later than epoch start. There should be no PTO and startNumber=0 still."
testOutputFile = "start.mpd"
rm_outfile(testOutputFile)
urlParts = ['pdash', 'start_1200', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
write_data_to_outfile(d.encode('utf-8'), testOutputFile)
self.assertTrue(d.find('availabilityStartTime="1970-01-01T00:20:00Z"') > 0)
self.assertTrue(d.find('startNumber="0"') > 0)
self.assertTrue(d.find('presentationTimeOffset') < 0)
def testMPDwithStartandDur(self):
urlParts = ['pdash', 'start_1200', 'dur_600', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=900)
d = mpd_proxy.get_mpd(dp)
if dash_proxy.PUBLISH_TIME:
self.assertTrue(d.find('publishTime="1970-01-01T00:15:00Z"') > 0)
self.assertTrue(d.find('availabilityEndTime="1970-01-01T00:30:00Z"') > 0)
def testMPDwithStartand2Durations(self):
urlParts = ['pdash', 'start_1200', 'dur_600', 'dur_300', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=900)
d = mpd_proxy.get_mpd(dp)
if dash_proxy.PUBLISH_TIME:
self.assertTrue(d.find('publishTime="1970-01-01T00:15:00Z"') > 0)
self.assertTrue(d.find('availabilityEndTime="1970-01-01T00:30:00Z"') > 0)
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=1795)
d = mpd_proxy.get_mpd(dp)
if dash_proxy.PUBLISH_TIME:
self.assertTrue(d.find('publishTime="1970-01-01T00:29:00Z"') > 0)
self.assertTrue(d.find('availabilityEndTime="1970-01-01T00:35:00Z"') > 0)
def testHttpsBaseURL(self):
"Check that protocol is set to https if signalled to DashProvider."
mpdprocessor.SET_BASEURL = True
urlParts = ['pdash', 'testpic', 'Manifest.mpd']
is_https = 1
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0,
is_https=is_https)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find("https://streamtest.eu/pdash/testpic/") > 0)
def test_location_for_rel_times(self):
mpdprocessor.SET_BASEURL = True
urlParts = ['pdash', 'startrel_-20', 'stoprel_40', 'testpic',
'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT, now=1000)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find(
'availabilityStartTime="1970-01-01T00:16:18Z"') > 0)
self.assertTrue(d.find('startNumber="0"') > 0)
self.assertTrue(d.find("") < 0)
self.assertTrue(
d.find('http://streamtest.eu/pdash/start_978/stop_1044/'
'testpic/Manifest.mpd') > 0)
def test_location_for_rel_times_zero_offset(self):
mpdprocessor.SET_BASEURL = True
urlParts = ['pdash', 'startrel_-20', 'stoprel_40', 'timeoffset_0',
'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT, now=1000)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find(
'availabilityStartTime="1970-01-01T00:16:18Z"') > 0)
self.assertTrue(d.find('startNumber="163"') > 0)
self.assertTrue(d.find('presentationTimeOffset="978"') > 0)
self.assertTrue(d.find("") < 0)
self.assertTrue(
d.find('http://streamtest.eu/pdash/start_978/stop_1044/'
'timeoffset_0/testpic/Manifest.mpd') > 0)
def test_absolute_times(self):
mpdprocessor.SET_BASEURL = True
urlParts = ['pdash', 'start_978', 'stop_1044', 'testpic',
'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT, now=1000)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find(
'availabilityStartTime="1970-01-01T00:16:18Z"') > 0)
self.assertTrue(d.find("") > 0)
self.assertTrue(d.find('') < 0)
self.assertTrue(d.find('type="dynamic"') > 0)
self.assertTrue(d.find('mediaPresentationDuration="PT1M6S') > 0)
self.assertTrue(d.find('minimumUpdatePeriod') > 0)
def test_absolute_times_after_stop(self):
mpdprocessor.SET_BASEURL = True
urlParts = ['pdash', 'start_978', 'stop_1044', 'testpic',
'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT, now=1046)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find(
'availabilityStartTime="1970-01-01T00:16:18Z"') > 0)
self.assertTrue(d.find('type="static"') > 0)
self.assertTrue(d.find('mediaPresentationDuration="PT1M6S') > 0)
self.assertTrue(d.find('minimumUpdatePeriod') < 0)
class TestInitSegmentProcessing(unittest.TestCase):
def testInit(self):
urlParts = ['pdash', 'testpic', 'A1', 'init.mp4']
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = dash_proxy.get_init(dp)
self.assertEqual(len(d), 651)
class TestMediaSegments(unittest.TestCase):
def testMediaSegmentForTfdt32(self):
testOutputFile = "t1.m4s"
rm_outfile(testOutputFile)
now = 2101 # 1s after start of segment 350
segment = "349.m4s"
urlParts = ['pdash', 'tfdt_32', 'testpic', 'A1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
write_data_to_outfile(d, testOutputFile)
self.assertEqual(len(d), 39517)
def testMediaSegmentTooEarly(self):
urlParts = ['pdash', 'testpic', 'A1', '5.m4s'] # Should be available after 36s
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=34)
d = dash_proxy.get_media(dp)
self.assertEqual(d['ok'], False)
def testMediaSegmentTooEarlyWithAST(self):
urlParts = ['pdash', 'start_6', 'testpic', 'A1', '0.m4s'] # Should be available after 12s
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10)
d = dash_proxy.get_media(dp)
self.assertEqual(d['ok'], False)
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=14)
d = dash_proxy.get_media(dp)
self.assertEqual(len(d), 40346) # A full media segment
def testMediaSegmentBeforeTimeShiftBufferDepth(self):
now = 1356999060
segment = "%d.m4s" % ((now-330)/6)
urlParts = ['pdash', 'testpic', 'A1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
self.assertEqual(d['ok'], False)
def testLastMediaSegment(self):
"""With total duration of 2100, the last segment shall be 349
(independent of start) and available at 4101 start+dur_1800+dur_300."""
urlParts = ['pdash', 'start_2000', 'dur_1800', 'dur_300', 'testpic', 'A1', '349.m4s']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT, now=4101)
d = dash_proxy.get_media(dp)
# print "LMSG at %d" % d.find("lmsg")
self.assertEqual(d.find(b"lmsg"), 24)
def testMultiPeriod(self):
testOutputFile = "multiperiod.mpd"
rm_outfile(testOutputFile)
urlParts = ['pdash', 'periods_10', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=3602)
d = mpd_proxy.get_mpd(dp)
write_data_to_outfile(d.encode('utf-8'), testOutputFile)
periodPositions = findAllIndexes("')
direct_pos = d.find('http://streamtest.eu/pdash/testpic/"), 0)
def testInit(self):
urlParts = ['pdash', 'testpic', 'en', 'A1', 'init.mp4']
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = dash_proxy.get_init(dp)
self.assertEqual(len(d), 617)
def testMediaSegment(self):
testOutputFile = "t2.m4s"
rm_outfile(testOutputFile)
now = 1356998460
segment = "%d.m4s" % ((now-60)//6)
urlParts = ['pdash', 'testpic', 'en', 'A1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
write_data_to_outfile(d, testOutputFile)
class TestTfdt(unittest.TestCase):
"Test that the tfdt rewrite is working correctly"
def testMediaSegment(self):
testOutputFile = "tfdt.m4s"
rm_outfile(testOutputFile)
now = 1356998460
segment = "%d.m4s" % ((now-60)//6)
urlParts = ['pdash', 'testpic', 'A1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
write_data_to_outfile(d, testOutputFile)
def testTfdtValueFromZero(self):
"Tfdt value = mediaPresentationTime which corresponds to segmentNr*duration"
now = 1393936560
segNr = 232322749
segment = "%d.m4s" % segNr
urlParts = ['pdash', 'testpic', 'V1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
rm_outfile('tmp.m4s')
tmp_name = join(OUT_DIR, 'tmp.m4s')
with open(tmp_name, 'wb') as ofh:
ofh.write(d)
mf = MediaSegmentFilter(tmp_name)
mf.filter()
rm_outfile('tmp.m4s')
tfdtValue = mf.tfdt_value
presentationTime = tfdtValue // 90000
segmentTime = segNr * 6
self.assertEqual(presentationTime, segmentTime)
def testThatNoPresentationTimeOffsetForTfdt32(self):
now = 1393936560
# segNr = 232322749
urlParts = ['pdash', 'tfdt_32', 'testpic', 'V1', 'Manifest.mpd']
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = mpd_proxy.get_mpd(dp)
self.assertFalse(d.find('presentationTimeOffset') > 0)
class TestInitMux(unittest.TestCase):
def testInitMux(self):
testOutputFile = "test_mux_init.mp4"
rm_outfile(testOutputFile)
now = 1356998460
urlParts = ['pdash', 'testpic', 'V1__A1', "init.mp4"]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_init(dp)
write_data_to_outfile(d, testOutputFile)
def testMediaMux(self):
testOutputFile = "test_mux.m4s"
rm_outfile(testOutputFile)
now = 1356998460
segment = "%d.m4s" % ((now-60)//6)
urlParts = ['pdash', 'testpic', 'V1__A1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
write_data_to_outfile(d, testOutputFile)
class TestScte35Manifest(unittest.TestCase):
def setUp(self):
now = 1356998460
urlParts = ['pdash', 'scte35_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
self.mpd = mpd_proxy.get_mpd(dp)
def test_scte35_profile_presence(self):
self.assertTrue(self.mpd.find(",http://dashif.org/guidelines/adin/app") > 0)
def test_inband_stream_signal(self):
self.assertTrue(self.mpd.find(' 0)
class TestScte35Segments(unittest.TestCase):
def testScte35Event(self):
testOutputFile = "seg_scte35.m4s"
rm_outfile(testOutputFile)
segDur = 6
segNr = 1800000
now = segNr*segDur+50
segment = "%d.m4s" % segNr
urlParts = ['pdash', 'scte35_3', 'testpic', 'V1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
self.assertEqual(d.find(b'emsg'), 28)
write_data_to_outfile(d, testOutputFile)
def testNoScte35Event(self):
segDur = 6
segNr = 1800001
now = segNr*segDur+50
segment = "%d.m4s" % segNr
urlParts = ['pdash', 'scte35_1', 'testpic', 'V1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
self.assertEqual(d.find(b'emsg'), -1)
================================================
FILE: dashlivesim/tests/test_earlyterminatedperiod.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.dashlib import mpdprocessor
import xml.etree.ElementTree as ET
class TestXlinkPeriod(unittest.TestCase):
def setUp(self):
self.old_set_baseurl_value = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = True
def tearDown(self):
mpdprocessor.SET_BASEURL = self.old_set_baseurl_value
def testMpdPeriodReplaced(self):
" Check whether appropriate periods have been replaced by in .mpd file"
collectresult = 0
for k in [1, 2, 5, 10, 20, 30]:
nr_period_per_hour = 60
nr_etp_periods_per_hour = k
urlParts = ['livesim', 'periods_%s' % nr_period_per_hour, 'etp_%s' % nr_etp_periods_per_hour,
'testpic_2s', 'Manifest.mpd']
dp = dash_proxy.DashProvider("10.4.247.98", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10000)
d = mpd_proxy.get_mpd(dp)
xml = ET.fromstring(d)
# Make the string as a xml document.
periods_containing_duration_attribute = []
# This array would contain all the period id that have duration attributes.
# In the following, we will check if the correct period element have been assigned duration attributes.
for child in xml.findall('{urn:mpeg:dash:schema:mpd:2011}Period'): # Collect all period elements first
if 'duration' in child.attrib: # If the period element has the duration attribute.
periods_containing_duration_attribute.append(child.attrib['id'])
# Then collect its period id in this array
one_etp_for_how_many_periods = nr_period_per_hour/nr_etp_periods_per_hour
checker_array = [int(x[1:]) % one_etp_for_how_many_periods for x in periods_containing_duration_attribute]
# In the above line, we check if each period id evaluates to zero or not.
# Ideally, if everything worked well, then the checker array would be all zero.
collectresult = collectresult + sum(checker_array)
# Here, we keep collecting the sum of checker array. Even if one element evaluates to non zero values, then
# the whole test will fail.
self.assertTrue(collectresult == 0)
================================================
FILE: dashlivesim/tests/test_initsegmentfilter.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from os.path import join
from dashlivesim.tests.dash_test_util import CONTENT_ROOT
from dashlivesim.dashlib import initsegmentfilter
class TestInitParsing(unittest.TestCase):
def setUp(self):
fileName = join(CONTENT_ROOT, "testpic/A1/init.mp4")
self.f = initsegmentfilter.InitFilter(fileName)
self.f.filter()
def testTrackTimeScale(self):
self.assertEqual(self.f.track_timescale, 48000)
def testTrackHdlrType(self):
self.assertEqual(self.f.handler_type, b'soun')
================================================
FILE: dashlivesim/tests/test_lowlatency.py
================================================
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import mpd_proxy, dash_proxy
from dashlivesim.dashlib import mpdprocessor
class TestLowLatencyMPD(unittest.TestCase):
"Test that low-latency MPD has the right attributes"
def setUp(self):
self.oldBaseUrlState = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = False
def tearDown(self):
mpdprocessor.SET_BASEURL = self.oldBaseUrlState
def testCorrectFieldsInMPD(self):
mpdprocessor.SET_BASEURL = True
urlParts = ['livesim', 'chunkdur_1', 'ato_7', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
# print(d)
self.assertTrue(d.find('UTCTiming') > 0,
"Should find UTC-timing element")
self.assertTrue(d.find('http://www.dashif.org/guidelines/low-latency-live-v5') > 0,
"Should find low-latency profile")
self.assertTrue(d.find("http://streamtest.eu/livesim/chunkdur_1/ato_7/testpic/") > 0,
"Should not have availabilityTimeComplete here")
self.assertTrue(d.find('availabilityTimeComplete="false"') > 0,
"Should find availabilityTimeComplete in SegmentTemplate")
self.assertTrue(d.find('availabilityTimeOffset="7.000000"') > 0,
"Should find availabilityTimeOffset in SegmentTemplate")
self.assertTrue(d.find(' 0,
"Should find ServiceDescription in MPD")
self.assertTrue(d.find(' 0,
"Should find ProducerReferenceTime in MPD")
================================================
FILE: dashlivesim/tests/test_moduloperiod.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.dashlib.moduloperiod import ModuloPeriod
class TestModuloCalculations(unittest.TestCase):
def testMiddlePeriod(self):
mp = ModuloPeriod(10, 2000)
self.assertEqual(mp._minimum_update_period, 30)
self.assertEqual(mp._availability_start_time, 1800)
self.assertEqual(mp._media_presentation_duration, 360)
self.assertEqual(mp._availability_end_time, 2190)
def testEndOfMediaInPeriod(self):
mp = ModuloPeriod(5, 540)
self.assertEqual(mp._minimum_update_period, 15)
self.assertEqual(mp._availability_start_time, 300)
self.assertEqual(mp._media_presentation_duration, 240)
self.assertEqual(mp._availability_end_time, 555)
self.assertEqual(mp.compare_with_last_segment(269, 2), 0)
def testFuturePeriod(self):
mp = ModuloPeriod(5, 575)
self.assertEqual(mp._minimum_update_period, 15)
self.assertEqual(mp._availability_start_time, 600)
self.assertEqual(mp._media_presentation_duration, 60)
self.assertEqual(mp._availability_end_time, 675)
================================================
FILE: dashlivesim/tests/test_mpdcallback.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.dashlib import mpdprocessor
import xml.etree.ElementTree as ET
class TestXlinkPeriod(unittest.TestCase):
def setUp(self):
self.old_set_baseurl_value = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = True
def tearDown(self):
mpdprocessor.SET_BASEURL = self.old_set_baseurl_value
def testMpdPeriodReplaced(self):
" Check whether appropriate periods have been replaced by in .mpd file"
collectresult = 0
for k in [1, 2, 5, 10, 20, 30]:
nr_period_per_hour = 60
nr_callback_periods_per_hour = k
urlParts = ['livesim', 'periods_%s' % nr_period_per_hour, 'mpdcallback_%s' % nr_callback_periods_per_hour,
'testpic_2s', 'Manifest.mpd']
dp = dash_proxy.DashProvider("10.4.247.98", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10000)
d = mpd_proxy.get_mpd(dp)
xml = ET.fromstring(d)
# Make the string as a xml document.
periods_containing_callback_element = []
# This array would contain all the period id that have duration attributes.
# In the following, we will check if the correct period element have been assigned duration attributes.
for child in xml.findall('{urn:mpeg:dash:schema:mpd:2011}Period'): # Collect all period elements first
eventStream = child.find('{urn:mpeg:dash:schema:mpd:2011}EventStream')
if eventStream is None:
continue
periods_containing_callback_element.append(child.attrib['id'])
event = eventStream.find('{urn:mpeg:dash:schema:mpd:2011}Event')
self.assertIsNotNone(event, "there should be an Event inside EventStream")
self.assertTrue("/mpdcallback/" in event.attrib['messageData'])
# Then collect its period id in this array
one_callback_for_how_many_periods = nr_period_per_hour/nr_callback_periods_per_hour
checker_array = [int(x[1:]) % one_callback_for_how_many_periods for x in periods_containing_callback_element]
# In the above line, we check if each period id evaluates to zero or not.
# Ideally, if everything worked well, then the checker array would be all zero.
collectresult = collectresult + sum(checker_array)
# Here, we keep collecting the sum of checker array. Even if one element evaluates to non zero values, then
# the whole test will fail.
self.assertTrue(collectresult == 0)
================================================
FILE: dashlivesim/tests/test_mpdprocessor.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from os.path import join
from dashlivesim.tests.dash_test_util import CONTENT_ROOT
from dashlivesim.dashlib import mpdprocessor
vodMPD = join(CONTENT_ROOT, "testpic", "Manifest.mpd")
class FakeConfig(object):
def __init__(self):
pass
class TestMpdProcessor(unittest.TestCase):
"Test of MPD parsing and writing"
def setUp(self):
self.mpd_cfg = {'scte35Present': False, 'utc_timing_methods': [],
'utc_head_url': "", 'continuous': False,
'segtimeline': False, 'segtimeline_nr': False,
'now': 100000}
def test_mpd_in_out(self):
mp = mpdprocessor.MpdProcessor(vodMPD, self.mpd_cfg)
mp.process({'availabilityStartTime': "1971", 'availability_start_time_in_s': 31536000,
'BaseURL': "http://india/", 'minimumUpdatePeriod': "0", 'periodOffset': 100000},
[{'id': "p0", 'startNumber': "0", 'presentationTimeOffset': 0},
{'id': "p1", 'startNumber': "3600", 'presentationTimeOffset': 100000}])
xml = mp.get_full_xml()
def test_utc_timing_head(self):
self.mpd_cfg['utc_timing_methods'] = ["head"]
mp = mpdprocessor.MpdProcessor(vodMPD, self.mpd_cfg)
mp.process({'availabilityStartTime': "1971", 'availability_start_time_in_s': 31536000,
'BaseURL': "http://india/", 'minimumUpdatePeriod': "0", 'periodOffset': 100000},
[{'id': "p0", 'startNumber': "0", 'presentationTimeOffset': 0}])
xml = mp.get_full_xml()
head_pos = xml.find('", d)
ud_indexes = findAllIndexes("baseurl_u40_d20", d)
du_indexes = findAllIndexes("baseurl_d40_u20", d)
self.assertEqual(len(baseURLindexes), 2)
self.assertEqual(len(ud_indexes), 1)
self.assertEqual(len(du_indexes), 1)
def testMpdGenerationHttps(self):
urlParts = ['livesim', 'baseurl_u40_d20', 'baseurl_d40_u20', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0,
is_https=True)
d = mpd_proxy.get_mpd(dp)
httpsIndexes = findAllIndexes("https://", d)
self.assertEqual(len(httpsIndexes), 2)
def testCheckUpAndDownDependingOnTime(self):
urlParts = ['livesim', 'ato_inf', 'baseurl_u40_d20', 'testpic', 'A1', '0.m4s']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=68)
self.assertTrue(isMediaSegment(dash_proxy.get_media(dp)))
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=108)
self.assertFalse(isMediaSegment(dash_proxy.get_media(dp)))
def testCheckDowAndUpDependingOnTime(self):
urlParts = ['livesim', 'ato_inf', 'baseurl_d40_u20', 'testpic', 'A1', '0.m4s']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=68)
self.assertFalse(isMediaSegment(dash_proxy.get_media(dp)))
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=108)
self.assertTrue(isMediaSegment(dash_proxy.get_media(dp)))
def testCheckDowAndUpDependingOnTime30sPeriod(self):
urlParts = ['livesim', 'ato_inf', 'baseurl_d20_u10', 'testpic', 'A1', '0.m4s']
expected_results = [False, False, True, False, False, True]
times = [7, 17, 27, 37, 47, 57]
for (exp, now) in zip(expected_results, times):
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
self.assertEqual(isMediaSegment(dash_proxy.get_media(dp)), exp, "Did not match for time %s" % now)
def testCheckUpAndDownDependingOnTime30sPeriod(self):
urlParts = ['livesim', 'ato_inf', 'baseurl_u20_d10', 'testpic', 'A1', '0.m4s']
expected_results = [True, True, False, True, True, False]
times = [7, 17, 27, 37, 47, 57]
for (exp, now) in zip(expected_results, times):
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
self.assertEqual(isMediaSegment(dash_proxy.get_media(dp)), exp, "Did not match for time %s" % now)
def testOtherOrderOfOptions(self):
urlParts = ['livesim', 'baseurl_u20_d10', 'ato_inf', 'testpic', 'A1', '0.m4s']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10)
d = dash_proxy.get_media(dp)
self.assertTrue(isMediaSegment(d), "Not a media segment, but %r" % d)
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=25)
self.assertFalse(isMediaSegment(dash_proxy.get_media(dp)), "Is a media segment, but should not be")
================================================
FILE: dashlivesim/tests/test_scte35.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.dashlib import scte35
testMessage = """\
"""
cancelMessage = """\
"""
class TestScte35(unittest.TestCase):
def testScte35MessageData(self):
ptsAdjustment = 0
tier = 4095
spliceEventId = 22
spliceEventCancelIndicator = False
outOfNetworkIndicator = False
uniqueProgramId = 0
availNum = 0
availsExpected = 0
spliceImmediateFlag = False
ptsTime = 1234
autoReturn = True
duration = 900000
message_data = scte35.create_scte35_insert_message(ptsAdjustment, tier, spliceEventId,
spliceEventCancelIndicator, outOfNetworkIndicator,
uniqueProgramId, availNum, availsExpected,
spliceImmediateFlag, ptsTime, autoReturn, duration)
self.assertEqual(message_data, testMessage)
def testScteCancelMessage(self):
ptsAdjustment = 0
tier = 4095
spliceEventId = 22
spliceEventCancelIndicator = True
outOfNetworkIndicator = False
uniqueProgramId = 0
availNum = 0
availsExpected = 0
spliceImmediateFlag = False
ptsTime = 1234
autoReturn = True
duration = 900000
message_data = scte35.create_scte35_insert_message(ptsAdjustment, tier, spliceEventId,
spliceEventCancelIndicator, outOfNetworkIndicator,
uniqueProgramId, availNum, availsExpected,
spliceImmediateFlag, ptsTime, autoReturn, duration)
self.assertEqual(message_data, cancelMessage)
class TestEmsgMessage(unittest.TestCase):
def testEmsgMessage(self):
timeScale = 90000
presentationTimeOffset = 1000000000000
presentationTime = 1000001800000
duration = 900000
messageId = 18
spliceId = 13
emsgBox = scte35.Scte35Emsg(timeScale, presentationTimeOffset, presentationTime, duration, messageId, spliceId)
self.assertEqual(emsgBox.presentation_time_delta, presentationTime-presentationTimeOffset)
self.assertEqual(emsgBox.emsg_id, messageId)
def testNonAllowedTimescale(self):
timeScale = 36000
presentationTimeOffset = 1000000000000
presentationTime = 1000001800000
duration = 900000
messageId = 18
spliceId = 13
self.assertRaises(scte35.Scte35Error, scte35.Scte35Emsg, timeScale, presentationTimeOffset, presentationTime,
duration, messageId, spliceId)
================================================
FILE: dashlivesim/tests/test_segment_duration_ms.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2018, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR
from dashlivesim.dashlib import configprocessor
class TestConfigProcessor(unittest.TestCase):
def testReadVodConfigFile(self):
cfg_file = os.path.join(VOD_CONFIG_DIR, 'testpic.cfg')
vod_cfg = configprocessor.VodConfig()
vod_cfg.read_config(cfg_file)
================================================
FILE: dashlivesim/tests/test_segmentloss_mainlive.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.tests.dash_test_util import write_data_to_outfile
from dashlivesim.dashlib import dash_proxy, mpd_proxy
# from dashlivesim.dashlib import mpdprocessor
def isEmsgPresentInSegment(data):
"Check if emsg box is present in segment."
return data.find(b"emsg") >= 0
class TestSegTimelineLossMainLive(unittest.TestCase):
"Test of Segment timeline loss signalling in MPD and segments for main live case"
# def setUp(self):
# self.oldBaseUrlState = mpdprocessor.SET_BASEURL
# mpdprocessor.SET_BASEURL = True
# def tearDown(self):
# mpdprocessor.SET_BASEURL = self.oldBaseUrlState
def testNoInbandStreamElemInMPD(self):
urlParts = ['livesim', 'baseurl_u10_d20', 'segtimeline_1', 'segtimelineloss_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=60)
d = mpd_proxy.get_mpd(dp)
inbandEventElement = d.find('')
end = d.find('')
testOutputFile = "SegTimeline1.txt"
segTimeline = d[start:end+18]
write_data_to_outfile(d[start:end+18].encode('utf-8'), testOutputFile)
dp2 = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=15)
d2 = mpd_proxy.get_mpd(dp2)
start2 = d2.find('')
end2 = d2.find('')
testOutputFile = "SegTimeline2.txt"
segTimeline2 = d2[start2:end2+18]
write_data_to_outfile(d2[start2:end2+18].encode('utf-8'), testOutputFile)
self.assertEqual(segTimeline, segTimeline2)
def testNewSegmentsAdded(self):
urlParts = ['livesim', 'baseurl_u10_d20', 'segtimeline_1', 'segtimelineloss_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10)
d = mpd_proxy.get_mpd(dp)
start = d.find('')
end = d.find('')
testOutputFile = "SegTimeline3.txt"
write_data_to_outfile(d[start:end+18].encode('utf-8'), testOutputFile)
segTimeline = d[start:end+18]
dp2 = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=31)
d2 = mpd_proxy.get_mpd(dp2)
start2 = d2.find('')
end2 = d2.find('')
testOutputFile = "SegTimeline4.txt"
write_data_to_outfile(d2[start2:end2+18].encode('utf-8'), testOutputFile)
segTimeline2 = d2[start2:end2+18]
self.assertNotEqual(segTimeline, segTimeline2)
================================================
FILE: dashlivesim/tests/test_segmentmuxer.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from os.path import join
from dashlivesim.tests.dash_test_util import rm_outfile, write_data_to_outfile, CONTENT_ROOT
from dashlivesim.dashlib import segmentmuxer
V1_INIT = join(CONTENT_ROOT, "testpic/V1/init.mp4")
A1_INIT = join(CONTENT_ROOT, "testpic/A1/init.mp4")
V1_1 = join(CONTENT_ROOT, "testpic/V1/1.m4s")
A1_1 = join(CONTENT_ROOT, "testpic/A1/1.m4s")
class TestInitMuxing(unittest.TestCase):
def testInitMuxing(self):
testOutputFile = "init_muxed.mp4"
rm_outfile(testOutputFile)
mi = segmentmuxer.MultiplexInits(V1_INIT, A1_INIT)
muxed = mi.construct_muxed()
write_data_to_outfile(muxed, testOutputFile)
class TestSegmentMuxing(unittest.TestCase):
def testFragmentMuxing(self):
testOutputFile = "1_fmux.mp4s"
rm_outfile(testOutputFile)
ml = segmentmuxer.MultiplexMediaSegments(V1_1, A1_1)
fmux = ml.mux_on_fragment_level()
write_data_to_outfile(fmux, testOutputFile)
def testSampleMuxing(self):
testOutputFile = "1_smux.m4s"
rm_outfile(testOutputFile)
ml = segmentmuxer.MultiplexMediaSegments(V1_1, A1_1)
smux = ml.mux_on_sample_level()
write_data_to_outfile(smux, testOutputFile)
================================================
FILE: dashlivesim/tests/test_segmenttimeline.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from xml.etree import ElementTree
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT, rm_outfile, write_data_to_outfile
from dashlivesim.dashlib import dash_proxy, mpd_proxy
NAMESPACE = 'urn:mpeg:dash:schema:mpd:2011'
def node_ns(name):
return '{%s}%s' % (NAMESPACE, name)
class TestMPDWithSegmentTimeline(unittest.TestCase):
"Test that the MPD looks correct when segtimeline_1 is defined."
def setUp(self):
self.now = 6003
self.tsbd = 30
urlParts = ['livesim', 'segtimeline_1', 'tsbd_%d' % self.tsbd, 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
def testThatNumberTemplateFeaturesAreAbsent(self):
testOutputFile = "segtimeline.mpd"
rm_outfile(testOutputFile)
write_data_to_outfile(self.d.encode('utf-8'), testOutputFile)
self.assertTrue(self.d.find("startNumber") == -1) # There should be no startNumber in the MPD
self.assertTrue(self.d.find("duration") == -1) # There should be no duration in the segmentTemplate
self.assertTrue(self.d.find("$Number$") == -1) # There should be no $Number$ in template
self.assertTrue(self.d.find("maxSegmentDuration") == -1) # There should be no maxSegmentDuration in MPD
def testThatSegmentTimeLineDataIsPresent(self):
testOutputFile = "segtimeline.mpd"
rm_outfile(testOutputFile)
write_data_to_outfile(self.d.encode('utf-8'), testOutputFile)
self.assertTrue(self.d.find("$Time$") > 0) # There should be $Time$ in the MPD
def testThatTheLastSegmentReallyIsTheLatest(self):
"Check that the last segment's end is less than one duration from now."
period = self.root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
timescale = int(segment_template.attrib['timescale'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
s_elements = segment_timeline.findall(node_ns('S'))
seg_start_time = None
seg_end_time = None
for s_elem in s_elements:
if seg_start_time is None:
seg_start_time = int(s_elem.attrib['t'])
else:
seg_start_time = seg_end_time
nr_repeat = int(s_elem.attrib.get('r', 0))
duration = int(s_elem.attrib['d'])
seg_end_time = seg_start_time + duration * (1 + nr_repeat)
last_end_time = seg_end_time / timescale
self.assertLess(last_end_time, self.now)
last_end_time_plus_duration = (seg_end_time + duration)/timescale
self.assertGreater(last_end_time_plus_duration, self.now)
def testThatTheLastSegmentReallyIsTheLatestAtWrapAround(self):
"Check that the last segment's end is less than one duration from now at wraparound."
self.now = 3603
self.tsbd = 30
urlParts = ['livesim', 'segtimeline_1', 'tsbd_%d' % self.tsbd, 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
period = self.root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
timescale = int(segment_template.attrib['timescale'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
s_elements = segment_timeline.findall(node_ns('S'))
seg_start_time = None
seg_end_time = None
for s_elem in s_elements:
if seg_start_time is None:
seg_start_time = int(s_elem.attrib['t'])
else:
seg_start_time = seg_end_time
nr_repeat = int(s_elem.attrib.get('r', 0))
duration = int(s_elem.attrib['d'])
seg_end_time = seg_start_time + duration * (1 + nr_repeat)
last_end_time = seg_end_time / timescale
self.assertLess(last_end_time, self.now)
last_end_time_plus_duration = (seg_end_time + duration)/timescale
self.assertGreater(last_end_time_plus_duration, self.now)
def testThatFirstSegmentStartsJustBeforeTsbd(self):
"Check that the first segment starts less than one period before now-tsbd."
period = self.root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
timescale = int(segment_template.attrib['timescale'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
first_s_elem = segment_timeline.find(node_ns('S'))
first_start = int(first_s_elem.attrib['t'])
duration = int(first_s_elem.attrib['d'])
start_time = first_start / timescale
start_time_plus_duration = (first_start + duration) / timescale
self.assertLess(start_time, self.now - self.tsbd)
self.assertGreater(start_time_plus_duration, self.now - self.tsbd)
def find_first_audio_t(root):
"""Return t and d from the first audio segment."""
period = root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
content_type = adaptation_set.attrib['contentType']
if content_type != 'audio':
continue
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
# timescale = int(segment_template.attrib['timescale'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
first_s_elem = segment_timeline.find(node_ns('S'))
first_t = int(first_s_elem.attrib['t'])
first_d = int(first_s_elem.attrib['d'])
return first_t, first_d
raise ValueError("Could not find audio adaptation set")
class TestAvoidJump(unittest.TestCase):
def testThatTimesDontJump(self):
"Test that times don't jump as reported in ISSUE #91."
# First get the MPD corresponding to 5.mpd.txt
now = 1578681199
urlParts = ['livesim', 'segtimeline_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = mpd_proxy.get_mpd(dp)
root = ElementTree.fromstring(d)
first_t, first_d = find_first_audio_t(root)
# tsbd = 300 # TimeShiftBufferDepth
self.assertTrue(now - first_t/48000 > 300, "Did not get before timeshift window start")
later = now + 6
urlParts = ['livesim', 'segtimeline_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=later)
d = mpd_proxy.get_mpd(dp)
root = ElementTree.fromstring(d)
second_t, second_d = find_first_audio_t(root)
self.assertEqual(second_t, first_t + first_d, "Second t is not first t + first d ")
class TestMPDWithSegmentTimelineWrap(unittest.TestCase):
"Test that the MPD looks correct when wrapping."
def testAfterWrap(self):
self.now = 3610
self.tsbd = 60
urlParts = ['livesim', 'segtimeline_1', 'tsbd_%d' % self.tsbd, 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
nrSegments = self.getNrSegments(self.root)
self.assertEqual(2*10, nrSegments)
write_data_to_outfile(self.d.encode('utf-8'), "AfterWrap.mpd")
def testBefore(self):
self.now = 3590
self.tsbd = 60
urlParts = ['livesim', 'segtimeline_1', 'tsbd_%d' % self.tsbd,
'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None,
VOD_CONFIG_DIR, CONTENT_ROOT,
now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
nrSegments = self.getNrSegments(self.root)
self.assertEqual(2*10, nrSegments)
write_data_to_outfile(self.d.encode('utf-8'), "BeforeWrap.mpd")
def getNrSegments(self, root):
nrSegments = 0
period = root.find(node_ns('Period'))
aSets = period.findall(node_ns('AdaptationSet'))
for aSet in aSets:
sTempl = aSet.find(node_ns('SegmentTemplate'))
sLines = sTempl.findall(node_ns('SegmentTimeline'))
for sLine in sLines:
sElems = sLine.findall(node_ns('S'))
for sElem in sElems:
if 'r' in sElem.attrib:
nrSegments += int(sElem.attrib['r']) + 1
else:
nrSegments += 1
return nrSegments
class TestSegmentTimelineInterval(unittest.TestCase):
"""SegmentTimeline with start, stop, timeoffset"""
def setUp(self):
self.now = 100
urlParts = ['livesim', 'segtimeline_1', 'start_60', 'stop_120',
'timeoffset_0', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
def testSegmentList(self):
period = self.root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
content_type = adaptation_set.attrib['contentType']
if content_type != "video":
continue
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
timescale = int(segment_template.attrib['timescale'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
s_elements = segment_timeline.findall(node_ns('S'))
seg_start_time = None
seg_end_time = None
for s_elem in s_elements:
if seg_start_time is None:
seg_start_time = int(s_elem.attrib['t'])
self.assertEqual(60 * timescale, seg_start_time)
else:
seg_start_time = seg_end_time
nr_repeat = int(s_elem.attrib.get('r', 0))
duration = int(s_elem.attrib['d'])
seg_end_time = seg_start_time + duration * (1 + nr_repeat)
last_end_time = seg_end_time / timescale
self.assertLess(last_end_time, self.now)
last_end_time_plus_duration = (seg_end_time + duration)/timescale
self.assertGreater(last_end_time_plus_duration, self.now)
class TestMultiPeriodSegmentTimeline(unittest.TestCase):
"Test that the MPD looks correct when segtimeline_1 and periods_60 are both defined."
def setUp(self):
self.now = 6003
self.tsbd = 90
urlParts = ['livesim', 'segtimeline_1', 'periods_60', 'tsbd_%d' % self.tsbd, 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
def testThatThereAreMultiplePeriods(self):
"Check that the first segment starts less than one period before now-tsbd."
testOutputFile = "segtimeline_periods.mpd"
rm_outfile(testOutputFile)
write_data_to_outfile(self.d.encode('utf-8'), testOutputFile)
self.root = ElementTree.fromstring(self.d)
periods = self.root.findall(node_ns('Period'))
self.assertGreater(len(periods), 1)
class TestMediaSegments(unittest.TestCase):
"Test that media segments are served properly."
def setUp(self):
self.seg_nr = 349
self.timescale = 48000
self.duration = 6
self.seg_time = self.seg_nr * self.duration * self.timescale
self.now = (self.seg_nr+2)*self.duration
def testThatTimeLookupWorks(self):
urlParts = ['livesim', 'segtimeline_1', 'testpic', 'A1', 't%d.m4s' % self.seg_time]
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
d = dash_proxy.get_media(dp)
self.assertTrue(isinstance(d, bytes), "A segment is returned")
def testThatTimeSegmentIsSameAsNumber(self):
urlParts = ['livesim', 'segtimeline_1', 'testpic', 'A1', 't%d.m4s' % self.seg_time]
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
time_seg = dash_proxy.get_media(dp)
urlParts = ['livesim', 'segtimeline_1', 'testpic', 'A1', '%d.m4s' % self.seg_nr]
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
nr_seg = dash_proxy.get_media(dp)
self.assertEqual(len(time_seg), len(nr_seg))
self.assertEqual(time_seg, nr_seg)
class TestMPDWithSegmentTimelineNumber(unittest.TestCase):
"Test that the MPD looks correct when segtimelinenr_1 is defined."
def setUp(self):
self.now = 6003
self.tsbd = 30
urlParts = ['livesim', 'segtimelinenr_1', 'tsbd_%d' % self.tsbd,
'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("server.org", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=self.now)
self.d = mpd_proxy.get_mpd(dp)
self.root = ElementTree.fromstring(self.d)
def testThatSomeFeaturesAreAbsent(self):
testOutputFile = "segtimelinenr.mpd"
rm_outfile(testOutputFile)
write_data_to_outfile(self.d.encode('utf-8'), testOutputFile)
self.assertTrue(self.d.find("duration") == -1) # There should be no duration in the segmentTemplate
self.assertTrue(self.d.find("$Time$") == -1) # There should be no
# $Number$ in template
self.assertTrue(self.d.find("maxSegmentDuration") == -1) # There should be no maxSegmentDuration in MPD
def testThatSegmentTimeLineDataIsPresent(self):
testOutputFile = "segtimelinenr.mpd"
rm_outfile(testOutputFile)
write_data_to_outfile(self.d.encode('utf-8'), testOutputFile)
self.assertTrue(self.d.find("$Number$") > 0, "$Number$ missing")
def testThatFirstSegmentHasRightNumber(self):
"Check that the first segment has the right number."
duration_in_s = 6
period = self.root.find(node_ns('Period'))
for adaptation_set in period.findall(node_ns('AdaptationSet')):
segment_template = adaptation_set.find(node_ns('SegmentTemplate'))
timescale = int(segment_template.attrib['timescale'])
start_number = int(segment_template.attrib['startNumber'])
segment_timeline = segment_template.find(node_ns('SegmentTimeline'))
first_s_elem = segment_timeline.find(node_ns('S'))
first_start = int(first_s_elem.attrib['t'])
duration = duration_in_s * timescale
start_nr_from_time = int(round(1.0 * first_start / duration))
self.assertEqual(start_number, start_nr_from_time)
================================================
FILE: dashlivesim/tests/test_startnr.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from os.path import join
from dashlivesim.tests.dash_test_util import OUT_DIR
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.tests.dash_test_util import findAllIndexes
class TestMpdChange(unittest.TestCase):
"Test that MPD gets startNr changed in an appropriate way"
def testMpdWithNormalStartNr(self):
"Check that startNumber=0."
urlParts = ['pdash', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
with open(join(OUT_DIR, 'tmp.mpd'), 'wb') as ofh:
ofh.write(d.encode('utf-8'))
self.assertEqual(len(findAllIndexes('startNumber="0"', d)), 2)
self.assertTrue(d.find('availabilityStartTime="1970-01-01T00:00:00Z"') > 0)
def testMpdWitdStartNrIs111(self):
"Check that startNumber=111."
urlParts = ['pdash', 'snr_111', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertEqual(len(findAllIndexes('startNumber="111"', d)), 2)
self.assertTrue(d.find('availabilityStartTime="1970-01-01T00:00:00Z"') > 0)
def testMpdWithStartNrIs1(self):
"Check that startNumber=1."
urlParts = ['pdash', 'snr_1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertEqual(len(findAllIndexes('startNumber="1"', d)), 2)
self.assertTrue(d.find('availabilityStartTime="1970-01-01T00:00:00Z"') > 0)
def testMpdWithImplicitStartNr(self):
"Check that startNumber is not present in MPD."
urlParts = ['pdash', 'snr_-1', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find('startNumber=') < 0)
self.assertTrue(d.find('availabilityStartTime="1970-01-01T00:00:00Z"') > 0)
# Could add tests to check availability time of segments depending on startNr
# Add test to check if segmentNumber and tfdt are OK depending on startNr.
# Just running the reference player with different values seems to show that it is working properly, though.
================================================
FILE: dashlivesim/tests/test_subtitles.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.tests.dash_test_util import rm_outfile, write_data_to_outfile, VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import ttml_timing_offset
from dashlivesim.dashlib import dash_proxy, mpd_proxy
TEST_STRING_1 = b'< begin="01:02:03.1234" end="10:59:43:29" >'
TEST_STRING_SEG_NR = b'... Segment # 12 ...'
class TestTtmlTimingChange(unittest.TestCase):
"Test that TTML string is changed properly."
def testNoChange(self):
"Offset is 0 seconds."
outString = ttml_timing_offset.adjust_ttml_content(TEST_STRING_1, 0, None)
self.assertEqual(outString, TEST_STRING_1)
def testAdd1Hour(self):
"Offset is 3600."
outString = ttml_timing_offset.adjust_ttml_content(TEST_STRING_1, 3600, None)
outGoal = b'< begin="02:02:03.1234" end="11:59:43:29" >'
self.assertEqual(outString, outGoal)
def testWrap(self):
"Add an offset that wraps."
outString = ttml_timing_offset.adjust_ttml_content(TEST_STRING_1, 360050, None)
outGoal = b'< begin="101:02:53.1234" end="111:00:33:29" >'
self.assertEqual(outString, outGoal)
class TestTtmlSegmentNrChange(unittest.TestCase):
"Test that TTML string is changed properly."
def testSetToRightNr(self):
"Output Nr should be what is input."
outString = ttml_timing_offset.adjust_ttml_content(TEST_STRING_SEG_NR, 360050, 22)
outGoal = b'... Segment # 22 ...'
self.assertEqual(outString, outGoal)
class TestSegmentModification(unittest.TestCase):
def testTtmlSegment(self):
testOutputFile = "sub.m4s"
rm_outfile(testOutputFile)
segmentNr = 718263000
segment = "%d.m4s" % segmentNr
now = segmentNr * 2 + 10
urlParts = ['livsim', 'ato_inf', 'testpic_stpp', 'S1', segment]
dp = dash_proxy.DashProvider("127.0.0.1", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=now)
d = dash_proxy.get_media(dp)
write_data_to_outfile(d, testOutputFile)
self.assertTrue(d.find(b'begin="399035:00:00.000"') > 0)
self.assertTrue(d.find(b'eng : UTC = 2015-07-10T11:00:00Z') > 0)
class TestMpdExtraction(unittest.TestCase):
def testStartNumber(self):
"Check that all 3 media components have startNumber=0"
urlParts = ['livesim', 'testpic_stpp', 'Manifest_stpp.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertEqual(d.count('startNumber="0'), 3)
================================================
FILE: dashlivesim/tests/test_suggested_presentation_delay.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2018, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
class TestSuggestedPresentationDelay(unittest.TestCase):
"Test that MPD gets startNr changed in an appropriate way"
def testSuggestedPresentationDelayNotPresent(self):
"Check that suggestedPresentationDelayIsNotPresent."
urlParts = ['pdash', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find('suggestedPresentationDelay') < 0)
def testSuggestedPresentationDelayPresent(self):
"Check that suggestedPresentationDelay get the right value."
urlParts = ['pdash', 'spd_10', 'testpic', 'Manifest.mpd']
dp = dash_proxy.DashProvider("streamtest.eu", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=0)
d = mpd_proxy.get_mpd(dp)
self.assertTrue(d.find('suggestedPresentationDelay="PT10S"') > 0)
================================================
FILE: dashlivesim/tests/test_ttml_update.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2020, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from dashlivesim.dashlib import ttml_timing_offset
TTML_IN = b'\d\d):(?P\d\d):(?P\d\d)')
# CONTENT_PATTERN_S = re.compile(rb'(?P\w+) : (?P\d\d):(?P\d\d):(?P\d\d)(\.\d+)?')
# CONTENT_PATTERN_SEGMENT = re.compile(rb'(?PSegment # )(?P\d+)')
class TestTTMLTimeUpdate(unittest.TestCase):
def testUpdateTTMLTime(self):
outbytes = ttml_timing_offset.adjust_ttml_content(TTML_IN, 22, 24)
self.assertEqual(outbytes, TTML_OUT)
================================================
FILE: dashlivesim/tests/test_xlinkperiod.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import unittest
from re import findall
from operator import mul
from functools import reduce
from dashlivesim.tests.dash_test_util import VOD_CONFIG_DIR, CONTENT_ROOT
from dashlivesim.dashlib import dash_proxy, mpd_proxy
from dashlivesim.dashlib import mpdprocessor
class TestXlinkPeriod(unittest.TestCase):
def setUp(self):
self.old_set_baseurl = mpdprocessor.SET_BASEURL
mpdprocessor.SET_BASEURL = True
def tearDown(self):
mpdprocessor.SET_BASEURL = self.old_set_baseurl
def testMpdPeriodReplaced(self):
" Check whether appropriate periods have been replaced by in .mpd file"
collectresult = 1
for k in [1, 2, 5, 10]:
nr_period_per_hour = 10
nr_xlink_periods_per_hour = k
urlParts = ['livesim', 'periods_%s' % nr_period_per_hour, 'xlink_%s' % nr_xlink_periods_per_hour,
'testpic_2s', 'Manifest.mpd']
dp = dash_proxy.DashProvider("10.4.247.98", urlParts, None, VOD_CONFIG_DIR, CONTENT_ROOT, now=10000)
d = mpd_proxy.get_mpd(dp)
period_id_all = findall('Period id="([^"]*)"', d)
# Find all period ids in the .mpd file returned.
# We will check whether the correct periods have been xlinked here.
one_xlinks_for_how_many_periods = nr_period_per_hour/nr_xlink_periods_per_hour
period_id_xlinks = [int(x[1:]) % one_xlinks_for_how_many_periods for x in period_id_all]
# All the period ids.
# If there were any periods, that were not supposed to be there,
# then one of the elements in period_id_xlinks would be zero.
result = reduce(mul, period_id_xlinks, 1)
collectresult = result * collectresult
self.assertTrue(collectresult != 0)
================================================
FILE: dashlivesim/tests/testpic/Manifest.mpd
================================================
Media Presentation Description by MobiTV. Powered by MDL Team@Sweden.
================================================
FILE: dashlivesim/tests/testpic_2s/Manifest.mpd
================================================
Media Presentation Description by MobiTV. Powered by MDL Team@Sweden.
================================================
FILE: dashlivesim/tests/testpic_stpp/Manifest_stpp.mpd
================================================
Media Presentation Description by MobiTV. Powered by MDL Team@Sweden.
================================================
FILE: dashlivesim/tests/vod_cfg/testpic.cfg
================================================
[General]
version = 1.1
[Setup]
first_segment_in_loop = 1
nr_segments_in_loop = 600
segment_duration_s = 6
default_tsbd_secs = 300
[video]
representations = V1
timescale = 90000
total_duration = 324000000
dat_file = testpic_video.dat
[audio]
representations = A1
timescale = 48000
total_duration = 172800000
dat_file = testpic_audio.dat
================================================
FILE: dashlivesim/tests/vod_cfg/testpic_2s.cfg
================================================
[Setup]
default_tsbd_secs = 300
segment_duration_s = 2
first_segment_in_loop = 1
nr_segments_in_loop = 1800
[audio]
representations = A48
timescale = 48000
[video]
representations = V300
timescale = 90000
[subtitles]
representations = S1,sub_eng,sub_swe,sub_eng_cap,S1_one_region,S1_two_regions,sub_ttml_qbb,sub_nor, S1_two_regions_multi_color
timescale = 1000
[General]
version = 1.0
================================================
FILE: dashlivesim/tests/vod_cfg/testpic_stpp.cfg
================================================
[General]
version = 1.0
[Setup]
first_segment_in_loop = 1
nr_segments_in_loop = 1800
segment_duration_s = 2
default_tsbd_secs = 300
[video]
representations = V1
timescale = 90000
[audio]
representations = A1
timescale = 48000
[subtitles]
representations = S1
timescale = 1000
================================================
FILE: dashlivesim/vodanalyzer/__init__.py
================================================
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: dashlivesim/vodanalyzer/dashanalyzer.py
================================================
"""Analyze DASH content in live profile and extract parameters for VoD-config file for live source simulator.
"""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
import os
import time
import re
from struct import pack
from dashlivesim.dashlib import configprocessor
from dashlivesim.dashlib import initsegmentfilter, mediasegmentfilter
from dashlivesim.vodanalyzer.mpdprocessor import MpdProcessor
DEFAULT_DASH_NAMESPACE = "urn:mpeg:dash:schema:mpd:2011"
MUX_TYPE_NONE = 0
MUX_TYPE_FRAGMENT = 1
MUX_TYPE_SAMPLES = 2
## Utility functions
def makeTimeStamp(t):
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t))
def makeDurationFromS(nrSeconds):
return "PT%dS" % nrSeconds
class DashAnalyzerError(Exception):
"""Error in DashAnalyzer."""
class DashAnalyzer(object):
def __init__(self, mpd_filepath, verbose=1):
self.mpd_filepath = mpd_filepath
path_parts = mpd_filepath.split('/')
self.base_name = 'content'
if len(path_parts) >= 2:
self.base_name = path_parts[-2]
self.config_filename = self.base_name + ".cfg"
self.base_path = os.path.split(mpd_filepath)[0]
self.verbose = verbose
self.as_data = {} # List of adaptation sets (one for each media)
self.muxedRep = None
self.muxedPaths = {}
self.mpdSegStartNr = -1
self.segDuration = None
self.firstSegmentInLoop = -1
self.lastSegmentInLoop = -1
self.nrSegmentsInLoop = -1
self.mpdProcessor = MpdProcessor(self.mpd_filepath)
self.loopTime = self.mpdProcessor.media_presentation_duration_in_s
def analyze(self):
self.initMedia()
self.checkAndUpdateMediaData()
self.write_config(self.config_filename)
def initMedia(self):
"Init media by analyzing the MPD and the media files."
for adaptation_set in self.mpdProcessor.get_adaptation_sets():
content_type = adaptation_set.content_type
if content_type is None:
print("No contentType for adaptation set")
sys.exit(1)
if content_type in self.as_data:
raise DashAnalyzerError("Multiple adaptation sets for contentType " + content_type)
as_data = {'as' : adaptation_set, 'reps' : []}
as_data['presentationDurationInS'] = self.mpdProcessor.media_presentation_duration_in_s
self.as_data[content_type] = as_data
for rep in adaptation_set.representations:
rep_data = {'representation' : rep, 'id' : rep.rep_id}
as_data['reps'].append(rep_data)
initPath = rep.initialization_path
rep_data['relInitPath'] = initPath
rep_data['absInitPath'] = os.path.join(self.base_path, initPath)
init_filter = initsegmentfilter.InitFilter(rep_data['absInitPath'])
init_filter.filter()
rep_data['trackID'] = init_filter.track_id
print("%s trackID = %d" % (content_type, rep_data['trackID']))
rep_data['relMediaPath'] = rep.get_media_path()
rep_data['absMediaPath'] = os.path.join(self.base_path, rep.get_media_path())
rep_data['default_sample_duration'] = \
init_filter.default_sample_duration
self.getSegmentRange(rep_data)
track_timescale = init_filter.track_timescale
if 'track_timescale' not in as_data:
as_data['track_timescale'] = track_timescale
elif track_timescale != as_data['track_timescale']:
raise DashAnalyzerError("Timescales not consistent between %s tracks" % content_type)
if self.verbose:
print("%s data: " % content_type)
for (k, v) in rep_data.items():
print(" %s=%s" % (k, v))
def getSegmentRange(self, rep_data):
"Search the directory for the first and last segment and set firstNumber and lastNumber for this MediaType."
rep_id = rep_data['id']
mediaDir, mediaName = os.path.split(rep_data['absMediaPath'])
mediaRegexp = mediaName.replace("%d", "(\d+)").replace(".", "\.")
mediaReg = re.compile(mediaRegexp)
files = os.listdir(mediaDir)
numbers = []
for f in files:
matchObj = mediaReg.match(f)
if matchObj:
number = int(matchObj.groups(1)[0])
numbers.append(number)
numbers.sort()
for i in range(1, len(numbers)):
if numbers[i] != numbers[i-1] + 1:
raise DashAnalyzerError("%s segment missing between %d and %d" % (rep_id, numbers[i-1], numbers[i]))
print("Found %s segments %d - %d" % (rep_id, numbers[0], numbers[-1]))
rep_data['firstNumber'] = numbers[0]
rep_data['lastNumber'] = numbers[-1]
def checkAndUpdateMediaData(self):
"""Check all segments for good values and return startTimes and total duration."""
lastGoodSegments = []
print("Checking all the media segment durations for deviations.")
def writeSegTiming(ofh, firstSegmentInRepeat, firstStartTimeInRepeat, duration, repeatCount):
data = pack(configprocessor.SEGTIMEFORMAT, firstSegmentInRepeat, repeatCount,
firstStartTimeInRepeat, duration)
ofh.write(data)
for content_type in self.as_data.keys():
as_data = self.as_data[content_type]
as_data['datFile'] = "%s_%s.dat" % (self.base_name, content_type)
adaptation_set = as_data['as']
print("Checking %s with timescale %d" % (content_type, as_data['track_timescale']))
if self.segDuration is None:
self.segDuration = adaptation_set.duration
else:
assert self.segDuration == adaptation_set.duration
track_timescale = as_data['track_timescale']
with open(as_data['datFile'], "wb") as ofh:
for (rep_nr, rep_data) in enumerate(as_data['reps']):
rep_id = rep_data['id']
rep_data['endNr'] = None
rep_data['startTick'] = None
rep_data['endTick'] = None
if self.firstSegmentInLoop >= 0:
assert rep_data['firstNumber'] == self.firstSegmentInLoop
else:
self.firstSegmentInLoop = rep_data['firstNumber']
if self.mpdSegStartNr >= 0:
assert adaptation_set.start_number == self.mpdSegStartNr
else:
self.mpdSegStartNr = adaptation_set.start_number
segTicks = self.segDuration*track_timescale
maxDiffInTicks = int(track_timescale*0.1) # Max 100ms
segNr = rep_data['firstNumber']
repeatCount = -1
firstSegmentInRepeat = -1
firstStartTimeInRepeat = -1
lastDuration = 0
while (True):
segmentPath = rep_data['absMediaPath'] % segNr
if not os.path.exists(segmentPath):
if self.verbose:
print("\nLast good %s segment is %d, endTime=%.3fs, totalTime=%.3fs" % (
rep_id, rep_data['endNr'], rep_data['endTime'],
rep_data['endTime']-rep_data['startTime']))
break
msf = mediasegmentfilter.MediaSegmentFilter(
segmentPath, default_sample_duration = rep_data[
'default_sample_duration'])
msf.filter()
tfdt = msf.get_tfdt_value()
duration = msf.get_duration()
print("{0} {1:8d} {2} {3}".format(content_type, segNr, tfdt, duration))
if duration == lastDuration:
repeatCount += 1
else:
if lastDuration != 0 and rep_nr == 0:
writeSegTiming(ofh, firstSegmentInRepeat,
firstStartTimeInRepeat,
lastDuration, repeatCount)
repeatCount = 0
lastDuration = duration
firstSegmentInRepeat = segNr
firstStartTimeInRepeat = tfdt
if rep_data['startTick'] is None:
rep_data['startTick'] = tfdt
rep_data['startTime'] = rep_data['startTick']/float(track_timescale)
print("First %s segment is %d starting at time %.3fs" % (rep_id, segNr,
rep_data['startTime']))
# Check that there is not too much drift. We want to end with at most maxDiffInTicks
endTick = tfdt + duration
idealTicks = (segNr - rep_data['firstNumber'] + 1)*segTicks + rep_data['startTick']
absDiffInTicks = abs(idealTicks - endTick)
if absDiffInTicks < maxDiffInTicks:
# This is a good wrap point
rep_data['endTick'] = tfdt + duration
rep_data['endTime'] = rep_data['endTick']/float(track_timescale)
rep_data['endNr'] = segNr
else:
raise DashAnalyzerError("Too much drift in the duration of the segments")
segNr += 1
if self.verbose:
sys.stdout.write(".")
if rep_nr == 0:
writeSegTiming(ofh, firstSegmentInRepeat, firstStartTimeInRepeat, duration, repeatCount)
lastGoodSegments.append(rep_data['endNr'])
as_data['totalTicks'] = rep_data['endTick'] - rep_data['startTick']
self.lastSegmentInLoop = min(lastGoodSegments)
self.nrSegmentsInLoop = self.lastSegmentInLoop-self.firstSegmentInLoop+1
self.loopTime = self.nrSegmentsInLoop*self.segDuration
if self.verbose:
print("")
print("Will loop segments %d-%d with loop time %ds" % (self.firstSegmentInLoop, self.lastSegmentInLoop,
self.loopTime))
def write_config(self, config_file):
"""Write a config file for the analyzed content, that can then be used to serve it efficiently."""
cfg_data = {'version' : '1.1', 'first_segment_in_loop' : self.firstSegmentInLoop,
'nr_segments_in_loop' : self.nrSegmentsInLoop, 'segment_duration_s' : self.segDuration}
media_data = {}
for content_type in ('video', 'audio'):
if content_type in self.as_data:
mdata = self.as_data[content_type]
media_data[content_type] = {'representations' : [rep['id'] for rep in mdata['reps']],
'timescale' : mdata['track_timescale'],
'totalDuration' : mdata['totalTicks'],
'datFile' : mdata['datFile']}
cfg_data['media_data'] = media_data
vod_cfg = configprocessor.VodConfig()
vod_cfg.write_config(config_file, cfg_data)
def processMpd(self):
"""Process the MPD and make an appropriate live version."""
mpdData = {"availabilityStartTime" :makeTimeStamp(self.mpdAvailabilityStartTIme),
"timeShiftBufferDepth" : makeDurationFromS(self.timeShiftBufferDepthInS),
"minimumUpdatePeriod" : "PT30M"}
if not self.muxType != MUX_TYPE_NONE:
self.mpdProcessor.makeLiveMpd(mpdData)
else:
self.mpdProcessor.makeLiveMultiplexedMpd(mpdData, self.media_data)
self.muxedRep = self.mpdProcessor.getMuxedRep()
targetMpdNamespace = None
if self.fixNamespace:
targetMpdNamespace = DEFAULT_DASH_NAMESPACE
self.mpd = self.mpdProcessor.getCleanString(True, targetMpdNamespace)
def main():
from optparse import OptionParser
verbose = 0
usage = "usage: %prog [options] mpdPath"
parser = OptionParser(usage)
parser.add_option("-v", "--verbose", dest="verbose", action="store_true")
(options, args) = parser.parse_args()
if options.verbose:
verbose = 1
if len(args) != 1:
parser.error("incorrect number of arguments")
mpdFile = args[0]
dashAnalyzer = DashAnalyzer(mpdFile, verbose)
dashAnalyzer.analyze()
if __name__ == "__main__":
main()
================================================
FILE: dashlivesim/vodanalyzer/mpdprocessor.py
================================================
"""DASH MPD processor and classes for MPD elements."""
# The copyright in this software is being made available under the BSD License,
# included below. This software may be subject to other third party and contributor
# rights, including patent rights, and no such rights are granted under this license.
#
# Copyright (c) 2015, Dash Industry Forum.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of Dash Industry Forum nor the names of its
# contributors may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from xml.etree import ElementTree
import io
import re
from dashlivesim.dashlib import timeformatconversions as tfc
RE_DURATION = re.compile(r"PT((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?")
RE_NAMESPACE_TAG = re.compile(r"({.*})?(.*)")
class MpdElementError(Exception):
"""General MpdElement Error."""
class MpdElement(object):
"""BaseClass for MPD elements."""
def __init__(self, node):
self.node = node
self.attribs = {}
# pylint: disable=no-self-use
def parse(self):
"""Parse the node and its children."""
raise MpdElementError("Not implemented")
# pylint: disable=no-self-use, unused-argument
def make_live(self, data):
"""Change attributes and values to make this MPD live. Use the data dictionary for this."""
raise MpdElementError("Not implemented")
#pylint: disable=no-self-use, unused-variable
def tag_and_namespace(self, full_tag):
"""Extract tag and namespace."""
match_obj = RE_NAMESPACE_TAG.match(full_tag)
tag = match_obj.group(2)
namespace = match_obj.group(1)
return (tag, namespace)
def compare_tag(self, full_tag, string):
"""Compare tag to see if it is equal."""
tag, namespace = self.tag_and_namespace(full_tag)
return tag == string
def check_and_add_attributes(self, node, attribs):
"""Check if node has attributes and add them to self.attribs."""
for attr in attribs:
if attr in node.attrib:
self.attribs[attr] = node.attrib[attr]
else:
if not attr in self.attribs:
self.attribs[attr] = None
def set_value(self, element, key, data):
"""Set attribute key of element to value data[key], if present."""
if key in data:
element.set(key, str(data[key]))
class Mpd(MpdElement):
"""Top level MPD element."""
def __init__(self, node):
MpdElement.__init__(self, node)
self.periods = []
def parse(self):
"""Parse the node and its children."""
self.check_and_add_attributes(self.node, ('profiles', 'maxSegmentDuration', 'minBufferTime',
'type', 'mediaPresentationDuration'))
for child in self.node:
if self.compare_tag(child.tag, 'Period'):
period = Period(child)
period.parse()
self.periods.append(period)
def make_live(self, data):
"""Change attributes and values to make this MPD live. Use the data dictionary for this."""
self.set_value(self.node, 'type', 'dynamic')
for attr in ('availabilityStartTime', 'availabilityEndTime'):
self.set_value(self.node, attr, data[attr])
for period in self.periods:
period.make_live(data)
class Period(MpdElement):
"Period element in MPD."
def __init__(self, node):
MpdElement.__init__(self, node)
self.adaptation_sets = []
def parse(self):
"Parse the node and its children."
self.check_and_add_attributes(self.node, ('id', 'start'))
for child in self.node:
if self.compare_tag(child.tag, 'AdaptationSet'):
adaptation_set = AdaptationSet(child)
adaptation_set.parse()
self.adaptation_sets.append(adaptation_set)
def make_live(self, data):
for attr in ('start'):
self.set_value(self.node, attr, data[attr])
for adaptation_set in self.adaptation_sets:
adaptation_set.make_live(data)
class AdaptationSet(MpdElement):
"AdaptationSet element in a Period."
def __init__(self, node):
MpdElement.__init__(self, node)
self.segment_template = None
self.representations = []
@property
def content_type(self):
"Get the contentType for the AdaptationSet."
return self.attribs['contentType']
@property
def media_pattern(self):
"Get the media pattern from SegmentTemplate."
return self.attribs['media']
@property
def initialization_pattern(self):
"Get the initialization pattern from SegmentTemplate."
return self.attribs['initialization']
@property
def start_number(self):
"StartNumber for segments (from SegmentTemplate)."
return int(self.attribs['startNumber'])
@property
def timescale(self):
"Timescale in units per seconds to be used for the derivation of different real-time duration values in the Segment Information"
return int(self.attribs['timescale'] or 1)
@property
def duration(self):
"Segment duration (in whole seconds)."
return int(self.attribs['duration']) // self.timescale
def parse(self):
"Parse the node and its children."
self.check_and_add_attributes(self.node, ('contentType', 'mimeType'))
for child in self.node:
if self.compare_tag(child.tag, 'SegmentTemplate'):
self.check_and_add_attributes(child, ('initialization', 'startNumber', 'media',
'duration', 'timescale'))
elif self.compare_tag(child.tag, 'Representation'):
rep = Representation(self, child)
rep.parse()
self.representations.append(rep)
def make_live(self, data):
for attr in ('startNr'):
self.set_value(self.node, attr, data[attr])
for adaptation_set in self.adaptation_sets:
adaptation_set.make_live(data)
class Representation(MpdElement):
"Representation element in an AdaptationSet."
def __init__(self, adaptation_set, node):
MpdElement.__init__(self, node)
self.adaptation_set = adaptation_set
@property
def initialization_path(self):
"The initialization path of this representation."
return self.get_initialization_path()
@property
def rep_id(self):
"Id of this representation."
return self.attribs['id']
def parse(self):
"Parse the node and its children."
self.check_and_add_attributes(self.node, ('id', 'bandwidth'))
def get_initialization_path(self):
"The initialization path of this representation."
init_pattern = self.adaptation_set.initialization_pattern
rep_id = self.attribs['id']
bandwidth = self.attribs['bandwidth']
init_path = init_pattern.replace("$RepresentationID$", rep_id).replace("$bandwidth$", bandwidth)
return init_path
def get_media_path(self, segNr="%d"):
"Return the media path for this representation and given segNr."
media_pattern = self.adaptation_set.media_pattern
rep_id = self.attribs['id']
bandwidth = self.attribs['bandwidth']
media_path = media_pattern.replace("$RepresentationID$", rep_id).replace("$bandwidth$", bandwidth)
media_path = media_path.replace("$Number$", str(segNr))
return media_path
class MpdProcessor(MpdElement):
"""Modify the mpd to become live. Whatever is input in data is set to these values."""
def __init__(self, infile):
self.tree = ElementTree.parse(infile)
self.mpd_namespace = None
self.root = self.tree.getroot()
self.is_base_url_set = False
self.adaptation_sets = []
self.media_presentation_duration = None
self.media_presentation_duration_in_s = None
self.muxed_rep = None
self.parse()
def parse(self):
"Parse and find all the adaptation sets and their representations."
mpd = self.root
tag, self.mpd_namespace = self.tag_and_namespace(mpd.tag)
assert tag == "MPD"
if 'mediaPresentationDuration' in mpd.attrib:
self.media_presentation_duration = mpd.attrib['mediaPresentationDuration']
self.media_presentation_duration_in_s = tfc.iso_duration_to_seconds(self.media_presentation_duration)
print("Found mediaPresentationDuration = %ds" % self.media_presentation_duration_in_s)
for child in mpd:
if self.compare_tag(child.tag, 'Period'):
for grand_child in child:
if self.compare_tag(grand_child.tag, 'AdaptationSet'):
AS = AdaptationSet(grand_child)
AS.parse()
self.adaptation_sets.append(AS)
def get_adaptation_sets(self):
return self.adaptation_sets
def getMuxedRep(self):
return self.muxed_rep
def getMuxedInitPath(self):
initPath = None
for AS in self.adaptation_sets:
if AS.contentType == "video":
print(AS.initialization)
initPath = AS.initialization.replace("$RepresentationID$", self.muxed_rep)
return initPath
def getMuxedMediaPath(self):
mediaPath = None
for AS in self.adaptation_sets:
if AS.contentType == "video":
mediaPath = AS.media.replace("$RepresentationID$", self.muxed_rep).replace("$Number$", "%d")
return mediaPath
def process(self, mpdData={}):
MPD = self.root
self.processMPD(MPD, mpdData)
def makeLiveMpd(self, data):
"""Process the root element (MPD) and set values from data dictionary.
Typical keys are: availabilityStartTime, timeShiftBufferDepth, minimumUpdatePeriod."""
MPD = self.root
MPD.set('type', "dynamic")
for key in data.keys():
self.setValue(MPD, key, data)
if 'mediaPresentationDuration' in MPD.attrib:
del MPD.attrib['mediaPresentationDuration']
for child in MPD:
if self.compare_tag(child.tag, 'Period'):
child.set("start", "PT0S") # Set Period start to 0
def makeLiveMultiplexedMpd(self, data, mediaData):
self.makeLiveMpd(data)
MPD = self.root
audioAS = None
videoAS = None
period = None
audioRep = None
vidoeRep = None
for child in MPD:
if self.compare_tag(child.tag, 'Period'):
period = child
for grandChild in child:
if self.compare_tag(grandChild.tag, 'AdaptationSet'):
AS = AdaptationSet(grandChild)
AS.parse()
if AS.contentType == "audio":
audioAS = grandChild
elif AS.contentType == "video":
videoAS = grandChild
for contentType, mData in mediaData.items():
trackID = mData['trackID']
cc = self.makeContentComponent(contentType, trackID)
videoAS.insert(0, cc)
del videoAS.attrib['contentType']
audioRep = audioAS.find(self.mpd_namespace+"Representation")
videoRep = videoAS.find(self.mpd_namespace+"Representation")
videoRep.set("id", self.muxed_rep)
try:
audioCodec = audioRep.attrib["codecs"]
videoCodec = videoRep.attrib["codecs"]
combinedCodecs = "%s,%s" % (audioCodec, videoCodec)
videoRep.set("codecs", combinedCodecs)
except KeyError:
print("Could not combine codecs")
period.remove(audioAS)
def makeContentComponent(self, contentType, trackID):
"Create and insert a contentComponent element."
elem = ElementTree.Element('%sContentComponent' % self.mpd_namespace)
elem.set("id", str(trackID))
elem.set("contentType", contentType)
elem.tail = "\n"
return elem
def getCleanString(self, clean=True, targetMpdNameSpace=None):
"Get a string of all XML cleaned (no ns0 namespace)"
ofh = io.StringIO()
self.tree.write(ofh)#, default_namespace=NAMESPACE)
value = ofh.getvalue()
if clean:
value = value.replace("ns0:", "").replace("xmlns:ns0=", "xmlns=")
if targetMpdNameSpace is not None:
newStr = 'xmlns="%s"' % targetMpdNameSpace
value = re.sub('xmlns="[^"]+"', newStr, value)
xmlIntro = '\n'
return xmlIntro + value
================================================
FILE: dashlivesim/vodanalyzer/parse_dat_file.py
================================================
import sys
from struct import unpack
import argparse
from dashlivesim.dashlib.configprocessor import SEGTIMEFORMAT, SegTimeEntry
def parse_dat_file(infile_handle, verbosity_level):
data = infile_handle.read(12)
lste = None
while data:
ste = SegTimeEntry(*unpack(SEGTIMEFORMAT, data))
if lste:
last_end = lste.start_time + lste.duration * (lste.repeats + 1)
if last_end != ste.start_time:
print("Mismatch in end vs start time %d %d" % (ste.start_time, last_end))
if verbosity_level > 0:
print(ste)
lste = ste
data = infile_handle.read(12)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Check segment datfile')
parser.add_argument('infile', nargs=1, type=argparse.FileType('rb'))
parser.add_argument('--verbose', '-v', action='count', default=0)
args = parser.parse_args()
parse_dat_file(args.infile[0], args.verbose)
================================================
FILE: doc/content.mdown
================================================
DASH-IF live source simulator test content.
=========================================
"Live" content from test server
---------------------
**Test live content with clock synchronized to wall clock (modulo hour)**
* [livesim/testpic_2s/Manifest.mpd](pdash/testpic_2s/Manifest.mpd) A live stream synchronized with wall clock with 2s segments. Max video bitrate: 348kbps, audio: 60kbps
VoD
---
VoD content used for the live content above (generated by MobiTV VoD segmenter).
* [dash/vod/testpic_2s/Manifest.mpd](dash/vod/testpic_2s/Manifest.mpd) A VoD test stream for AV sync (one representation with 2s segments). Max video bitrate: 348kbps, audio: 60kbps
Subtitles
---
VoD and live content:
* [dash/vod/testpic_2s/Manifest_stpp.mpd](dash/vod/testpic_2s/Manifest_stpp.mpd) One subtitle track.
* [dash/vod/testpic_2s/multi_subs.mpd](dash/vod/testpic_2s/multi_subs.mpd) Three subtitle tracks.
* [livesim/testpic_2s/Manifest_stpp.mpd](livsim/testpic_2s/Manifest_stpp.mpd) One subtitle track.
* [livesim/testpic_2s/multi_subs.mpd](livesim/testpic_2s/multi_subs.mpd) Three subtitle tracks.
================================================
FILE: doc/dashlivesim.mdown
================================================
DASH-IF configurable live content simulator
==================================================
Purpose
-------
The purpose of the DASH live simulator is to provide well-controlled live time-synced content for testing.
The main link to the content is [http://vm2.dashif.org/livesim/testpic_2s/Manifest.mpd][testpic_2s_base].
You can test it directly with Chrome (version>=38), Safari 8+, IE 11 (on Windows 8.1), Edge, and FireFox >=42 using
the [DASH-IF reference player][dashif_player].
It provides an infinite source that is synced with the wall clock at the source side (modulo 1 hour).
Time-synced content
--------------
The content testpic\_2s shows pictures with a clock and frame number.

The content wraps every hour, and is synchronized with the server clock, so that the content for a
specific time is made available when the end the corresponding segment has passed.
For example, the segment containing the data for 2min to 2min and ss passed a full hour,
is available at 2min and ss after the hour.
By watching the screen and noting the time and comparing it to a synchronized clock,
it is possible to measure the delay in the transport and rendering system. It is typically
longer for longer segments.
Options
---------
The test server offers the following features where some are turned on by modifying the URL path to the MPD.
* Live content simulation by providing segments from VoD content in a looped fashion
* Synchronization with wall-clock, so that a particular media segment is made available at a specific time.
This can be used for e2e delay estimation, see below.
* Full control over the bitrate and duration of the segments, since these are preproduced.
There are thus no surprises due to network problems in the live content stream towards the segmenter.
* Compliance with DASH264 Interoperability Guidelines
* Support for many different scenarios by specifying modifiers in the path
* Support for infinite sessions (no change in the MPD, and no duration set)
* Support for time-limited services and updated MPD which is configured in the URL
* Support for configuration of minimumUpdatePeriod, timeShiftBufferDepth, startNumber (including implicit)
* Support for periodic services which repeat every 10min or similar.
* Support for multiple periods and one period that started later that the session.
* Feedback on too early or too late segment fetching attempts by explicit text in HTTP 404 return messages
* Support for availability time offset in BaseURL element
* Disabling of all timing checks of segments by specifying the `ato_inf` modifier.
* On the fly multiplexing of audio and video (for eMBMS testing, rather than DASH-IF)
* Insertion of SCTE-35 ad signaling as emsg messages following the DASH-IF guidelines.
* Support for client-server time-sync using UTCTiming (head and direct methods). This reduces dash.js startup time.
* Support for choosing the startNumber in the MPD. All timing will be appropriately shifted so the content will be in sync, independent of the startNumber value.
* Support for live subtitling in TTML and more specifically in EBU-TT-D format.
* Support for signalling continuous adaptationSets over period boundaries.
* Support for SegmentTimeline manifests
* Support for xlink periods for ad insertion
* Support for ntp and sntp UTC timing
* Support for early-terminated periods
* Support for availabilityTimeOffset
Links and usage
---------------
The DASH-IF server is in the Amazon cloud at vm2.dashif.org.
There is currently only one test sequence, which is 1 hour long and provides a clock.
It is available with 2s segments in DASH live profile.
The content is available as `http://[serveraddress]/livesim/[contentName]/[Manifest].mpd`
and, in particular, the 2s source is [http://vm2.dashif.org/livesim/testpic_2s/Manifest.mpd][testpic_2s_base].
There are multiple modifiers that can be used. They need to be placed before `/testpic_2s` and are all of the form `/option_n`.
Events and other time-limited content
-------------------------------------
To control availabilityStartTime (AST) and availabilityEndTime (AET), one can add extra parts to the URL after pdash.
http:////start_ut/... will set the AST to the UNIX time ut (clipped to a multiple of duration)
http:////dur_dt/... will set the AET as well (ut+dt)
http:////dur_dt1/dur_dt2/ will set the AET and then updated it 2*minimumUpdatePeriod before the first has duration has been reached
The minimumUpdatePeriod is set to 1min by default in this mode.
One can also make the initialization segments available earlier than AST, by specifying init_ot where ot is an offset time in seconds.
An example
http:////start_1370809900/dur_1800/dur_300/init_10800/testpic_2s/Manifest.mpd
will set the availabilityStartTime to `2013-06-09T20:31:40 (UTC)`, and set the availabilityEndTime to
`013-06-09T21:01:40`, and the update it to `2013-06-09T21:06:40`. The initialization segments are set to be available 3 hours in advance.
The last media segments in a timelimited session, (with duration) will have the `lmsg` compatibility brand set,
to signal that they are last and that there are no more segments to fetch.
Note that one can influence the
`minimumUpdatePeriod`by the parameter `mup_x` in the path and `timeShiftBufferDepth` by the parameter `tsbd_x`. Here `x` is the value in seconds.
Too facilitate the creation of URLs for timelimited content, there is an online link generator [urlgen.html](urlgen.html).
There is also a command-line tool written in Python [generate_dashif_url.py](generate_dashif_url).
Dynamic MPD with static URLs
-----------------------------
To allow for testing of dynamic MPD updates without the need to construct a time-specific MPD, the following scheme is supported:
http:////modulo_x//
Here, `modulo_x` denotes that the content is available module `x` minutes,
the `availabilityStartTime` and the `mediaPresentationDuration` vary in an `x`-minute periodic pattern.
The number `x` must be a divisor of 60, i.e. on of 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60.
For example, if `x=10`, the following happens (mpd = mediaPresentationDuration, mup=minimumUpdatePeriod):
hh:00:00-hh:00:59 ast = hh:00:00 mpd = 120s mup = 30s
hh:01:00-hh:02:59 ast = hh:00:00 mpd = 240s
hh:03:00-hh:04:59 ast = hh:00:00 mpd = 360s
hh:05:00-hh:08:59 ast = hh:00:00 mpd = 480s
hh:09:00-hh:10:59 ast = hh:10:00 mpd = 120s
In other words:
mup = 5% of the interval
0-10% of the interval, the mpd=20% of interval
10-30% of the interval, the mpd=40% of the interval
30-50% of the interval, the mpd=60% of the interval
50-90% of the interval, the mpd=80% of the interval
90-100% of the interval, the mpd=20% of the next interval
Thus, beyond the media session getting longer and longer during the first 50% of the session,
from 80-90% of the interval, the MPD will describe an expired session, and
from 90-100% of the interval, the MPD will describe a future session.
Multiple periods
----------------
Multiple periods are generated by specifying `periods_n` which results in the following:
* n = 0 One period but it starts 1000h after AST, so Period@start and presentatimeOffset are non-zero
* n > 0 n periods per hour (n = 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) and
`minimumUpdatePeriod` adjusted to be 5s less than half the period duration. The number of previous periods presented
depends on the `timeShiftBufferDepth`
The `presentationTimeOffset` is signalled in the `SegmentTemplate`. By specifying the option `/peroff_1`,
the `PTO` is instead signaled in a `SegmentBase` element in top level of the `Period`.
Ad insertion
------------
For testing of App-based ad-insertion, the server can add SCTE35 Splice Insert signals for one-three 10s ad insertions per minute.
Turn this on by specifying scte35_ where is 1,2, or 3 in the path before the content.
The ad timing is the following
* 1 ad per minute, start 10s past full minute
* 2 ads per minute, start at 10s and 40s past full minute
* 3 ads per minute, start at 10s, 30s, and 50s past full minute
The presence of such an event-stream is indicated in the manifest.
UTCTiming
---------
By specifying utc_head, utc_direct or a combination like utc_direct-head extra information will be added in the MPD
to provide the timing information. This is used by the dash.js to get a shorter startup time.
Multiplexed Content
-------------------
For eMBMS, better robustness can be achieved by multiplexing audio and video segments.
This can be done automatically by the server. It happens if the path to a segment has two underscores in the
next to last path component, like `.../V1__A1/123.m4s`. In this case the two segments `V1/123.m4s` and `V2/123.m4s` are fetched and multiplexed.
The corresponding thing happens for the init segments. For this to work, the MPD must be manually changed to have
a multiplexed representation.
How it works
------------
The module is stateless, and uses the URL to find out what content to serve.
The content must have an MPD with extension .mpd, initialization segments with extension .mp4 and
media segments with extension .m4s. Depending on the extension, the server will deliver read the
corresponding data from the VoD content and modify it. The modifications to the various
data server are:
*MPD file:*
@MPD level
set type dynamic
set publishTime /* Configurable in code */
set timeShiftBufferDepth
set availabilityStartTime
set minimumUpdatePeriod
set maxSegmentDuration
set mediaPresentationDuration if time-limited
set availabilityEndTime if time-limited
set startNumber (depending on snr_ parameter. Default is 0).
@Period level
set start
set periodname
@BaseURL
adding BaseURL if not present, or modifying if present
@SegmentTemplate level
set startNumber
set presentationTimeOffset if needed
*Initialization segments:*
No change (actually setting duration to 0, but it should already be 0)
*Media segments:*
Mapped from live number to VoD number and multiplexed if needed
sequenceNumber updated to be continuous (and identical to segment number)
earliest presentation time in tfdt is changed to be continuously grown with start at epoch.
Our basic service is a 24/7 service with no updates to the MPD. To facilitate calculations
we have chosen the epoch time to be the the anchor for calculations. Our default is:
startNumber = 0
availabilityStartTime = epochStart (Jan. 1 1970)
minimumUpdatePeriod = 100Y
period start = PT0s
presentationTimeOffset = 0
The availabilityStartTime tells when the initialization segments are available.
The first segments should be available one segmentDuration later.
The latest available segment number can be then be calculated as
latestSegmentNumber = (now - availabilityStartTime - duration)/duration + startNumber
The server makes sure that segments are only available from their startTime and for a period
corresponding to the timeShiftBufferDepth (the default is 5 min).
The media timeline shall be synchronized with the MPD. Thus, at the start of a period,
the offset of the presentationTime in the media segments should be equal to the period start time or rather
MediaTimeAtPeriodStart = AST + Period@Start - presentationTimeOffset
We fulfill this by having Period@start = 0 and presentationTimeOffset = 0, and relating the media timeline to AST.
However, when periods do not start at AST, we must adjust the presentationTimeOffset to be equal to the Period@start.
Some examples of this are the modifiers:
/periods_0 - one period that starts 1000hours after AST
/periods_n - multiple periods (n per hour)
/periods_0/peroff_1 - one period but presentationTimeOffset signaled in SegmentBase at Period level
/periods_n/peroff_1 - similar to /periods_n but SegmentBase used for presentationTimeOffset
In addition, one can turn on signalling of continuous periods, but adding the flag /continuous_1/`.
## Changelog
2.0.2 - May 17 2022: Fix EventStream/Event (Issue #105 & #109). Update vodanalyzer to Python 3
2.0.1 - May 24 2020: Fixed https detection (Issue #97)
2.0.0 - May 18 2020: Changed to Python3. Integrated low-latencu chunked mode triggered by chunkdur and ato parameters.
1.7.0 - Dec. 12 2019: Added two more UTCTiming modes, new mode
segtimelineloss, query-strings are neglected, possible session time limit and
id via redirect, fixed bug in segmenttimeline wraparound
1.6.0 - Dec. 18, 2018: Support for SegmentTimeline with Number, added suggestedPresentationDelay, propagates default-sample-duration from trex and tfhd, and can now generate sidx boxes.
1.5.1 - June 3, 2018: Fix: Removed timeShiftBufferDepth in start-over case.
1.5 - May 17, 2018: Added support for thumbnails and start-over use case.
* 1.4 - Sep. 29 2016: Added support for SegmentTimeline manifests, xlink periods for ad insertion, ntp and sntp UTC timing, early-terminated periods, availabilityTimeOffset, and MPD callback.
* 1.3 - Oct. 13, 2015: Added support for continuous periods (continuous_1) and https protocol fo`r BaseURL & UTCTiming. Moved some configuration to mod_wsgi. Also made the stand-alone wsgi server much better.
* 1.2 - Aug. 18, 2015: Added support for live subtitles in TTML format. Beyond segment renumbering, the content will also be changed. A tool for generating a sequence of such segments is also provided. Support for choosing the starNr. Added support for running using mod_wsgi and not only mod_python.
* 1.1 - Jun. 2, 2015: Added snr_ option to control startNumber in manifest. Fixed some bugs with availabilityStartTime and PTO._
* 1.0 - May 7, 2015: First public release. Available as http://vm2.dashif.org/livesim/
* - Changed init segment durations to 0.
* 0.9.9 - Apr. 29, 2015: First commit to DASH-IF/live-source-simulator Github project.
The following is the change-log for the code as internal MobiTV code.
* 0.9.5 - Apr. 13, 2015: Support for specifying UTCTiming methods (head or direct)
* 0.9.0 - Mar. 16, 2015: Added option of scte_35 signals 1, 2, or 3 times a minute, by specifying scte35_2 as option.
* - Changed duration in init segment to maxint (only 1's instead of 0) since the duration is unknown for live.
* 0.8.6 - Oct. 14, 2014: Added possibility of multiple periods and period not starting at AST.
* 0.8.5 - Mar. 4, 2014: Add presentationTimeOffset when AST is not start of epoch.
* 0.8.4 - Feb. 18, 2014: Corrected tfdt timing. Can now specify all\_1 as option to avoid timing checks.
* 0.8.3 - Feb. 13, 2014: The 404 responses do now include the a message of why content is not available including timing issues.
* 0.8.2 - Feb. 11, 2014: Fixed bug in BaseURL. Added error_logging for too early and too late segments.
* 0.8.1 - Jan. 28, 2014: Added modulo period
* 0.8.0 - Jan. 27, 2014: Supports multiplexing and aligned with DASH-IF contributed server with some extra functionality
* 0.6.8 - Oct. 30, 2013: One can now set another value for the availabilityStartTime for the bdash server.
* 0.6.7 - Sep. 30, 2013: Bugfixes for old namespace, range and non-existing segments.
* 0.6.6 - Sep. 27, 2013: Configurable to remove publishTime and substitute old namespace
* 0.6.5 - Sep. 20, 2013: Fixed byterange to handle open intervals and return 206.
* 0.6.4 - Sep. 16, 2013: Now uses tfdt for sync and removes sidx.
* 0.6.3 - Aug. 30, 2013: Added support for byte-range requests
* 0.6.2 - Aug. 23, 2013: Added support for additional directory structure for languages
* 0.6.1 - July 23, 2013: Added Z for GMT timezone in all dates in the MPD.
* 0.6 - June 5, 2013: Added support for events and other time-limited sessions.
[testpic_2s_base]: http://vm2.dashif.org/livesim/testpic_2s/Manifest.mpd "Live simulator infinite clock source."
[dashif_player]: http://dashif.org/reference/players/javascript/1.3.0/samples/dash-if-reference-player/index.html?url=http://vm2.dashif.org/livesim/testpic_2s/Manifest.mpd "DASH-IF Reference Player"
================================================
FILE: run_tests.sh
================================================
python3 -m unittest discover
================================================
FILE: setup/dash.conf
================================================
Header set Access-Control-Allow-Headers "origin,range"
Header set Access-Control-Expose-Headers "Server,range, Date"
Header set Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Header set Access-Control-Allow-Origin "*"
================================================
FILE: setup/installation.mdown
================================================
DASH-IF live source simulator installation
-------------------------------
The DASH live source simulator us using the wsgi HTTP API.
It is recommended to use Apache with mod_wsgi, and that is how the https://livesim.dashif.org server is run.
However, wsgi is cross-platform and should work with nginx as well.
For local testing, it is also possible to run the wsgi server itself by calling the script `tools/run_wsgi_server.sh`.
### Requirements:
Python >= 3.6, Apache2 with mod_wsgi(can also be run without Apache, or with nginx).
### Configuration for server static files (VoD mode)
The VoD content (in live profile) that serves as raw content for live should typically be as maps under
/var/www/html/dash/vod/
The directory is specified as `CONTENT_ROOT` to the simulator.
Example content that can be used for both VoD and live streaming is available as
https://livesim.dashif.org/dash/vod/testpic_2s.tar
https://livesim.dashif.org/dash/vod/testpic4_8s.tar
The main issue for browser players like dash.js is typically CORS.
For serving the VoD files using Apache2 from `/var/www/html/dash/vod` it is recommended to add the file
dash.conf to the Apache2 configuration.
The location depends on distribution, but in CentOS it is `/etc/httpd/conf.d/dash.conf`.
### Setup of simulated live streaming from Apache2 with mod_wsgi
This is the recommended way to run on a public server. For testing, it may easier to run a local server, see below.
How mod_wsgi is installed depends on the Linux distribution.
The mod_wsgi configuration goes into the configuration directory (for CentOS)
/etc/httpd/conf.d/mod_wsgi/dashlivesim.conf
or something similar on other Linux distributions.
As seen in the example file provided in this directory, `VOD_CONF_DIR` and `CONTENT_ROOT` must be set up,
and the source code tree `dashlivesim` must be found by the `WSGIPythonPath` and `WSGIScriptAlias`must point to
`mod_wsgi/mod_dashlivesim.py` must be specified.
To install the actual source code, get it from github and copy the `dashlivesim` directory recursively into
`/usr/local/bin/mod_wsgi/`.
Finally, the configuration files must be installed in `/var/www/html/livesim_vod_configs` or whatever `VOD_CONF_DIR`
points at.
### Setup for a locl wsgi server
To run a local wsgi http server use the script `tools/run_wsgi_server`. The `vod_config` and `content_root` directories need to specified on the command line.
### Configuration of live material
For each simulated live source directory containing manifest files , there must be a configuration
file with the corresponding name .cfg
For example, for testpic_2s there is a file
/testpic_2s.cfg
For files without subtitles, it can be automatically generated by running the tool
tools/run_vodanalyzer.sh
This runs the Python script `dashlivesim.vodanalyzer.dashanalyzer` and produces a file `.cfg`.
You can then edit the file, to include fewer segments, fewer representations, or more representations if there
are other manifests that contain other representations. In particular, all subtitle representations must be added by hand.
The corresponding content resides in
//
In such a directory there should one or more VoD MPDs and associated media files.
Note that the file extensions are critical, but the base names not.
[contentName]
-> [Manifest].mpd
-> [rep1]
-> [init].mp4
-> [seqNr].m4s
-> [rep2]
...
Sample content and configuration can be found at `https://livesim.dashif.org/dash/`.
The configurations can be copied from `https://livesim.dashif.org/dash/vod_configs/`.
### UTCTiming Head mode
For UTCTiming head mode to work, there must be file accessible via http:///dash/time.txt``.
The content is not relevant.
The CORS support for the header Date must be supported.
### Sample content and configurations
Sample content and configuration can be found at `https://livesim.dashif.org/dash/`.
Instead of downloading individual segments, it is recommended to download the `.tar` files when available.
The configurations can be copied from `https://livesim.dashif.org/dash/vod_configs/`.
================================================
FILE: setup/mod_wsgi_dashlivesim.conf
================================================
WSGIScriptAlias /livesim /usr/local/bin/mod_wsgi/dashlivesim/mod_wsgi/mod_dashlivesim.py
WSGIPythonPath /usr/local/bin/mod_wsgi
setEnv VOD_CONF_DIR /var/www/html/dash/vod_configs
setEnv CONTENT_ROOT /var/www/html/dash/vod
================================================
FILE: tools/run_cc_insert.py
================================================
export PYTHONPATH=${PYTHONPATH}:..
python3 -m dashlivesim.cc_inserter.cc_inserter $@
================================================
FILE: tools/run_stpp_generator.sh
================================================
export PYTHONPATH=${PYTHONPATH}:..
python -m dashlivesim.dashlib.stpp_generator.make_stpp_segments $@
================================================
FILE: tools/run_vodanalyzer.sh
================================================
export PYTHONPATH=${PYTHONPATH}:..
python3 -m dashlivesim.vodanalyzer.dashanalyzer $1
================================================
FILE: tools/run_wsgi_server.sh
================================================
# Run a local mod_wsgi server
export PYTHONPATH=${PYTHONPATH}:..
python3 -m dashlivesim.mod_wsgi.mod_dashlivesim $*