%PDF- %PDF-
| Direktori : /home/waritko/build/Bento4/Source/Python/utils/ |
| Current File : //home/waritko/build/Bento4/Source/Python/utils/mp4-dash-clone.py |
#!/usr/bin/env python
__author__ = 'Gilles Boccon-Gibod (bok@bok.net)'
__copyright__ = 'Copyright 2011-2012 Axiomatic Systems, LLC.'
###
# NOTE: this script needs Bento4 command line binaries to run
# You must place the 'mp4info' and 'mp4encrypt' binaries
# in a directory named 'bin/<platform>' at the same level as where
# this script is.
# <platform> depends on the platform you're running on:
# Mac OSX --> platform = macosx
# Linux x86 --> platform = linux-x86
# Windows --> platform = win32
### Imports
import sys
import os
import os.path
from optparse import OptionParser, make_option, OptionError
import urllib2
import urlparse
import shutil
import itertools
import json
import sys
from xml.etree import ElementTree
from subprocess import check_output, CalledProcessError
# constants
DASH_NS_URN_COMPAT = 'urn:mpeg:DASH:schema:MPD:2011'
DASH_NS_URN = 'urn:mpeg:dash:schema:mpd:2011'
DASH_NS_COMPAT = '{'+DASH_NS_URN_COMPAT+'}'
DASH_NS = '{'+DASH_NS_URN+'}'
MARLIN_MAS_NS_URN = 'urn:marlin:mas:1-0:services:schemas:mpd'
MARLIN_MAS_NS = '{'+MARLIN_MAS_NS_URN+'}'
def Bento4Command(name, *args, **kwargs):
cmd = [os.path.join(Options.exec_dir, name)]
for kwarg in kwargs:
arg = kwarg.replace('_', '-')
cmd.append('--'+arg)
if not isinstance(kwargs[kwarg], bool):
cmd.append(kwargs[kwarg])
cmd += args
#print cmd
try:
return check_output(cmd)
except CalledProcessError, e:
#print e
raise Exception("binary tool failed with error %d" % e.returncode)
def Mp4Info(filename, **args):
return Bento4Command('mp4info', filename, **args)
def GetTrackIds(mp4):
track_ids = []
json_info = Mp4Info(mp4, format='json')
info = json.loads(json_info, strict=False)
for track in info['tracks']:
track_ids.append(track['id'])
return track_ids
def ProcessUrlTemplate(template, representation_id, bandwidth, time, number):
if representation_id is not None: result = template.replace('$RepresentationID$', representation_id)
if number is not None:
nstart = result.find('$Number')
if nstart >= 0:
nend = result.find('$', nstart+1)
if nend >= 0:
var = result[nstart+1 : nend]
if 'Number%' in var:
value = var[6:] % (int(number))
else:
value = number
result = result.replace('$'+var+'$', value)
if bandwidth is not None: result = result.replace('$Bandwidth$', bandwidth)
if time is not None: result = result.replace('$Time$', time)
result = result.replace('$$', '$')
return result
class DashSegmentBaseInfo:
def __init__(self, xml):
self.initialization = None
self.type = None
for type in ['SegmentBase', 'SegmentTemplate', 'SegmentList']:
e = xml.find(DASH_NS+type)
if e is not None:
self.type = type
# parse common elements
# type specifics
if type == 'SegmentBase' or type == 'SegmentList':
init = e.find(DASH_NS+'Initialization')
if init is not None:
self.initialization = init.get('sourceURL')
if type == 'SegmentTemplate':
self.initialization = e.get('initialization')
self.media = e.get('media')
self.timescale = e.get('timescale')
self.startNumber = e.get('startNumber')
# segment timeline
st = e.find(DASH_NS+'SegmentTimeline')
if st is not None:
self.segment_timeline = []
entries = st.findall(DASH_NS+'S')
for entry in entries:
item = {}
s_t = entry.get('t')
if s_t is not None:
item['t'] = int(s_t)
s_d = entry.get('d')
if s_d is not None:
item['d'] = int(s_d)
s_r = entry.get('r')
if s_r is not None:
item['r'] = int(s_r)
else:
item['r'] = 0
self.segment_timeline.append(item)
break
class DashRepresentation:
def __init__(self, xml, parent):
self.xml = xml
self.parent = parent
self.init_segment_url = None
self.segment_urls = []
self.segment_base = DashSegmentBaseInfo(xml)
self.duration = 0
# parse standard attributes
self.bandwidth = xml.get('bandwidth')
self.id = xml.get('id')
# compute the segment base type
node = self
self.segment_base_type = None
while node is not None:
if node.segment_base.type in ['SegmentTemplate', 'SegmentList']:
self.segment_base_type = node.segment_base.type
break
node = node.parent
# compute the init segment URL
self.ComputeInitSegmentUrl()
def SegmentBaseLookup(self, field):
node = self
while node is not None:
if field in node.segment_base.__dict__:
return node.segment_base.__dict__[field]
node = node.parent
return None
def AttributeLookup(self, field):
node = self
while node is not None:
if field in node.__dict__:
return node.__dict__[field]
node = node.parent
return None
def ComputeInitSegmentUrl(self):
node = self
while node is not None:
if node.segment_base.initialization is not None:
self.initialization = node.segment_base.initialization
break
node = node.parent
self.init_segment_url = ProcessUrlTemplate(self.initialization, representation_id=self.id, bandwidth=self.bandwidth, time=None, number=None)
def GenerateSegmentUrls(self):
if self.segment_base_type == 'SegmentTemplate':
return self.GenerateSegmentUrlsFromTemplate()
else:
return self.GenerateSegmentUrlsFromList()
def GenerateSegmentUrlsFromTemplate(self):
media = self.SegmentBaseLookup('media')
if media is None:
print 'WARNING: no media attribute found for representation'
return
timeline = self.SegmentBaseLookup('segment_timeline')
if timeline is None:
start = self.SegmentBaseLookup('startNumber')
if start is None:
current_number = 1
else:
current_number = int(start)
while True:
url = ProcessUrlTemplate(media, representation_id=self.id, bandwidth=self.bandwidth, time="0", number=str(current_number))
current_number += 1
yield url
else:
current_number = 1
current_time = 0
for s in timeline:
if 't' in s:
current_time = s['t']
for r in xrange(1+s['r']):
url = ProcessUrlTemplate(media, representation_id=self.id, bandwidth=self.bandwidth, time=str(current_time), number=str(current_number))
current_number += 1
current_time += s['d']
yield url
def GenerateSegmentUrlsFromList(self):
segs = self.xml.find(DASH_NS+'SegmentList').findall(DASH_NS+'SegmentURL')
for seg in segs:
media = seg.get('media')
if media is not None:
yield media
def __str__(self):
result = "Representation: "
return result
class DashAdaptationSet:
def __init__(self, xml, parent):
self.xml = xml
self.parent = parent
self.segment_base = DashSegmentBaseInfo(xml)
self.representations = []
for r in self.xml.findall(DASH_NS+'Representation'):
self.representations.append(DashRepresentation(r, self))
def __str__(self):
result = 'Adaptation Set:\n' + '\n'.join([str (r) for r in self.representations])
return result
class DashPeriod:
def __init__(self, xml, parent):
self.xml = xml
self.parent = parent
self.segment_base = DashSegmentBaseInfo(xml)
self.adaptation_sets = []
for s in self.xml.findall(DASH_NS+'AdaptationSet'):
self.adaptation_sets.append(DashAdaptationSet(s, self))
def __str__(self):
result = 'Period:\n' + '\n'.join([str(s) for s in self.adaptation_sets])
return result
class DashMPD:
def __init__(self, url, xml):
self.url = url
self.xml = xml
self.parent = None
self.periods = []
self.segment_base = DashSegmentBaseInfo(xml)
self.type = xml.get('type')
for p in self.xml.findall(DASH_NS+'Period'):
self.periods.append(DashPeriod(p, self))
# compute base URL (note: we'll just use the MPD URL for now)
self.base_urls = [url]
base_url = self.xml.find(DASH_NS+'BaseURL')
if base_url is not None:
self.base_urls = [base_url.text]
def __str__(self):
result = "MPD:\n" + '\n'.join([str(p) for p in self.periods])
return result
def ParseMpd(url, xml):
mpd_tree = ElementTree.XML(xml)
if mpd_tree.tag.startswith(DASH_NS_COMPAT):
global DASH_NS
global DASH_NS_URN
DASH_NS = DASH_NS_COMPAT
DASH_NS_URN = DASH_NS_URN_COMPAT
if Options.verbose:
print '@@@ Using backward compatible namespace'
mpd = DashMPD(url, mpd_tree)
if not (mpd.type is None or mpd.type == 'static'):
raise Exception('Only static MPDs are supported')
return mpd
def MakeNewDir(dir, is_warning=False):
if os.path.exists(dir):
if is_warning:
print 'WARNING: ',
else:
print 'ERROR: ',
print 'directory "'+dir+'" already exists'
if not is_warning:
sys.exit(1)
else:
os.mkdir(dir)
def OpenURL(url):
if url.startswith("file://"):
return open(url[7:], 'rb')
else:
return urllib2.urlopen(url)
def ComputeUrl(base_url, url):
if url.startswith('http://') or url.startswith('https://'):
raise Exception('Absolute URLs are not supported')
if base_url.startswith('file://'):
return os.path.join(os.path.dirname(base_url), url)
else:
return urlparse.urljoin(base_url, url)
class Cloner:
def __init__(self, root_dir):
self.root_dir = root_dir
self.track_ids = []
self.init_filename = None
def CloneSegment(self, url, path_out, is_init):
while path_out.startswith('/'):
path_out = path_out[1:]
target_dir = os.path.join(self.root_dir, path_out)
if Options.verbose:
print 'Cloning', url, 'to', path_out
#os.makedirs(target_dir)
try:
os.makedirs(os.path.dirname(target_dir))
except OSError:
if os.path.exists(target_dir):
pass
except:
raise
data = OpenURL(url)
outfile_name = os.path.join(self.root_dir, path_out)
use_temp_file = False
if Options.encrypt:
use_temp_file = True
outfile_name_final = outfile_name
outfile_name += '.tmp'
outfile = open(outfile_name, 'wb+')
try:
shutil.copyfileobj(data, outfile)
outfile.close()
if Options.encrypt:
if is_init:
self.track_ids = GetTrackIds(outfile_name)
self.init_filename = outfile_name
#shutil.copyfile(outfile_name, outfile_name_final)
args = ["--method", "MPEG-CENC"]
for t in self.track_ids:
args.append("--property")
args.append(str(t)+":KID:"+Options.kid.encode('hex'))
for t in self.track_ids:
args.append("--key")
args.append(str(t)+":"+Options.key.encode('hex')+':random')
args += [outfile_name, outfile_name_final]
if not is_init:
args += ["--fragments-info", self.init_filename]
if Options.verbose:
print 'mp4encrypt '+(' '.join(args))
Bento4Command("mp4encrypt", *args)
finally:
if use_temp_file and not is_init:
os.unlink(outfile_name)
def Cleanup(self):
if (self.init_filename):
os.unlink(self.init_filename)
def main():
# determine the platform binary name
platform = sys.platform
if platform.startswith('linux'):
platform = 'linux-x86'
elif platform.startswith('darwin'):
platform = 'macosx'
# parse options
parser = OptionParser(usage="%prog [options] <file-or-http-url> <output-dir>\n")
parser.add_option('', '--quiet', dest="verbose",
action='store_false', default=True,
help="Be quiet")
parser.add_option('', "--encrypt", metavar='<KID:KEY>',
dest='encrypt', default=None,
help="Encrypt the media, with KID and KEY specified in Hex (32 characters each)")
parser.add_option('', "--exec-dir", metavar="<exec_dir>",
dest="exec_dir", default=os.path.join(SCRIPT_PATH, 'bin', platform),
help="Directory where the Bento4 executables are located")
global Options
(Options, args) = parser.parse_args()
if len(args) != 2:
parser.print_help()
sys.exit(1)
# process arguments
mpd_url = args[0]
output_dir = args[1]
if Options.encrypt:
if len(Options.encrypt) != 65:
raise Exception('Invalid argument for --encrypt option')
Options.kid = Options.encrypt[:32].decode('hex')
Options.key = Options.encrypt[33:].decode('hex')
# create the output dir
MakeNewDir(output_dir, True)
# load and parse the MPD
if Options.verbose: print "Loading MPD from", mpd_url
try:
mpd_xml = OpenURL(mpd_url).read()
except Exception as e:
print "ERROR: failed to load MPD:", e
sys.exit(1)
if Options.verbose: print "Parsing MPD"
mpd_xml = mpd_xml.replace('nitialisation', 'nitialization')
mpd = ParseMpd(mpd_url, mpd_xml)
ElementTree.register_namespace('', DASH_NS_URN)
ElementTree.register_namespace('mas', MARLIN_MAS_NS_URN)
cloner = Cloner(output_dir)
for period in mpd.periods:
for adaptation_set in period.adaptation_sets:
for representation in adaptation_set.representations:
# compute the base URL
base_url = representation.AttributeLookup('base_urls')[0]
if Options.verbose:
print 'Base URL = '+base_url
# process the init segment
if Options.verbose:
print '### Processing Initialization Segment'
url = ComputeUrl(base_url, representation.init_segment_url)
cloner.CloneSegment(url, representation.init_segment_url, True)
# process all segment URLs
if Options.verbose:
print '### Processing Media Segments for AdaptationSet', representation.id
for seg_url in representation.GenerateSegmentUrls():
url = ComputeUrl(base_url, seg_url)
try:
cloner.CloneSegment(url, seg_url, False)
except (urllib2.HTTPError, urllib2.URLError, IOError):
# move to the next representation
break
# cleanup the init segment
cloner.Cleanup()
# modify the MPD if needed
if Options.encrypt:
for p in mpd.xml.findall(DASH_NS+'Period'):
for s in p.findall(DASH_NS+'AdaptationSet'):
cp = ElementTree.Element(DASH_NS+'ContentProtection', schemeIdUri='urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4')
cp.tail = s.tail
cids = ElementTree.SubElement(cp, MARLIN_MAS_NS+'MarlinContentIds')
cid = ElementTree.SubElement(cids, MARLIN_MAS_NS+'MarlinContentId')
cid.text = 'urn:marlin:kid:'+Options.kid.encode('hex')
s.insert(0, cp)
# write the MPD
xml_tree = ElementTree.ElementTree(mpd.xml)
xml_tree.write(os.path.join(output_dir, os.path.basename(urlparse.urlparse(mpd_url).path)), encoding="UTF-8", xml_declaration=True)
###########################
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
if __name__ == '__main__':
main()