Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support Nagra DRM in mp4-dash and mp4-dash-clone script #96

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 82 additions & 15 deletions Source/Python/utils/mp4-dash-clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
DASH_NS = '{'+DASH_NS_URN+'}'
MARLIN_MAS_NS_URN = 'urn:marlin:mas:1-0:services:schemas:mpd'
MARLIN_MAS_NS = '{'+MARLIN_MAS_NS_URN+'}'
NAGRA_SCHEME_ID_URI = 'urn:uuid:adb41c24-2dbf-4a6d-958b-4457c0d27b95'
NAGRA_PRM_NS_URN = 'urn:nagra:prm:1-0:services:schemas:mpd'
NAGRA_PRM_NS = '{'+NAGRA_PRM_NS_URN+'}'


def Bento4Command(name, *args, **kwargs):
cmd = [os.path.join(Options.exec_dir, name)]
Expand All @@ -43,7 +47,7 @@ def Bento4Command(name, *args, **kwargs):
if not isinstance(kwargs[kwarg], bool):
cmd.append(kwargs[kwarg])
cmd += args
#print cmd
print cmd
try:
return check_output(cmd)
except CalledProcessError, e:
Expand Down Expand Up @@ -378,6 +382,47 @@ def Cleanup(self):
if (self.init_filename):
os.unlink(self.init_filename)


def is_uuid(uuid_str):
'''
check whether uuid_str is valid UUID version 3
@param uuid_str
'''
import uuid
try:
uuid_obj = uuid.UUID(uuid_str, version=3)
return True
except ValueError:
return False


def uuid_2_hex(uuid_str):
'''
convert version 3 UUID to hex string, i.e. remove '-'
'''
return uuid_str.replace('-', '')


def get_kid_and_key(key_spec):
'''
extract Key ID and Key from key_spec
@param key_spec Key_Id:Key
'''
if ':' not in key_spec:
raise Exception('Invalid argument syntax for --encrypt option')
kid_hex, key_hex = key_spec.split(':', 1)
if len(kid_hex) != 32:
if is_uuid(kid_hex):
kid_hex = uuid_2_hex(kid_hex)
else:
raise Exception('Invalid argument format for --encrypt option')
if len(key_hex) != 32:
raise Exception('Invalid argument format for --encryption-key option')
# print('kid_hex:', kid_hex, kid_hex.decode('hex'))
# print('key_hex:', key_hex, key_hex.decode('hex'))
return kid_hex.decode('hex'), key_hex.decode('hex')


def main():
# determine the platform binary name
platform = sys.platform
Expand All @@ -393,26 +438,32 @@ def main():
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)")
help="Encrypt the media, with KID and KEY specified in Hex (32 characters each. Or KID can be in UUID version 3 format)")
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")

parser.add_option('', '--nagra', dest='nagra', action='store_true', default=False,
help='Add Nagra signaliing to the MPD (requires --encryption-key and --content-id option')
parser.add_option('', '--content-id', dest='content_id',
help='Content ID required by Nagra DRM', metavar='<content_id>')


global Options
(Options, args) = parser.parse_args()
if len(args) != 2:
parser.print_help()
sys.exit(1)

if Options.nagra and not Options.content_id:
print('Error: Need contentId for Nagra encryption, use --content-id')
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')

Options.kid, Options.key = get_kid_and_key(Options.encrypt)

# create the output dir
MakeNewDir(output_dir, True)

Expand All @@ -430,6 +481,7 @@ def main():

ElementTree.register_namespace('', DASH_NS_URN)
ElementTree.register_namespace('mas', MARLIN_MAS_NS_URN)
ElementTree.register_namespace('prm', NAGRA_PRM_NS_URN)

cloner = Cloner(output_dir)
for period in mpd.periods:
Expand Down Expand Up @@ -464,19 +516,34 @@ def main():
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')
if Options.nagra:
cp = ElementTree.Element('ContentProtection', schemeIdUri=NAGRA_SCHEME_ID_URI)
cp.tail = s.tail
prm = ElementTree.SubElement(cp, NAGRA_PRM_NS+'PRM')
prmSignalization = ElementTree.SubElement(prm, NAGRA_PRM_NS+'PRMSignalization')
# license = {"contentId":"pz_dash_test_1","keyId":"121a0-fca0-f1b4-75b8-9102-97fa-8e0a07e"}
import uuid
key_id = str(uuid.UUID(Options.kid.encode('hex')))
signalization_info = '{{"contentId":"{content_id}","keyId":"{key_id}"}}'.format(content_id=Options.content_id, key_id=key_id)
if Options.verbose:
print('PRM signalization info: {signalization_info}'.format(signalization_info=signalization_info))
import base64
encoded_signalization_info = base64.b64encode(signalization_info)
prmSignalization.text = encoded_signalization_info
else:
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()

54 changes: 52 additions & 2 deletions Source/Python/utils/mp4-dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
MARLIN_MAS_NAMESPACE = 'urn:marlin:mas:1-0:services:schemas:mpd'
MARLIN_PSSH_SYSTEM_ID = '69f908af481646ea910ccd5dcccb0a3a'

NAGRA_SCHEME_ID_URI = 'urn:uuid:adb41c24-2dbf-4a6d-958b-4457c0d27b95'
NAGRA_PRM_NAMESPACE = 'urn:nagra:prm:1-0:services:schemas:mpd'

PLAYREADY_PSSH_SYSTEM_ID = '9a04f07998404286ab92e65be0885f95'
PLAYREADY_SCHEME_ID_URI = 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'
PLAYREADY_SCHEME_ID_URI_V10 = 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95'
Expand Down Expand Up @@ -106,6 +109,28 @@

TempFiles = []


#############################################
def is_uuid(uuid_str):
'''
check whether uuid_str is valid UUID version 3
@param uuid_str
'''
import uuid
try:
uuid_obj = uuid.UUID(uuid_str, version=3)
return True
except ValueError:
return False


#############################################
def uuid_2_hex(uuid_str):
'''
convert version 3 UUID to hex string, i.e. remove '-'
'''
return uuid_str.replace('-', '')

#############################################
def AddSegmentList(options, container, subdir, track, use_byte_range=False):
if subdir:
Expand Down Expand Up @@ -251,6 +276,21 @@ def AddContentProtection(options, container, tracks):
cid = xml.SubElement(cids, '{' + MARLIN_MAS_NAMESPACE + '}MarlinContentId')
cid.text = 'urn:marlin:kid:' + kid

# Nagra
if options.nagra:
container.append(xml.Comment(' Nagra '))
xml.register_namespace('prm', NAGRA_PRM_NAMESPACE)
cp = xml.SubElement(container, 'ContentProtection', schemeIdUri=NAGRA_SCHEME_ID_URI)
prm = xml.SubElement(cp, '{' + NAGRA_PRM_NAMESPACE + '}PRM')
prmSignalization = xml.SubElement(prm, '{' + NAGRA_PRM_NAMESPACE + '}PRMSignalization')
# license = {"contentId":"pz_dash_test_1","keyId":"121a0fca0f1b475b8910297fa8e0a07e"}
signalization_info = '{{"contentId":"{content_id}","keyId":"{key_id}"}}'.format(content_id=options.content_id, key_id=default_kid)
if options.verbose:
print('signalization info: {signalization_info}'.format(signalization_info=signalization_info))
import base64
encoded_signalization_info = base64.b64encode(signalization_info)
prmSignalization.text = encoded_signalization_info

# PlayReady
if options.playready:
container.append(xml.Comment(' PlayReady '))
Expand Down Expand Up @@ -1012,7 +1052,10 @@ def ResolveEncryptionKeys(options):
raise Exception('Invalid argument syntax for --encryption-key option')
kid_hex, key_hex = key_spec.split(':', 1)
if len(kid_hex) != 32:
raise Exception('Invalid argument format for --encryption-key option')
if is_uuid(kid_hex):
kid_hex = uuid_2_hex(kid_hex)
else:
raise Exception('Invalid argument format for --encryption-key option')

if key_hex.startswith('#'):
if len(key_hex) != 41:
Expand Down Expand Up @@ -1127,14 +1170,18 @@ def main():
help="Use the original DASH MPD namespace as it was specified in the first published specification")
parser.add_option('', "--encryption-key", dest="encryption_key", metavar='<key-spec>', default=None,
help="Encrypt some or all tracks with MPEG CENC (AES-128), where <key-spec> specifies the KID(s) and Key(s) to use, using one of the following forms: " +
"(1) <KID>:<key> with <KID> as a 32-character hex string and <key> either a 32-character hex string or the character '#' followed by a base64-encoded key seed; or " +
"(1) <KID>:<key> with <KID> as a 32-character hex string or UUID version 3 format and <key> either a 32-character hex string or the character '#' followed by a base64-encoded key seed; or " +
"(2) @<key-locator> where <key-locator> is an expression of one of the supported key locator schemes. Each entry may be prefixed with an optional track filter, and multiple <key-spec> entries can be used, separated by ','. (see online docs for details)")
parser.add_option('', "--encryption-args", dest="encryption_args", metavar='<cmdline-arguments>', default=None,
help="Pass additional command line arguments to mp4encrypt (separated by spaces)")
parser.add_option('', "--eme-signaling", dest="eme_signaling", metavar='<eme-signaling-type>', choices=['pssh-v0', 'pssh-v1'],
help="Add EME-compliant signaling in the MPD and PSSH boxes (valid options are 'pssh-v0' and 'pssh-v1')")
parser.add_option('', "--marlin", dest="marlin", action="store_true", default=False,
help="Add Marlin signaling to the MPD (requires an encrypted input, or the --encryption-key option)")
parser.add_option('', '--nagra', dest='nagra', action='store_true', default=False,
help='Add Nagra signaliing to the MPD (requires --encryption-key and --content-id option')
parser.add_option('', '--content-id', dest='content_id',
help='Content ID required by Nagra DRM', metavar='<content_id>')
parser.add_option('', "--marlin-add-pssh", dest="marlin_add_pssh", action="store_true", default=False,
help="Add an (optional) Marlin 'pssh' box in the init segment(s)")
parser.add_option('', "--playready", dest="playready", action="store_true", default=False,
Expand Down Expand Up @@ -1194,6 +1241,9 @@ def main():
if options.max_playout_rate_strategy:
if not options.max_playout_rate_strategy.startswith('lowest:'):
PrintErrorAndExit('Max Playout Rate strategy '+options.max_playout_rate_strategy+' is not supported')
if options.nagra:
if not options.content_id:
PrintErrorAndExit('Need contentId for Nagra encryption, use --content-id')

# switch variables
if options.segment_template_padding:
Expand Down