Skip to content

Commit 9233d7f

Browse files
committed
add support BuyDRM.v4
1 parent c32b916 commit 9233d7f

File tree

5 files changed

+240
-0
lines changed

5 files changed

+240
-0
lines changed

qencode/drm/buydrm_v4.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# for python2.7
2+
import os
3+
from lxml import etree
4+
from signxml import XMLSigner, XMLVerifier
5+
#
6+
import const
7+
8+
def create_cpix_user_request(
9+
key_ids, media_id,
10+
content_id, commonEncryptionScheme,
11+
private_key, public_cert, delivery_public_cert=None,
12+
use_playready=False, use_widevine=False, use_fairplay=False,
13+
nsmap=const.NSMAP
14+
):
15+
if delivery_public_cert is None:
16+
delivery_public_cert_path = (os.path.dirname(__file__) + '/keys/qencode-public_cert.pem')
17+
delivery_public_cert = open(delivery_public_cert_path, 'rb').read()
18+
19+
root = etree.Element('{%s}CPIX' % nsmap['cpix'],
20+
name=media_id, contentId=content_id, nsmap=nsmap)
21+
root.set('{%s}schemaLocation' % nsmap['xsi'],
22+
'urn:dashif:org:cpix cpix.xsd')
23+
24+
delivery_data_list = etree.SubElement(root, '{%s}DeliveryDataList' % nsmap['cpix'])
25+
delivery_data = etree.SubElement(delivery_data_list,'{%s}DeliveryData' % nsmap['cpix'])
26+
delivery_key = etree.SubElement(delivery_data, '{%s}DeliveryKey' % nsmap['cpix'])
27+
28+
x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds'])
29+
x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds'])
30+
x509_cert.text = delivery_public_cert.replace(
31+
'-----BEGIN CERTIFICATE-----', ''
32+
).replace(
33+
'-----END CERTIFICATE-----', ''
34+
).replace('\n', '')
35+
36+
content_key_list = etree.SubElement(root,
37+
'{%s}ContentKeyList' % nsmap['cpix'])
38+
content_key_usage_list = etree.SubElement(root,
39+
'{%s}ContentKeyUsageRuleList' % nsmap['cpix'])
40+
drm_system_list = etree.SubElement(root,
41+
'{%s}DRMSystemList' % nsmap['cpix'])
42+
43+
for data in key_ids:
44+
if commonEncryptionScheme == 'default':
45+
etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'])
46+
else:
47+
etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'],
48+
commonEncryptionScheme=commonEncryptionScheme)
49+
50+
if use_playready:
51+
etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'],
52+
systemId=const.SYSTEM_ID_PLAYREADY)
53+
54+
if use_widevine:
55+
etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'],
56+
systemId=const.SYSTEM_ID_WIDEVINE)
57+
58+
if use_fairplay:
59+
etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'],
60+
systemId=const.SYSTEM_ID_FAIRPLAY)
61+
62+
etree.SubElement(content_key_usage_list,
63+
'{%s}ContentKeyUsageRule' % nsmap['cpix'],
64+
kid=data['kid'],
65+
intendedTrackType=data['track_type'])
66+
67+
signed_root = XMLSigner(
68+
c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
69+
signature_algorithm='rsa-sha256',
70+
digest_algorithm='sha512'
71+
).sign(root, key=private_key, cert=public_cert)
72+
73+
xml_text = etree.tostring(signed_root, encoding='utf-8')
74+
75+
return xml_text

qencode/drm/const.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#
2+
CPIX_API_URL_V4 = 'https://cpix-integration.keyos.com/api/v4/getKeys'
3+
LA_URL_V4 = 'https://widevine.keyos.com/api/v4/getLicense'
4+
5+
NSMAP = {
6+
'cpix': 'urn:dashif:org:cpix',
7+
'enc': 'http://www.w3.org/2001/04/xmlenc#',
8+
'pskc': 'urn:ietf:params:xml:ns:keyprov:pskc',
9+
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
10+
'ds': 'http://www.w3.org/2000/09/xmldsig#'
11+
}
12+
13+
SYSTEM_ID_PLAYREADY = '9a04f079-9840-4286-ab92-e65be0885f95'
14+
SYSTEM_ID_WIDEVINE = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
15+
SYSTEM_ID_FAIRPLAY = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2'
16+
17+
TRACK_TYPES = [
18+
('SD', 0), # (<720p)
19+
('HD', 720), # (720p)
20+
('FULLHD', 1080), # (1080p)
21+
('2KUHD', 1440), # (1440p)
22+
('4KUHD', 2160), # (2160p)
23+
('8KUHD', 4320), # (4320p)
24+
('16KUHD', 8640), # (8640p)
25+
]
26+
27+
DEFAULT_TRACK_TYPE = 'SD'
28+
AUDIO_TRACK_TYPE = 'AUDIO' # SD - old
29+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFTzCCAzegAwIBAgIUWkfteBfJlQamCwn6AyXY1X4mhTkwDQYJKoZIhvcNAQEL
3+
BQAwNzELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB1FlbmNvZGUxFjAUBgNVBAMMDSou
4+
cWVuY29kZS5jb20wHhcNMjUxMTA2MTc1NjQ0WhcNMjkxMTA2MTc1NjQ0WjA3MQsw
5+
CQYDVQQGEwJVUzEQMA4GA1UECgwHUWVuY29kZTEWMBQGA1UEAwwNKi5xZW5jb2Rl
6+
LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMHuQqzvoFV7mFsE
7+
SNOefpN1hyXQvnQ5qRLt/hyX+0CM4wx+HgCylijPuL4tujcxGdC1QhrD3+ntv9yk
8+
e8Et5PxXIsoxanra6LXXP9UAKljmw2ktfEIBPCyblzCgETeedGH/QqDy+ys8cgQ8
9+
D/y4wDCwJdWU888qFQaFXgY432tzVLpz4pWqJwrV/EacVk5gVSmT/uFIal+D4rDg
10+
OFi9IAsc9FKVwKnZQoSWsVLoqal/pMVqW6jPMqqQhMClBFVvONBBIC4ztEckLhXu
11+
prkToKHE/sffPvOPoZSzlnz5wTD0WZXjj+RbcFU1mPjZ8lwUBgCn/VTD8SOO5YZx
12+
lpagF+3Qf/nCKjYBupBxvvI9Dfdoai6HvP0clT/99igG9yl0v5viynAW+fNuUhg5
13+
Krc6rZfw8AgQeJPHgUYElA+rcrhbVamVKLhYhCBAoai12sxMa+EvCwW++I+d/SA0
14+
9Gft2E1fEWgCyFgzXRKJPPI9CONYe5hL4FitERZhe0oktwbnvT0x9YRU1wb56sG/
15+
3ykTSaVToGROnjn8reuLRqWgR06cuJjtHqtB+yBbRc5jv1OPL97atzyqePnX5wIN
16+
hiMcse+4IRGR/jtD45AAXQkjwT/MaHjcHpcjaHWUO5w3nV6QIxam7dmXadNgoEbg
17+
a33gqLmj29/RX++nvFOq+9HFD18JAgMBAAGjUzBRMB0GA1UdDgQWBBT8cKty8KH6
18+
TxiSTunAN7RbyQVkNjAfBgNVHSMEGDAWgBT8cKty8KH6TxiSTunAN7RbyQVkNjAP
19+
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA5XSQVhTpuSdEAhG+e
20+
/tQlbS0yZOuPZPMApzyy8+axTZjitpRZPULvoRKnpldmTDFVRqPwBFXhKWBLufxo
21+
Ghr6GKAG8IZgcr66dx8XquFHIQLooyHpFAx9t7J/SfdWKoKpIugaSDH6Jg4WywoZ
22+
lKjmql9f5QMTRNgOVhDDlMv+vDMQ+30idUODhMwZ36NOlcb0XCeOxE5iGUH3CE4b
23+
XG6RRS2u3rpRH36PL4KFd28uMy73zsxp5ocSrSpDAIibec3qpYSIRVeiGxBZf9q1
24+
B1q7Ta1BIWzWr7GtkIXxxxLwfnki/QiiEhCX4fUNAXeBH9/XHbDbBUa/qCLsCJ+m
25+
NqLe6AJqea7xo/+KIy+qM3gmn+RFRLhf1NbxX5J+MKIyBdPqtdAvFCcRsbaRSgcP
26+
nIEUYrHJhH+2d3bTKb7Tb9jx1PAXbWXpt4QpnmhQUHtmXGVgoQFIUgNUEsGBnN3n
27+
DMO8n8pav3s1Rwd9c34DKpgC3PkFxosKKNeH2W/r5nmSBNVGAeIPM/81B7VkjmpE
28+
Lrc9yc0V4risNsekYIJpMY+k6nrSklofyyCDalS263toxPrYNrxHjd899zsekBvn
29+
2X+1hvxRQ5BxFgmIBgTS6/vhs/vFDE80cX0te852cV7toNDRcX/72x/lO18FpjUt
30+
cLKMgddCWrOK6HjBetTK1CkKHA==
31+
-----END CERTIFICATE-----
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import base64
2+
import json
3+
import uuid
4+
import time
5+
import qencode
6+
from qencode.drm.buydrm_v4 import create_cpix_user_request
7+
from qencode import QencodeClientException, QencodeTaskException
8+
9+
# replace with your API KEY (can be found in your Project settings on Qencode portal)
10+
# https://portal.qencode.com/
11+
API_KEY = 'your-api-qencode-key'
12+
# specify path to your BuyDRM certificate files,
13+
# for example create dir keys/ and put keys into
14+
enduser_pvk, enduser_pub = [
15+
open('keys/' + p, 'r').read() for p in (
16+
'user-private_key.pem',
17+
'user-public_cert.pem',
18+
# 'qencode-public_cert.pem',
19+
)
20+
]
21+
# Qencode query template for job with {cpix_request}
22+
query_json = 'query.json'
23+
# correspond to stream resolution in query.json
24+
key_ids = [
25+
{ 'kid': str(uuid.uuid4()), 'track_type': 'SD' },
26+
{ 'kid': str(uuid.uuid4()), 'track_type': 'HD' }
27+
]
28+
# need for BuyDRM
29+
media_id = 'my asset'
30+
content_id = 'group21'
31+
common_encryption = 'cenc'
32+
# unified with the new BuyDRM API params
33+
drm_list = {
34+
'PR': True, # use_playready
35+
'WV': True, # use_widevine
36+
'FP': False # use_fairplay
37+
}
38+
# for create log files: state-<token>.json, xml
39+
debug = True
40+
41+
def start_encode():
42+
cpix_request = create_cpix_user_request(
43+
key_ids, media_id,
44+
content_id, common_encryption,
45+
enduser_pvk, enduser_pub, #delivery_public_cert=None, # if None - get from SDK, else - set public Qenocde certificate
46+
use_playready=drm_list['PR'], use_widevine=drm_list['WV'], use_fairplay=drm_list['FP']
47+
)
48+
49+
client = qencode.client(API_KEY)
50+
if client.error:
51+
raise QencodeClientException(client.message)
52+
53+
print('The client created. Expire date: %s' % client.expire)
54+
55+
task = client.create_task()
56+
57+
if task.error:
58+
raise QencodeTaskException(task.message)
59+
60+
template = open(query_json, 'r').read()
61+
62+
query = template.replace('{cpix_request}', base64.b64encode(cpix_request))
63+
64+
task.custom_start(query)
65+
66+
if task.error:
67+
raise QencodeTaskException(task.message)
68+
task_token = task.task_token
69+
print('Start encode. Task: %s' % task_token)
70+
71+
while True:
72+
status = task.status()
73+
print('Job %s status: \n %s' % (task_token, json.dumps(status, indent=2, sort_keys=True)))
74+
if status['error'] or status['status'] == 'completed':
75+
break
76+
time.sleep(5)
77+
status = task.extend_status()
78+
print('Job %s finished with status "%s" and error: %s' % \
79+
(task_token, status['status'], status['error'])
80+
)
81+
if debug:
82+
open('job-cpix_request-%s.xml' % task_token, 'w').write(cpix_request)
83+
open('job-query-%s.json' % task_token, 'w').write(query)
84+
open('job-result-%s.json' % task_token, 'w').write(json.dumps(status, indent=2, sort_keys=True))
85+
86+
if __name__ == '__main__':
87+
start_encode()

sample-code/drm/buydrm/query.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"query": {
3+
"encoder_version": "2",
4+
"format": [
5+
{
6+
"output": "advanced_dash",
7+
"stream": [
8+
{ "video_codec": "libx264", "height": 360, "audio_bitrate": 128, "keyframe": 25, "bitrate": 950 },
9+
{ "video_codec": "libx264", "height": 720, "audio_bitrate": 128, "keyframe": 25, "bitrate": 2000 }
10+
],
11+
"buydrm_drm": {
12+
"request": "{cpix_request}"
13+
}
14+
}
15+
],
16+
"source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4"
17+
}
18+
}

0 commit comments

Comments
 (0)