diff --git a/qencode/client.py b/qencode/client.py index 69d5869..af487ae 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -49,3 +49,17 @@ def get_metadata(self, uri): metadata = Metadata(self.access_token, self.connect) video_info = metadata.get(uri) return video_info + + def tasks(self, start_date='', end_date='', project_name='', status='', limit=10, offset=0, sort='date:desc'): + params = { + 'access_token': self.access_token, + 'start_date': start_date, + 'end_date': end_date, + 'project_name': project_name, + 'status': status, + 'limit': limit, + 'offset': offset, + 'sort': sort + } + response = self.connect.request('tasks', params) + return response diff --git a/qencode/drm/buydrm_v4.py b/qencode/drm/buydrm_v4.py new file mode 100644 index 0000000..1b376d9 --- /dev/null +++ b/qencode/drm/buydrm_v4.py @@ -0,0 +1,77 @@ +# for python2.7 +import os +from lxml import etree +from signxml import XMLSigner, XMLVerifier +# +import const + +def create_cpix_user_request( + key_ids, media_id, + content_id, commonEncryptionScheme, + private_key_path, public_cert_path, delivery_public_cert_path=None, + use_playready=False, use_widevine=False, use_fairplay=False, + nsmap=const.NSMAP + ): + private_key = open(private_key_path, 'r').read() + public_cert = open(public_cert_path, 'r').read() + if delivery_public_cert_path is None: + delivery_public_cert_path = (os.path.dirname(__file__) + '/keys/qencode-public_cert.pem') + delivery_public_cert = open(delivery_public_cert_path, 'rb').read() + + root = etree.Element('{%s}CPIX' % nsmap['cpix'], + name=media_id, contentId=content_id, nsmap=nsmap) + root.set('{%s}schemaLocation' % nsmap['xsi'], + 'urn:dashif:org:cpix cpix.xsd') + + delivery_data_list = etree.SubElement(root, '{%s}DeliveryDataList' % nsmap['cpix']) + delivery_data = etree.SubElement(delivery_data_list,'{%s}DeliveryData' % nsmap['cpix']) + delivery_key = etree.SubElement(delivery_data, '{%s}DeliveryKey' % nsmap['cpix']) + + x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds']) + x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds']) + x509_cert.text = delivery_public_cert.replace( + '-----BEGIN CERTIFICATE-----', '' + ).replace( + '-----END CERTIFICATE-----', '' + ).replace('\n', '') + + content_key_list = etree.SubElement(root, + '{%s}ContentKeyList' % nsmap['cpix']) + content_key_usage_list = etree.SubElement(root, + '{%s}ContentKeyUsageRuleList' % nsmap['cpix']) + drm_system_list = etree.SubElement(root, + '{%s}DRMSystemList' % nsmap['cpix']) + + for data in key_ids: + if commonEncryptionScheme == 'default': + etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid']) + else: + etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'], + commonEncryptionScheme=commonEncryptionScheme) + + if use_playready: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_PLAYREADY) + + if use_widevine: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_WIDEVINE) + + if use_fairplay: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_FAIRPLAY) + + etree.SubElement(content_key_usage_list, + '{%s}ContentKeyUsageRule' % nsmap['cpix'], + kid=data['kid'], + intendedTrackType=data['track_type']) + + signed_root = XMLSigner( + c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315', + signature_algorithm='rsa-sha256', + digest_algorithm='sha512' + ).sign(root, key=private_key, cert=public_cert) + + xml_text = etree.tostring(signed_root, encoding='utf-8') + + return xml_text diff --git a/qencode/drm/const.py b/qencode/drm/const.py new file mode 100644 index 0000000..a126053 --- /dev/null +++ b/qencode/drm/const.py @@ -0,0 +1,29 @@ +# +CPIX_API_URL_V4 = 'https://cpix-integration.keyos.com/api/v4/getKeys' +LA_URL_V4 = 'https://widevine.keyos.com/api/v4/getLicense' + +NSMAP = { + 'cpix': 'urn:dashif:org:cpix', + 'enc': 'http://www.w3.org/2001/04/xmlenc#', + 'pskc': 'urn:ietf:params:xml:ns:keyprov:pskc', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'ds': 'http://www.w3.org/2000/09/xmldsig#' +} + +SYSTEM_ID_PLAYREADY = '9a04f079-9840-4286-ab92-e65be0885f95' +SYSTEM_ID_WIDEVINE = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed' +SYSTEM_ID_FAIRPLAY = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2' + +TRACK_TYPES = [ + ('SD', 0), # (<720p) + ('HD', 720), # (720p) + ('FULLHD', 1080), # (1080p) + ('2KUHD', 1440), # (1440p) + ('4KUHD', 2160), # (2160p) + ('8KUHD', 4320), # (4320p) + ('16KUHD', 8640), # (8640p) +] + +DEFAULT_TRACK_TYPE = 'SD' +AUDIO_TRACK_TYPE = 'AUDIO' # SD - old + diff --git a/qencode/drm/keys/qencode-public_cert.pem b/qencode/drm/keys/qencode-public_cert.pem new file mode 100644 index 0000000..4986de0 --- /dev/null +++ b/qencode/drm/keys/qencode-public_cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFTzCCAzegAwIBAgIUWkfteBfJlQamCwn6AyXY1X4mhTkwDQYJKoZIhvcNAQEL +BQAwNzELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB1FlbmNvZGUxFjAUBgNVBAMMDSou +cWVuY29kZS5jb20wHhcNMjUxMTA2MTc1NjQ0WhcNMjkxMTA2MTc1NjQ0WjA3MQsw +CQYDVQQGEwJVUzEQMA4GA1UECgwHUWVuY29kZTEWMBQGA1UEAwwNKi5xZW5jb2Rl +LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMHuQqzvoFV7mFsE +SNOefpN1hyXQvnQ5qRLt/hyX+0CM4wx+HgCylijPuL4tujcxGdC1QhrD3+ntv9yk +e8Et5PxXIsoxanra6LXXP9UAKljmw2ktfEIBPCyblzCgETeedGH/QqDy+ys8cgQ8 +D/y4wDCwJdWU888qFQaFXgY432tzVLpz4pWqJwrV/EacVk5gVSmT/uFIal+D4rDg +OFi9IAsc9FKVwKnZQoSWsVLoqal/pMVqW6jPMqqQhMClBFVvONBBIC4ztEckLhXu +prkToKHE/sffPvOPoZSzlnz5wTD0WZXjj+RbcFU1mPjZ8lwUBgCn/VTD8SOO5YZx +lpagF+3Qf/nCKjYBupBxvvI9Dfdoai6HvP0clT/99igG9yl0v5viynAW+fNuUhg5 +Krc6rZfw8AgQeJPHgUYElA+rcrhbVamVKLhYhCBAoai12sxMa+EvCwW++I+d/SA0 +9Gft2E1fEWgCyFgzXRKJPPI9CONYe5hL4FitERZhe0oktwbnvT0x9YRU1wb56sG/ +3ykTSaVToGROnjn8reuLRqWgR06cuJjtHqtB+yBbRc5jv1OPL97atzyqePnX5wIN +hiMcse+4IRGR/jtD45AAXQkjwT/MaHjcHpcjaHWUO5w3nV6QIxam7dmXadNgoEbg +a33gqLmj29/RX++nvFOq+9HFD18JAgMBAAGjUzBRMB0GA1UdDgQWBBT8cKty8KH6 +TxiSTunAN7RbyQVkNjAfBgNVHSMEGDAWgBT8cKty8KH6TxiSTunAN7RbyQVkNjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA5XSQVhTpuSdEAhG+e +/tQlbS0yZOuPZPMApzyy8+axTZjitpRZPULvoRKnpldmTDFVRqPwBFXhKWBLufxo +Ghr6GKAG8IZgcr66dx8XquFHIQLooyHpFAx9t7J/SfdWKoKpIugaSDH6Jg4WywoZ +lKjmql9f5QMTRNgOVhDDlMv+vDMQ+30idUODhMwZ36NOlcb0XCeOxE5iGUH3CE4b +XG6RRS2u3rpRH36PL4KFd28uMy73zsxp5ocSrSpDAIibec3qpYSIRVeiGxBZf9q1 +B1q7Ta1BIWzWr7GtkIXxxxLwfnki/QiiEhCX4fUNAXeBH9/XHbDbBUa/qCLsCJ+m +NqLe6AJqea7xo/+KIy+qM3gmn+RFRLhf1NbxX5J+MKIyBdPqtdAvFCcRsbaRSgcP +nIEUYrHJhH+2d3bTKb7Tb9jx1PAXbWXpt4QpnmhQUHtmXGVgoQFIUgNUEsGBnN3n +DMO8n8pav3s1Rwd9c34DKpgC3PkFxosKKNeH2W/r5nmSBNVGAeIPM/81B7VkjmpE +Lrc9yc0V4risNsekYIJpMY+k6nrSklofyyCDalS263toxPrYNrxHjd899zsekBvn +2X+1hvxRQ5BxFgmIBgTS6/vhs/vFDE80cX0te852cV7toNDRcX/72x/lO18FpjUt +cLKMgddCWrOK6HjBetTK1CkKHA== +-----END CERTIFICATE----- diff --git a/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py index 36ef352..eb24523 100644 --- a/sample-code/drm/buydrm/buydrm_widevine_playready.py +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -1,87 +1,79 @@ +import base64 +import json import uuid import time -import json -import base64 import qencode -from qencode.drm.buydrm import create_cpix_user_request +from qencode.drm.buydrm_v4 import create_cpix_user_request from qencode import QencodeClientException, QencodeTaskException # replace with your API KEY (can be found in your Project settings on Qencode portal) +# https://portal.qencode.com/ API_KEY = 'your-api-qencode-key' -# specify path to your BuyDRM certificate files -USER_PVT_KEY_PATH = './keys/user_private_key.pem' -USER_PUB_CERT_PATH = './keys/user_public_cert.pem' +# specify path to your BuyDRM certificate files, +# for example create dir keys/ and put keys into +USER_PVT_KEY_PATH = 'keys/user-private_key.pem' +USER_PUB_CERT_PATH = 'keys/user-public_cert.pem' +# Qencode query template for job with {cpix_request} +query_json = 'query.json' +# correspond to stream resolution in query.json key_ids = [ { 'kid': str(uuid.uuid4()), 'track_type': 'SD' }, { 'kid': str(uuid.uuid4()), 'track_type': 'HD' } ] -media_id = 'my first stream' - - -QUERY = """ -{ - "query": { - "format": [ - { - "output": "advanced_dash", - "stream": [ - { - "video_codec": "libx264", - "height": 360, - "audio_bitrate": 128, - "keyframe": 25, - "bitrate": 950 - } - ], - "buydrm_drm": { - "request": "{cpix_request}" - } - } - ], - "source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4" - } +media_id = 'my first stream' +content_id = 'my movies group' +common_encryption = 'cenc' + +# unified with the new BuyDRM API params +drm_list = { + 'PR': True, # use_playready + 'WV': True, # use_widevine + 'FP': False # use_fairplay } -""" - def start_encode(): - # this creates signed request to BuyDRM cpix_request = create_cpix_user_request( - key_ids, media_id, USER_PVT_KEY_PATH, USER_PUB_CERT_PATH, - use_playready=True, use_widevine=True + key_ids, media_id, + content_id, common_encryption, + USER_PVT_KEY_PATH, USER_PUB_CERT_PATH, + use_playready=drm_list['PR'], use_widevine=drm_list['WV'], use_fairplay=drm_list['FP'] ) client = qencode.client(API_KEY) if client.error: raise QencodeClientException(client.message) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: %s' % client.expire) task = client.create_task() if task.error: raise QencodeTaskException(task.message) - query = QUERY.replace('{cpix_request}', base64.b64encode(cpix_request)) + template = open(query_json, 'r').read() + + query = template.replace('{cpix_request}', base64.b64encode(cpix_request)) task.custom_start(query) if task.error: raise QencodeTaskException(task.message) - - print 'Start encode. Task: %s' % task.task_token + task_token = task.task_token + print('Start encode. Task: %s' % task_token) while True: status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) + print('Job %s status: \n %s' % (task_token, json.dumps(status, indent=2, sort_keys=True))) if status['error'] or status['status'] == 'completed': break time.sleep(5) - - + status = task.extend_status() + print('Job %s finished with status "%s" and error: %s' % \ + (task_token, status['status'], status['error']) + ) + if __name__ == '__main__': start_encode() diff --git a/sample-code/drm/buydrm/query.json b/sample-code/drm/buydrm/query.json new file mode 100644 index 0000000..476879a --- /dev/null +++ b/sample-code/drm/buydrm/query.json @@ -0,0 +1,23 @@ +{ + "query": { + "encoder_version": "2", + "format": [ + { + "output": "advanced_dash", + "stream": [ + { + "video_codec": "libx264", + "height": 360, + "audio_bitrate": 128, + "keyframe": 25, + "bitrate": 950 + } + ], + "buydrm_drm": { + "request": "{cpix_request}" + } + } + ], + "source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4" + } +} \ No newline at end of file diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index 3d9728d..0c5411d 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -36,7 +36,7 @@ FORMAT.destination = DESTINATION #replace with a link to your input video -params.source = 'https://qencode.com/static/1.mp4' +params.source = 'https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4' params.format = [FORMAT] diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index 9541e4c..3dd94e4 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -29,7 +29,7 @@ FORMAT.destination = DESTINATION #replace with a link to your input video -params.source = 'https://qencode.com/static/1.mp4' +params.source = 'https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4' params.format = [FORMAT] diff --git a/sample-code/start_custom_with_dict.py b/sample-code/start_custom_with_dict.py index 32601c3..c5b38ff 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -14,7 +14,7 @@ API_KEY = 'your-api-qencode-key' #replace with a link to your input video -source_url = "https://qencode.com/static/1.mp4" +source_url = "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4" format_240 = dict( output="mp4", diff --git a/sample-code/start_custom_with_json.py b/sample-code/start_custom_with_json.py index 11d8f01..5101cae 100644 --- a/sample-code/start_custom_with_json.py +++ b/sample-code/start_custom_with_json.py @@ -14,7 +14,7 @@ params = """ {"query": { - "source": "https://qencode.com/static/1.mp4", + "source": "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4", "format": [ { "output": "mp4", diff --git a/sample-code/start_with_callback.py b/sample-code/start_with_callback.py index 35e3cfc..072d09d 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -14,7 +14,7 @@ params = """ {"query": { - "source": "https://qencode.com/static/1.mp4", + "source": "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4", "format": [ { "output": "mp4",