From 98df713115552f55213166e4c829237153b7b094 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Tue, 2 Mar 2021 23:00:10 +0200 Subject: [PATCH 01/21] fix for SSL Unverified error --- qencode/httptools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qencode/httptools.py b/qencode/httptools.py index 5a3d89f..a276517 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -2,6 +2,7 @@ import urllib import urllib2 from urlparse import urljoin +import ssl class Http(object): def __init__(self, version, url, debug=False): @@ -15,8 +16,9 @@ def _call_server(self, url, post_data): return json.dumps(response) data = urllib.urlencode(post_data) request = urllib2.Request(url, data) + context = ssl._create_unverified_context() try: - res = urllib2.urlopen(request) + res = urllib2.urlopen(request, context=context) except urllib2.HTTPError as e: headers = e.headers if self._debug else '' response = dict(error=True, message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers)) From a729f4500f9a8fffee0b1774f1bc1c2bba7398fe Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Tue, 2 Mar 2021 23:04:36 +0200 Subject: [PATCH 02/21] fix for SSL Unverified error --- qencode/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index 7b49092..dd8f9d0 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,7 +29,7 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "1.0" +__version__ = "1.0.1" __status__ = "Production/Stable" __author__ = "Qencode" diff --git a/setup.py b/setup.py index 144616d..ab77250 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='1.0', + version='1.0.1', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', From eba9d6e0edb64bb63eca3493c42e2e0e868c89cd Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Fri, 9 Apr 2021 02:21:19 +0300 Subject: [PATCH 03/21] integrated BuyDRM support --- .gitignore | 8 +- qencode/drm/__init__.py | 0 qencode/drm/buydrm.py | 106 ++++++++++++++++++ .../drm/keys/buydrm_qencode_public_cert.pem | 31 +++++ .../drm/buydrm/buydrm_widevine_playready.py | 95 ++++++++++++++++ 5 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 qencode/drm/__init__.py create mode 100644 qencode/drm/buydrm.py create mode 100644 qencode/drm/keys/buydrm_qencode_public_cert.pem create mode 100644 sample-code/drm/buydrm/buydrm_widevine_playready.py diff --git a/.gitignore b/.gitignore index 5f8c625..84d0c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,10 @@ .cache/ .vscode/ -.idea/ \ No newline at end of file +.idea/ +.eggs/ +build/ +dist/ +qencode.egg-info/ +sample-code/drm/buydrm/keys/user_private_key.pem +sample-code/drm/buydrm/keys/user_public_cert.pem diff --git a/qencode/drm/__init__.py b/qencode/drm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qencode/drm/buydrm.py b/qencode/drm/buydrm.py new file mode 100644 index 0000000..ad960d1 --- /dev/null +++ b/qencode/drm/buydrm.py @@ -0,0 +1,106 @@ +from lxml import etree +from signxml import XMLSigner +import os + +NSMAP = { + 'cpix': 'urn:dashif:org:cpix', + 'xenc': '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' + +def create_cpix_user_request( + key_ids, media_id, user_private_key_path, user_public_cert_path, + use_playready=False, use_widevine=False, use_fairplay=False, + nsmap=None +): + document_public_cert_path = os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + """Creates CPIX request XML signed end user + + Arguments: + key_ids {list} -- List of Key IDs and corresponding track quality types. The list is of + the following format - { 'kid': [string in GUID/UUID format], 'track_type': [string track type]}. + + media_id {string} -- Some random name for your asset which is shown in KeyOS console and reports. + + nsmap {list} -- List of namespaces. + + Returns: + string -- CPIX request XML signed end user + """ + nsmap = nsmap if nsmap is not None else NSMAP + + # Own private key and end user's private key used to sign the document + end_user_private_key = open(user_private_key_path, 'rb').read() + + # Own public certificate and end user's public certificate to include into the CPIX request + end_user_public_cert = open(user_public_cert_path, 'rb').read() + document_public_cert = open(document_public_cert_path, 'rb').read() + + root = etree.Element('{%s}CPIX' % nsmap['cpix'], name=media_id, nsmap=nsmap) + root.set('{%s}schemaLocation' % nsmap['xsi'], 'urn:dashif:org:cpix cpix.xsd') + + # Delivery data list + 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']) + + # The public certificate of a partner. This certificate's public key will be used + # to encrypt Document Key which will later be used to encrypt Contnet Keys. + x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds']) + x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds']) + x509_cert.text = document_public_cert.replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace('\n', '') + + # Content key list + content_key_list = etree.SubElement(root, '{%s}ContentKeyList' % nsmap['cpix']) + + # Content key usage rules + content_key_usage_list = etree.SubElement( + root, '{%s}ContentKeyUsageRuleList' % nsmap['cpix'] + ) + + # DRM systems list + drm_system_list = etree.SubElement(root, '{%s}DRMSystemList' % nsmap['cpix']) + + for data in key_ids: + etree.SubElement( + content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'] + ) + if use_playready: + etree.SubElement( + drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=SYSTEM_ID_PLAYREADY + ) + if use_widevine: + etree.SubElement( + drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=SYSTEM_ID_WIDEVINE + ) + if use_fairplay: + etree.SubElement( + drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=SYSTEM_ID_FAIRPLAY + ) + + etree.SubElement( + content_key_usage_list, '{%s}ContentKeyUsageRule' % nsmap['cpix'], + kid=data['kid'], intendedTrackType=data['track_type'] + ) + + # Signing document with end user's data + end_user_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=end_user_private_key, cert=end_user_public_cert) + x509_sign_cert = end_user_signed_root.xpath( + '//ds:X509Certificate', namespaces=nsmap + )[1] + x509_sign_cert.text = x509_sign_cert.text.replace('\n', '') +# + return etree.tostring(end_user_signed_root).decode('utf-8') + diff --git a/qencode/drm/keys/buydrm_qencode_public_cert.pem b/qencode/drm/keys/buydrm_qencode_public_cert.pem new file mode 100644 index 0000000..a20877e --- /dev/null +++ b/qencode/drm/keys/buydrm_qencode_public_cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFUzCCAzugAwIBAgIJALYz0aDtoodZMA0GCSqGSIb3DQEBBQUAMEAxCzAJBgNV +BAYTAlVTMRAwDgYDVQQKDAdRZW5jb2RlMR8wHQYDVQQDDBZRZW5jb2RlIEtleU9T +IENQSVggQVBJMB4XDTIwMTIyMTE5MDAyNFoXDTI0MTIyMDE5MDAyNFowQDELMAkG +A1UEBhMCVVMxEDAOBgNVBAoMB1FlbmNvZGUxHzAdBgNVBAMMFlFlbmNvZGUgS2V5 +T1MgQ1BJWCBBUEkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQD2D5aP +XSFJC3ErYVBNS54x8AnQ4997NY2Md3QtvWy1UT/8wzs87EJ+3uwoFgfj8Tpl3hZD +amvW9BH6SlSX2uPu5uuPovxnjMVQTg6RAdfQf+dOff4dCX9ubUD1iWen17DODUoY +BeZWky6zGTy5n6o9RGPJsE6Er/mIFWUJdKPrXMpnqJ+hfC2fWurMj8DZ2U+TiuBF +PK8cPkYzF8+L6BH7vkYK8Tvcoy42WPBMwO/fmFT5nTFCtmMROt/x/DaIdafF4tnx +X+liJ3q7rABZfHY66zXv4owQRrpZXupGLVS7vQtoTIHgA6wc/MWVFEyXZN66D0RI +5/uxRD7/GLYAbGcdCVuyJ8i/eXQ3B7OWcEiOxz3TfojlbQcaO8vb9hym5/B4H8Mh +XC99ddB1hSgNX/bWiA6kfCYtw8jR4g86vyDcU/K2e2IcliZsd6ehFWEGluSLqHJq +2g4d49gP72aFO7x5k95id271MMxhF8CW2VjARSqgHG6G+8cWHqaLnB2JJJE1606P +irDrEAcsSuYLsse393pfZMhOkKdq6FXRK/E5nnbh2VKeWP491lJaXakhMVqcjVQ5 +NRqHc8iXCS7qDUYDxJubwC00b2qVj+HqN+k3gx9kyU1If2S0dXOEyJEBmCScKw6k +glN+a6N5Wv723TKiMjUq+RgcZBbkCvbuoHdN7QIDAQABo1AwTjAdBgNVHQ4EFgQU +n6hFowsmBOhl3vm3lYj6Uu0baDIwHwYDVR0jBBgwFoAUn6hFowsmBOhl3vm3lYj6 +Uu0baDIwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAvVIt4QBYfCi9 +Ae1SVHGFN6dVSlnSQwC4pBTt8ucrsDs8kE93xyVNMz4OTDZSP0x/AxwV1LFtZOdZ +zauPGygYMf4puVwkns1DqiUkMaGR8PMPQR+SDPeM6qwIhuYLsAyzhKdRx/n+cKmJ +/lzFi20ZJv5y6ozFwUMIskfWnE2RvfV0bbyraR+LQgOA+fy2Vd0IeTOTgXtuNIi/ +q/VMWH2TGjh1vZQmuO1tg8dGU57XFjeK3ZBtcetNwXz6iXzZ5lprGvPG3tr4QRf5 +XiUoS+mJ5mJBBOMWlgGFELA7xAB3cYj00bTE5iE1YZCeE2WHyIiDw6OrILffeU9H +5+bo7R7z5uwn+h5Z+XYJHxS2wg3v7wpoRMFeQXFHlJhdVXlI9cpf0NgNPecnSrs5 +F2hZt1LmbrrFXVzE3BKVvxUILBW+sN9+79fcXAHewL7yPRy6G307Sy187N2RLyJT +mQdOlEaawR2Cgv4QI7NmAuY0Jy0uYqDCP591ZKg+j1lsWi48Eirf3/hMr/TXNDIk +A1MhY+SPW7mGbz6hVQkkJZNYyisAZcelLWyX5X0RwFo1rMQL/BWemdKU3kV4bk8p +cWmsYLPBMN69npjGWLLzovznOWQ4sVAOBt790pgrHDC+giPeqzx2tmmm4jP14IRj +xSPcwUZxCh50J//DsQ2PpVQxrHPnL94= +-----END CERTIFICATE----- diff --git a/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py new file mode 100644 index 0000000..69f254e --- /dev/null +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -0,0 +1,95 @@ +import uuid +import time +import json +import base64 +import qencode +from qencode.drm.buydrm 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) +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' + +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" + } +} +""" + + +def start_encode(): + """ + Create client object + :param api_key: string. required + :param api_url: string. not required + :param api_version: int. not required. default 'v1' + :return: task object + """ + + # 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 + ) + + client = qencode.client(API_KEY) + if client.error: + raise QencodeClientException(client.message) + + 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)) + + task.custom_start(query) + + if task.error: + raise QencodeTaskException(task.message) + + print 'Start encode. Task: %s' % task.task_token + + while True: + status = task.status() + # print status + print json.dumps(status, indent=2, sort_keys=True) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) + + +if __name__ == '__main__': + start_encode() From 79edfe8a250a14ef19971e1dd5210ef46c67feec Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Fri, 9 Apr 2021 02:24:08 +0300 Subject: [PATCH 04/21] updated version to 1.0.2 --- qencode/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index dd8f9d0..335617d 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,7 +29,7 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "1.0.1" +__version__ = "1.0.2" __status__ = "Production/Stable" __author__ = "Qencode" diff --git a/setup.py b/setup.py index ab77250..8f92298 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='1.0.1', + version='1.0.2', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', From ada0b8cdcbe3dcef65717bcb4de99bf082b4c46c Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Sat, 10 Apr 2021 01:46:39 +0300 Subject: [PATCH 05/21] added manifest and +version --- MANIFEST.in | 1 + qencode/__init__.py | 2 +- .../drm/buydrm/buydrm_widevine_playready.py | 22 ++++++++++--------- setup.py | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d45999b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include drm * \ No newline at end of file diff --git a/qencode/__init__.py b/qencode/__init__.py index 335617d..e52c204 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,7 +29,7 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "1.0.2" +__version__ = "1.0.3" __status__ = "Production/Stable" __author__ = "Qencode" diff --git a/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py index 69f254e..2825897 100644 --- a/sample-code/drm/buydrm/buydrm_widevine_playready.py +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -7,7 +7,9 @@ from qencode import QencodeClientException, QencodeTaskException # replace with your API KEY (can be found in your Project settings on Qencode portal) -API_KEY = 'your-api-qencode-key' +#API_KEY = 'your-api-qencode-key' +API_KEY = '5a2a846a26ace' +#API_KEY = '5a5db6fa5b4c5' # specify path to your BuyDRM certificate files USER_PVT_KEY_PATH = './keys/user_private_key.pem' @@ -27,6 +29,12 @@ "format": [ { "output": "advanced_dash", + "destination": { + "url": "s3://nyc3.digitaloceanspaces.com/qencode3/regression_tests/encrypt/buydrm_widevine/dash", + "key": "DRSKM355SM7QT4DB7Q37", + "secret": "CGE1pypu02SfZ8DDPtZ5l1M5drFoVmAoVUrPBkQdAjM", + "permissions": "public-read" + }, "stream": [ { "video_codec": "libx264", @@ -48,21 +56,15 @@ def start_encode(): - """ - Create client object - :param api_key: string. required - :param api_url: string. not required - :param api_version: int. not required. default 'v1' - :return: task object - """ - # 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 ) - client = qencode.client(API_KEY) + #client = qencode.client(API_KEY, api_url='https://stage-sfo2-1-api-do.qencode.com/') + #client = qencode.client(API_KEY, api_url='https://qa-sfo2-api-do.qencode.com/') + client = qencode.client(API_KEY, api_url='https://prod-nyc1-api-do.qencode.com/') if client.error: raise QencodeClientException(client.message) diff --git a/setup.py b/setup.py index 8f92298..9b7fdd4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='1.0.2', + version='1.0.3', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', From 96bd23c0a76c93ef4c0c0ba70772c175d2c23b11 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Sat, 10 Apr 2021 01:49:02 +0300 Subject: [PATCH 06/21] update drm sample --- sample-code/drm/buydrm/buydrm_widevine_playready.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py index 2825897..787d2bb 100644 --- a/sample-code/drm/buydrm/buydrm_widevine_playready.py +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -7,9 +7,7 @@ from qencode import QencodeClientException, QencodeTaskException # replace with your API KEY (can be found in your Project settings on Qencode portal) -#API_KEY = 'your-api-qencode-key' -API_KEY = '5a2a846a26ace' -#API_KEY = '5a5db6fa5b4c5' +API_KEY = 'your-api-qencode-key' # specify path to your BuyDRM certificate files USER_PVT_KEY_PATH = './keys/user_private_key.pem' @@ -62,9 +60,7 @@ def start_encode(): use_playready=True, use_widevine=True ) - #client = qencode.client(API_KEY, api_url='https://stage-sfo2-1-api-do.qencode.com/') - #client = qencode.client(API_KEY, api_url='https://qa-sfo2-api-do.qencode.com/') - client = qencode.client(API_KEY, api_url='https://prod-nyc1-api-do.qencode.com/') + client = qencode.client(API_KEY) if client.error: raise QencodeClientException(client.message) From ebe71647599ca02b05a3a34734de643214ce0ba4 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Sat, 10 Apr 2021 01:56:01 +0300 Subject: [PATCH 07/21] removed manifest and included qencode.drm package --- MANIFEST.in | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d45999b..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-include drm * \ No newline at end of file diff --git a/setup.py b/setup.py index 9b7fdd4..ff90f2a 100644 --- a/setup.py +++ b/setup.py @@ -29,5 +29,5 @@ ], keywords='qencode, qencode.com, cloud.qencode.com', - packages=['qencode'] + packages=['qencode', 'qencode.drm'] ) From 73e049996629ec3ac17767f90c03ec7a5a751bea Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Sat, 10 Apr 2021 02:00:08 +0300 Subject: [PATCH 08/21] include_package_data=True --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff90f2a..033e105 100644 --- a/setup.py +++ b/setup.py @@ -29,5 +29,6 @@ ], keywords='qencode, qencode.com, cloud.qencode.com', - packages=['qencode', 'qencode.drm'] + packages=['qencode', 'qencode.drm'], + include_package_data=True ) From 518c0be1fc8e1fec8bea187f39b141e8c5c1e736 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Sat, 10 Apr 2021 02:02:51 +0300 Subject: [PATCH 09/21] package_data --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 033e105..6981823 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,6 @@ ], keywords='qencode, qencode.com, cloud.qencode.com', packages=['qencode', 'qencode.drm'], + package_data={'qencode.drm': ['keys/buydrm_qencode_public_cert.pem']}, include_package_data=True ) From 2d74a053af97b39ff571102d71f667677acc7d26 Mon Sep 17 00:00:00 2001 From: Qencode Date: Sun, 11 Apr 2021 12:57:42 +0300 Subject: [PATCH 10/21] Update buydrm_widevine_playready.py --- sample-code/drm/buydrm/buydrm_widevine_playready.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py index 787d2bb..36ef352 100644 --- a/sample-code/drm/buydrm/buydrm_widevine_playready.py +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -27,12 +27,6 @@ "format": [ { "output": "advanced_dash", - "destination": { - "url": "s3://nyc3.digitaloceanspaces.com/qencode3/regression_tests/encrypt/buydrm_widevine/dash", - "key": "DRSKM355SM7QT4DB7Q37", - "secret": "CGE1pypu02SfZ8DDPtZ5l1M5drFoVmAoVUrPBkQdAjM", - "permissions": "public-read" - }, "stream": [ { "video_codec": "libx264", From b5a4a22914b8c4019e398fe14da9a90a3c075c84 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 27 May 2021 17:59:03 +0300 Subject: [PATCH 11/21] added AWS Signed Url, Fairplay DRM, Widevine DRM, Playready DRM --- qencode/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index d97309a..79674be 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,10 +29,9 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "1.0.3" from tools import generate_aws_signed_url, fps_drm, cenc_drm -__version__ = "1.0.1" +__version__ = "1.0.3" __status__ = "Production/Stable" __author__ = "Qencode" From 5ca659b0f82314ef420ce398c291340d20f4e7a0 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 27 May 2021 18:03:37 +0300 Subject: [PATCH 12/21] added AWS Signed Url, Fairplay DRM, Widevine DRM, Playready DRM --- qencode/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index 79674be..045c2d4 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -31,7 +31,7 @@ def x265_video_codec(): from tools import generate_aws_signed_url, fps_drm, cenc_drm -__version__ = "1.0.3" +__version__ = "1.0.4" __status__ = "Production/Stable" __author__ = "Qencode" diff --git a/setup.py b/setup.py index 6981823..028d1bc 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='1.0.3', + version='1.0.4', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', From 6aa19f026625ee5c9a5609db5d3f26be48ff51fd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 10 Aug 2020 11:40:28 -0500 Subject: [PATCH 13/21] black: Skip string normalization To enable string normalization, remove this file/toggle the option and rerun black. Without string normalization the formatter is purely structural, single quotes will not be changed to double quotes. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0097e9f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +skip-string-normalization = true From 4353452aefccb907923b804229fba2ea01a3ddb1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Dec 2021 08:30:00 -0600 Subject: [PATCH 14/21] Format code with black (PEP-8) See also: - https://www.python.org/dev/peps/pep-0008/#indentation - https://black.readthedocs.io/en/stable/the_black_code_style.html black *.py tests/**/*.py qencode/**/*.py --- qencode/__init__.py | 42 +++-- qencode/client.py | 90 ++++----- qencode/const.py | 36 ++-- qencode/custom_params.py | 238 +++++++++++------------ qencode/drm/buydrm.py | 58 ++++-- qencode/exeptions.py | 21 +-- qencode/httptools.py | 80 ++++---- qencode/metadata.py | 11 +- qencode/task.py | 394 ++++++++++++++++++++------------------- qencode/tools.py | 107 +++++++---- qencode/tus_uploader.py | 27 +-- qencode/utils.py | 133 +++++++------ setup.py | 5 +- tests/test_client.py | 8 +- 14 files changed, 673 insertions(+), 577 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index 045c2d4..f11b550 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,31 +1,44 @@ - def client(api_key, api_url=None, version=None, **kwargs): from client import QencodeApiClient + return QencodeApiClient(api_key, api_url=api_url, version=version, **kwargs) + def custom_params(): - from custom_params import CustomTranscodingParams - return CustomTranscodingParams() + from custom_params import CustomTranscodingParams + + return CustomTranscodingParams() + def format(): - from custom_params import Format - return Format() + from custom_params import Format + + return Format() + def destination(): - from custom_params import Destination - return Destination() + from custom_params import Destination + + return Destination() + def stream(): - from custom_params import Stream - return Stream() + from custom_params import Stream + + return Stream() + def x264_video_codec(): - from custom_params import Libx264_VideoCodecParameters - return Libx264_VideoCodecParameters() + from custom_params import Libx264_VideoCodecParameters + + return Libx264_VideoCodecParameters() + def x265_video_codec(): - from custom_params import Libx265_VideoCodecParameters - return Libx265_VideoCodecParameters() + from custom_params import Libx265_VideoCodecParameters + + return Libx265_VideoCodecParameters() + from exeptions import QencodeClientException, QencodeTaskException @@ -34,6 +47,3 @@ def x265_video_codec(): __version__ = "1.0.4" __status__ = "Production/Stable" __author__ = "Qencode" - - - diff --git a/qencode/client.py b/qencode/client.py index 993043c..69d5869 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -2,50 +2,50 @@ from task import Task from metadata import Metadata + class QencodeApiClient(object): - """ - :return: encoder object - - """ - def __init__(self, api_key, api_url=None, version=None): - self.api_key = api_key - self.api_url = api_url if api_url else 'https://api.qencode.com/' - self.version = version if version else 'v1' - self.connect = Http(self.version, self.api_url) - self.access_token = None - self.expire = None - self.error = None - self.code = None - self.message = '' - self._get_access_token() - - - def create_task(self, **kwargs): - return Task(self.access_token, self.connect, **kwargs) - - def refresh_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - - def _get_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - def get_metadata(self, uri): - metadata = Metadata(self.access_token, self.connect) - video_info = metadata.get(uri) - return video_info \ No newline at end of file + """ + :return: encoder object + + """ + + def __init__(self, api_key, api_url=None, version=None): + self.api_key = api_key + self.api_url = api_url if api_url else 'https://api.qencode.com/' + self.version = version if version else 'v1' + self.connect = Http(self.version, self.api_url) + self.access_token = None + self.expire = None + self.error = None + self.code = None + self.message = '' + self._get_access_token() + + def create_task(self, **kwargs): + return Task(self.access_token, self.connect, **kwargs) + + def refresh_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def _get_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def get_metadata(self, uri): + metadata = Metadata(self.access_token, self.connect) + video_info = metadata.get(uri) + return video_info diff --git a/qencode/const.py b/qencode/const.py index 353804b..f72355e 100644 --- a/qencode/const.py +++ b/qencode/const.py @@ -3,22 +3,26 @@ SLEEP_ERROR = 60 COMPLETED_STATUS = ['completed', 'saved'] -ERROR_OK = 0 -ERROR_SERVER_INTERNAL = 1 -ERROR_BAD_APP_ID = 2 -ERROR_APP_ID_NOT_FOUND = 3 -ERROR_BAD_TOKEN = 4 -ERROR_TOKEN_NOT_FOUND = 5 -ERROR_TARIFF_NOT_PAID = 6 -ERROR_MASTER_NOT_FOUND = 7 -ERROR_SYSTEM_BUSY = 8 -ERROR_BAD_PAYLOAD = 9 +ERROR_OK = 0 +ERROR_SERVER_INTERNAL = 1 +ERROR_BAD_APP_ID = 2 +ERROR_APP_ID_NOT_FOUND = 3 +ERROR_BAD_TOKEN = 4 +ERROR_TOKEN_NOT_FOUND = 5 +ERROR_TARIFF_NOT_PAID = 6 +ERROR_MASTER_NOT_FOUND = 7 +ERROR_SYSTEM_BUSY = 8 +ERROR_BAD_PAYLOAD = 9 ERROR_PROJECT_NOT_FOUND = 10 -ERROR_BAD_PROFILE = 11 +ERROR_BAD_PROFILE = 11 ERROR_PROFILE_NOT_FOUND = 12 -ERROR_BAD_TOKENS = 13 -ERROR_FIELD_REQUIRED = 14 +ERROR_BAD_TOKENS = 13 +ERROR_FIELD_REQUIRED = 14 -FPS_DRM_KEYGENERATOR_URI_TEMPLATE = 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=2' -CENC_DRM_KEYGENERATOR_URI_TEMPLATE = 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=1' -DRM_KEY_URL_TEMPLATE = 'skd://fps.ezdrm.com/;%s' \ No newline at end of file +FPS_DRM_KEYGENERATOR_URI_TEMPLATE = ( + 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=2' +) +CENC_DRM_KEYGENERATOR_URI_TEMPLATE = ( + 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=1' +) +DRM_KEY_URL_TEMPLATE = 'skd://fps.ezdrm.com/;%s' diff --git a/qencode/custom_params.py b/qencode/custom_params.py index 847f0a7..f23aef5 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -2,143 +2,151 @@ from json import JSONEncoder from utils import rm_attributes_if_null + class CustomTranscodingParams(object): - """CustomTranscodingParams + """CustomTranscodingParams :var source: String. Source video URI. Can be http(s) url or tus uri :var format: String. A list of objects, each describing params for a single output video stream (MP4, WEBM, HLS or MPEG-DASH) - """ - def __init__(self): - self.source = None - self.format = None - self.callback_url = None - rm_attributes_if_null(self) + """ + + def __init__(self): + self.source = None + self.format = None + self.callback_url = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Format(object): - """ - :var - :var + """ + :var + :var """ - def __init__(self): - self.output = None - self.file_extension = None - self.destination = None - self.segment_duration = None - self.stream = None - self.logo = None - self.start_time = None - self.duration = None - self.is_watermark = None - self.size = None - self.video_codec = None - self.audio_codec = None - self.aspect_ratio = None - self.quality = None - self.interval = None - self.width = None - self.height = None - self.time = None - self.path = None - self.resize_mod = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + + def __init__(self): + self.output = None + self.file_extension = None + self.destination = None + self.segment_duration = None + self.stream = None + self.logo = None + self.start_time = None + self.duration = None + self.is_watermark = None + self.size = None + self.video_codec = None + self.audio_codec = None + self.aspect_ratio = None + self.quality = None + self.interval = None + self.width = None + self.height = None + self.time = None + self.path = None + self.resize_mod = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Destination(object): - def __init__(self): - self.url = None - self.key = None - self.secret = None - self.permissions = None - self.storage_class = None - rm_attributes_if_null(self) + def __init__(self): + self.url = None + self.key = None + self.secret = None + self.permissions = None + self.storage_class = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Stream(object): - def __init__(self): - self.size = None - self.video_codec = None - self.bitrate = None - self.quality = None - self.rotate = None - self.framerate = None - self.pix_format = None - self.profile = None - self.video_codec_parameters = None - self.keyframe = None - self.segment_duration = None - self.start_time = None - self.duration = None - self.audio_bitrate = None - self.audio_sample_rate = None - self.audio_channels_number = None - self.audio_codec = None - self.downmix_mode = None - self.logo = None - self.aspect_ratio = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.size = None + self.video_codec = None + self.bitrate = None + self.quality = None + self.rotate = None + self.framerate = None + self.pix_format = None + self.profile = None + self.video_codec_parameters = None + self.keyframe = None + self.segment_duration = None + self.start_time = None + self.duration = None + self.audio_bitrate = None + self.audio_sample_rate = None + self.audio_channels_number = None + self.audio_codec = None + self.downmix_mode = None + self.logo = None + self.aspect_ratio = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx264_VideoCodecParameters(object): - def __init__(self): - self.vprofile = None - self.level = None - self.coder = None - self.flags2 = None - self.partitions = None - self.bf = None - self.directpred = None - self.me_method = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.vprofile = None + self.level = None + self.coder = None + self.flags2 = None + self.partitions = None + self.bf = None + self.directpred = None + self.me_method = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx265_VideoCodecParameters(object): - def __init__(self): - pass + def __init__(self): + pass class MyEncoder(JSONEncoder): - def default(self, obj): - return obj.__dict__ + def default(self, obj): + return obj.__dict__ class Query(object): - def __init__(self): - self.params = None - self.error = None - self.message = '' - self.query = None - - def prepare_params(self): - query = dict(query=self.params) - try: - self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') - except Exception as e: - self.error = True - self.message = repr(e) - - def validate_params(self): - if not self.params: - self.error = True - self.message = 'Params is required' - return - if not 'source' in self.params.__dict__: - self.error = True - self.message = 'Params: source is required' - return - if not 'format' in self.params.__dict__: - self.error = True - self.message = 'Params: format is required' - return \ No newline at end of file + def __init__(self): + self.params = None + self.error = None + self.message = '' + self.query = None + + def prepare_params(self): + query = dict(query=self.params) + try: + self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') + except Exception as e: + self.error = True + self.message = repr(e) + + def validate_params(self): + if not self.params: + self.error = True + self.message = 'Params is required' + return + if not 'source' in self.params.__dict__: + self.error = True + self.message = 'Params: source is required' + return + if not 'format' in self.params.__dict__: + self.error = True + self.message = 'Params: format is required' + return diff --git a/qencode/drm/buydrm.py b/qencode/drm/buydrm.py index ad960d1..8ad9fb9 100644 --- a/qencode/drm/buydrm.py +++ b/qencode/drm/buydrm.py @@ -7,19 +7,27 @@ 'xenc': '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#' + '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' + def create_cpix_user_request( - key_ids, media_id, user_private_key_path, user_public_cert_path, - use_playready=False, use_widevine=False, use_fairplay=False, - nsmap=None + key_ids, + media_id, + user_private_key_path, + user_public_cert_path, + use_playready=False, + use_widevine=False, + use_fairplay=False, + nsmap=None, ): - document_public_cert_path = os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + document_public_cert_path = ( + os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + ) """Creates CPIX request XML signed end user Arguments: @@ -47,14 +55,20 @@ def create_cpix_user_request( # Delivery data list delivery_data_list = etree.SubElement(root, '{%s}DeliveryDataList' % nsmap['cpix']) - delivery_data = etree.SubElement(delivery_data_list, '{%s}DeliveryData' % nsmap['cpix']) + delivery_data = etree.SubElement( + delivery_data_list, '{%s}DeliveryData' % nsmap['cpix'] + ) delivery_key = etree.SubElement(delivery_data, '{%s}DeliveryKey' % nsmap['cpix']) # The public certificate of a partner. This certificate's public key will be used # to encrypt Document Key which will later be used to encrypt Contnet Keys. x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds']) x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds']) - x509_cert.text = document_public_cert.replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace('\n', '') + x509_cert.text = ( + document_public_cert.replace('-----BEGIN CERTIFICATE-----', '') + .replace('-----END CERTIFICATE-----', '') + .replace('\n', '') + ) # Content key list content_key_list = etree.SubElement(root, '{%s}ContentKeyList' % nsmap['cpix']) @@ -73,34 +87,42 @@ def create_cpix_user_request( ) if use_playready: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_PLAYREADY + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_PLAYREADY, ) if use_widevine: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_WIDEVINE + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_WIDEVINE, ) if use_fairplay: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_FAIRPLAY + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_FAIRPLAY, ) etree.SubElement( - content_key_usage_list, '{%s}ContentKeyUsageRule' % nsmap['cpix'], - kid=data['kid'], intendedTrackType=data['track_type'] + content_key_usage_list, + '{%s}ContentKeyUsageRule' % nsmap['cpix'], + kid=data['kid'], + intendedTrackType=data['track_type'], ) # Signing document with end user's data end_user_signed_root = XMLSigner( c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315', - signature_algorithm='rsa-sha256', digest_algorithm='sha512' + signature_algorithm='rsa-sha256', + digest_algorithm='sha512', ).sign(root, key=end_user_private_key, cert=end_user_public_cert) x509_sign_cert = end_user_signed_root.xpath( '//ds:X509Certificate', namespaces=nsmap )[1] x509_sign_cert.text = x509_sign_cert.text.replace('\n', '') -# + # return etree.tostring(end_user_signed_root).decode('utf-8') - diff --git a/qencode/exeptions.py b/qencode/exeptions.py index 36b5fdc..fa5b795 100644 --- a/qencode/exeptions.py +++ b/qencode/exeptions.py @@ -1,18 +1,15 @@ class QencodeException(Exception): - def __init__(self, message, *args): - super(QencodeException, self).__init__(message, *args) - self.error = message - self.arg = [i for i in args] + def __init__(self, message, *args): + super(QencodeException, self).__init__(message, *args) + self.error = message + self.arg = [i for i in args] + class QencodeClientException(QencodeException): - def __init__(self, message, *args): - super(QencodeClientException, self).__init__(message, *args) + def __init__(self, message, *args): + super(QencodeClientException, self).__init__(message, *args) class QencodeTaskException(QencodeException): - def __init__(self, message, *args): - super(QencodeTaskException, self).__init__(message, *args) - - - - + def __init__(self, message, *args): + super(QencodeTaskException, self).__init__(message, *args) diff --git a/qencode/httptools.py b/qencode/httptools.py index a276517..30f11ff 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -4,45 +4,49 @@ from urlparse import urljoin import ssl + class Http(object): - def __init__(self, version, url, debug=False): - self.version = version - self.url = url - self._debug = debug + def __init__(self, version, url, debug=False): + self.version = version + self.url = url + self._debug = debug - def _call_server(self, url, post_data): - if not url: - response = dict(error=True, message='AttributeError: Bad URL') - return json.dumps(response) - data = urllib.urlencode(post_data) - request = urllib2.Request(url, data) - context = ssl._create_unverified_context() - try: - res = urllib2.urlopen(request, context=context) - except urllib2.HTTPError as e: - headers = e.headers if self._debug else '' - response = dict(error=True, message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers)) - response = json.dumps(response) - except urllib2.URLError as e: - response = dict(error=True, message='URLError: {0}'.format(e.reason)) - response = json.dumps(response) - else: - response = res.read() - return response + def _call_server(self, url, post_data): + if not url: + response = dict(error=True, message='AttributeError: Bad URL') + return json.dumps(response) + data = urllib.urlencode(post_data) + request = urllib2.Request(url, data) + context = ssl._create_unverified_context() + try: + res = urllib2.urlopen(request, context=context) + except urllib2.HTTPError as e: + headers = e.headers if self._debug else '' + response = dict( + error=True, + message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers), + ) + response = json.dumps(response) + except urllib2.URLError as e: + response = dict(error=True, message='URLError: {0}'.format(e.reason)) + response = json.dumps(response) + else: + response = res.read() + return response - def request(self, api_name, data): - path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) - response = self._call_server(urljoin(self.url, path), data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response + def request(self, api_name, data): + path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) + response = self._call_server(urljoin(self.url, path), data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response - def post(self, url, data): - response = self._call_server(url, data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response \ No newline at end of file + def post(self, url, data): + response = self._call_server(url, data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response diff --git a/qencode/metadata.py b/qencode/metadata.py index d91176d..fe0ca70 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -2,16 +2,19 @@ from qencode import QencodeTaskException import urllib2 -class Metadata(Task): +class Metadata(Task): def get(self, uri): - params = """ + params = ( + """ {"query": { "source": "%s", "format": [ {"output": "metadata", "metadata_version": "4.1.5"} ] } } - """ % uri + """ + % uri + ) self.custom_start(params) while True: status = self.status() @@ -36,5 +39,3 @@ def get(self, uri): data = urllib2.urlopen(url).read() return data - - diff --git a/qencode/task.py b/qencode/task.py index b45f032..194a9cb 100644 --- a/qencode/task.py +++ b/qencode/task.py @@ -6,198 +6,202 @@ class Task(object): - def __init__(self, access_token, connect, debug=False, **kwargs): - self.connect = connect - self.status_url = None - self.main_status_url = '{0}/{1}/status'.format(self.connect.url, self.connect.version) - self.task_token = None - self.upload_url = None - self.access_token = access_token - self._debug = debug - self.message = '' - self.error = None - self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT - self._create_task(1) - - - def start(self, profiles, video_url, **kwargs): - """Creating task and starting encode - - :param profiles: String or List object. Profile uuid - :param transfer_method: String. Transfer method uuid - :param video_url: String. Url of source video - :param payload: String. - :return: None - - """ - if not self.error: - # self._create_task(1) - data = self._prepare_data(profiles, video_url, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode', data) - - def custom_start(self, data, **kwargs): - """Creating task and starting encode - - :param query: JSON object for query param. For examples: https://docs.qencode.com - :param payload: String. - :return: None - - """ - if data is None: - self.error = True - self.message = 'Params is required' - - #if not self.error: - # self._create_task(1) - - if not self.error: - query = self._prepare_query(data) - - if not self.error: - data = self._prepare_data_custom(query, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode2', data) - - def status(self): - return self._status() - - def extend_status(self): - return self._extend_status() - - def progress_changed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def task_completed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def _prepare_query(self, params): - if isinstance(params, CustomTranscodingParams): - query_obj = Query() - query_obj.params = params - query_obj.validate_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - query_obj.prepare_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - return query_obj.query - - if isinstance(params, dict): - query = rm_key_if_null(params) - return json.dumps(query) - - if isinstance(params, basestring): - if is_json(params): - query = rm_key_if_null(params) - return query - else: - self.error = True - try: - self.message = "JSON is not well formatted: {0} Is not defined".format(params) - except Exception as e: - pass - finally: - self.message = "JSON is not well formatted" - - def _prepare_data(self, profiles, video_url, **kwargs): - data = dict( - task_token=self.task_token, - profiles=', '.join(profiles) if type(profiles).__name__ == 'list' else profiles - ) - if isinstance(video_url, list): - try: - data.update(stitch=json.dumps(video_url)) - except Exception: - data.update(stitch=video_url) - else: - data.update(uri=video_url) - if kwargs: - data.update(kwargs) - return data - - def _prepare_data_custom(self, query_json, **kwargs): - data = dict( - task_token=self.task_token, - query=query_json - ) - if kwargs: - data.update(kwargs) - return data - - def _create_task(self, count): - res = self.connect.request('create_task', dict(token=self.access_token)) - if not res['error']: - self.task_token = res.get('task_token') - self.upload_url = res.get('upload_url') - else: - self.error = res['error'] - self.message = res.get('message') - - if self.error and self.error == 8: - if count < REPEAT: - time.sleep(SLEEP_ERROR) - self._create_task(count + 1) - - - def _start_encode(self, api_name, data): - res = self.connect.request(api_name, data) - if not res['error'] and res.get('status_url'): - self.status_url = res['status_url'] - else: - self.status_url = self.main_status_url - self.error = res.get('error') - self.message = res.get('message') - - def _status(self): - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - status = None - - if response['error'] == ERROR_BAD_TOKENS: - raise ValueError('Bad token: ' + str(self.task_token)) - - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if not status and self.status_url != self.main_status_url: - self.status_url = self.main_status_url - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if status and 'status_url' in status: - self.status_url = status['status_url'] - - return status - - def _extend_status(self): - response = self.connect.post(self.main_status_url, dict(task_tokens=self.task_token)) - status = None - - if response['error'] == ERROR_BAD_TOKENS: - raise ValueError('Bad token: ' + str(self.task_token)) - - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - return status - + def __init__(self, access_token, connect, debug=False, **kwargs): + self.connect = connect + self.status_url = None + self.main_status_url = '{0}/{1}/status'.format( + self.connect.url, self.connect.version + ) + self.task_token = None + self.upload_url = None + self.access_token = access_token + self._debug = debug + self.message = '' + self.error = None + self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT + self._create_task(1) + + def start(self, profiles, video_url, **kwargs): + """Creating task and starting encode + + :param profiles: String or List object. Profile uuid + :param transfer_method: String. Transfer method uuid + :param video_url: String. Url of source video + :param payload: String. + :return: None + + """ + if not self.error: + # self._create_task(1) + data = self._prepare_data(profiles, video_url, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode', data) + + def custom_start(self, data, **kwargs): + """Creating task and starting encode + + :param query: JSON object for query param. For examples: https://docs.qencode.com + :param payload: String. + :return: None + + """ + if data is None: + self.error = True + self.message = 'Params is required' + + # if not self.error: + # self._create_task(1) + + if not self.error: + query = self._prepare_query(data) + + if not self.error: + data = self._prepare_data_custom(query, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode2', data) + + def status(self): + return self._status() + + def extend_status(self): + return self._extend_status() + + def progress_changed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + break + time.sleep(SLEEP_REGULAR) + + def task_completed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + return callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + break + time.sleep(SLEEP_REGULAR) + + def _prepare_query(self, params): + if isinstance(params, CustomTranscodingParams): + query_obj = Query() + query_obj.params = params + query_obj.validate_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + query_obj.prepare_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + return query_obj.query + + if isinstance(params, dict): + query = rm_key_if_null(params) + return json.dumps(query) + + if isinstance(params, basestring): + if is_json(params): + query = rm_key_if_null(params) + return query + else: + self.error = True + try: + self.message = ( + "JSON is not well formatted: {0} Is not defined".format(params) + ) + except Exception as e: + pass + finally: + self.message = "JSON is not well formatted" + + def _prepare_data(self, profiles, video_url, **kwargs): + data = dict( + task_token=self.task_token, + profiles=', '.join(profiles) + if type(profiles).__name__ == 'list' + else profiles, + ) + if isinstance(video_url, list): + try: + data.update(stitch=json.dumps(video_url)) + except Exception: + data.update(stitch=video_url) + else: + data.update(uri=video_url) + if kwargs: + data.update(kwargs) + return data + + def _prepare_data_custom(self, query_json, **kwargs): + data = dict(task_token=self.task_token, query=query_json) + if kwargs: + data.update(kwargs) + return data + + def _create_task(self, count): + res = self.connect.request('create_task', dict(token=self.access_token)) + if not res['error']: + self.task_token = res.get('task_token') + self.upload_url = res.get('upload_url') + else: + self.error = res['error'] + self.message = res.get('message') + + if self.error and self.error == 8: + if count < REPEAT: + time.sleep(SLEEP_ERROR) + self._create_task(count + 1) + + def _start_encode(self, api_name, data): + res = self.connect.request(api_name, data) + if not res['error'] and res.get('status_url'): + self.status_url = res['status_url'] + else: + self.status_url = self.main_status_url + self.error = res.get('error') + self.message = res.get('message') + + def _status(self): + response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) + status = None + + if response['error'] == ERROR_BAD_TOKENS: + raise ValueError('Bad token: ' + str(self.task_token)) + + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if not status and self.status_url != self.main_status_url: + self.status_url = self.main_status_url + response = self.connect.post( + self.status_url, dict(task_tokens=self.task_token) + ) + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if status and 'status_url' in status: + self.status_url = status['status_url'] + + return status + + def _extend_status(self): + response = self.connect.post( + self.main_status_url, dict(task_tokens=self.task_token) + ) + status = None + + if response['error'] == ERROR_BAD_TOKENS: + raise ValueError('Bad token: ' + str(self.task_token)) + + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + return status diff --git a/qencode/tools.py b/qencode/tools.py index d45052e..3f0a7af 100644 --- a/qencode/tools.py +++ b/qencode/tools.py @@ -8,7 +8,10 @@ import xml.etree.cElementTree as et import uuid -def generate_aws_signed_url(region, bucket, object_key, access_key, secret_key, expiration, endpoint=None): + +def generate_aws_signed_url( + region, bucket, object_key, access_key, secret_key, expiration, endpoint=None +): # request elements http_method = 'GET' @@ -34,11 +37,21 @@ def createSignatureKey(key, datestamp, region, service): timestamp = time.strftime('%Y%m%dT%H%M%SZ') datestamp = time.strftime('%Y%m%d') - standardized_querystring = ('X-Amz-Algorithm=AWS4-HMAC-SHA256' + - '&X-Amz-Credential=' + access_key + '/' + datestamp + '/' + region + '/s3/aws4_request' + - '&X-Amz-Date=' + timestamp + - '&X-Amz-Expires=' + str(expiration) + - '&X-Amz-SignedHeaders=host') + standardized_querystring = ( + 'X-Amz-Algorithm=AWS4-HMAC-SHA256' + + '&X-Amz-Credential=' + + access_key + + '/' + + datestamp + + '/' + + region + + '/s3/aws4_request' + + '&X-Amz-Date=' + + timestamp + + '&X-Amz-Expires=' + + str(expiration) + + '&X-Amz-SignedHeaders=host' + ) standardized_querystring_url_encoded = quote(standardized_querystring, safe='&=') standardized_resource = '/' + object_key @@ -48,35 +61,51 @@ def createSignatureKey(key, datestamp, region, service): standardized_headers = 'host:' + host signed_headers = 'host' - standardized_request = (http_method + '\n' + - standardized_resource + '\n' + - standardized_querystring_url_encoded + '\n' + - standardized_headers + '\n' + - '\n' + - signed_headers + '\n' + - payload_hash).encode('utf-8') + standardized_request = ( + http_method + + '\n' + + standardized_resource + + '\n' + + standardized_querystring_url_encoded + + '\n' + + standardized_headers + + '\n' + + '\n' + + signed_headers + + '\n' + + payload_hash + ).encode('utf-8') # assemble string-to-sign hashing_algorithm = 'AWS4-HMAC-SHA256' credential_scope = datestamp + '/' + region + '/' + 's3' + '/' + 'aws4_request' - sts = (hashing_algorithm + '\n' + - timestamp + '\n' + - credential_scope + '\n' + - hashlib.sha256(standardized_request).hexdigest()) + sts = ( + hashing_algorithm + + '\n' + + timestamp + + '\n' + + credential_scope + + '\n' + + hashlib.sha256(standardized_request).hexdigest() + ) # generate the signature signature_key = createSignatureKey(secret_key, datestamp, region, 's3') - signature = hmac.new(signature_key, - (sts).encode('utf-8'), - hashlib.sha256).hexdigest() + signature = hmac.new( + signature_key, (sts).encode('utf-8'), hashlib.sha256 + ).hexdigest() # create and send the request # the 'requests' package autmatically adds the required 'host' header - request_url = (endpoint + '/' + - object_key + '?' + - standardized_querystring_url_encoded + - '&X-Amz-Signature=' + - signature) + request_url = ( + endpoint + + '/' + + object_key + + '?' + + standardized_querystring_url_encoded + + '&X-Amz-Signature=' + + signature + ) def hex_hash(key, msg): return hmac.new(b'key', msg.encode('utf-8'), hashlib.sha256).hexdigest() @@ -112,17 +141,17 @@ def fps_drm(username, password, uid=None): def cenc_drm(username, password, uid=None): - asset_id = uid if uid else uuid.uuid4() - url = CENC_DRM_KEYGENERATOR_URI_TEMPLATE % (asset_id, username, password) - response = requests.post(url, {}) - tree = et.ElementTree(et.fromstring(response.content)) - root = tree.getroot() - key_id = root[0][0].get('kid') - key = root[0][0][0][0][0].text - pssh = root[1][0][0].text - key_id_hex = key_id.replace('-', '') - key_hex = key.decode('base64').encode('hex') - key_url = DRM_KEY_URL_TEMPLATE % key_id - payload = dict(AssetID=asset_id) - data = dict(key=key_hex, key_id=key_id_hex, pssh=pssh, key_url=key_url) - return data, payload + asset_id = uid if uid else uuid.uuid4() + url = CENC_DRM_KEYGENERATOR_URI_TEMPLATE % (asset_id, username, password) + response = requests.post(url, {}) + tree = et.ElementTree(et.fromstring(response.content)) + root = tree.getroot() + key_id = root[0][0].get('kid') + key = root[0][0][0][0][0].text + pssh = root[1][0][0].text + key_id_hex = key_id.replace('-', '') + key_hex = key.decode('base64').encode('hex') + key_url = DRM_KEY_URL_TEMPLATE % key_id + payload = dict(AssetID=asset_id) + data = dict(key=key_hex, key_id=key_id_hex, pssh=pssh, key_url=key_url) + return data, payload diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py index 23ca32d..748094a 100644 --- a/qencode/tus_uploader.py +++ b/qencode/tus_uploader.py @@ -2,31 +2,34 @@ from tusclient import client from utils import get_tus_from_url + class UploadStatus(object): - def __init__(self, error = None, url= None, status= None): - self.url = url - self.error = error - self.status = status + def __init__(self, error=None, url=None, status=None): + self.url = url + self.error = error + self.status = status def upload(file_path=None, url=None, chunk_size=None, log_func=None): """ - Returns upload status and url using tus protocol + Returns upload status and url using tus protocol - :fileUrl: + :fileUrl: - Url address where to upload the file + Url address where to upload the file - :Args: - see tusclient.uploader.Uploader for required and optional arguments. + :Args: + see tusclient.uploader.Uploader for required and optional arguments. """ try: my_client = client.TusClient(url=url) - uploader = my_client.uploader(file_path=file_path, chunk_size=chunk_size, log_func=log_func) + uploader = my_client.uploader( + file_path=file_path, chunk_size=chunk_size, log_func=log_func + ) uploader.upload() url_storage = uploader.url tus_url = get_tus_from_url(url_storage) return UploadStatus(url=tus_url, status='Ok', error='') except: - print('Error uploading file to ' + url) - raise \ No newline at end of file + print('Error uploading file to ' + url) + raise diff --git a/qencode/utils.py b/qencode/utils.py index 41db2f8..58cdcf7 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -2,80 +2,95 @@ import logging import json + def is_number(s): - try: - float(s) - return True - except: - return False + try: + float(s) + return True + except: + return False + def get_percent(p): - if is_number(p): - return round(p) - return 0 + if is_number(p): + return round(p) + return 0 + def is_json(value): - try: - json.loads(value) - except ValueError: - return False - return True + try: + json.loads(value) + except ValueError: + return False + return True + def rm_attributes_if_null(class_obj): - for key, val in class_obj.__dict__.items(): - if not val: - class_obj.__dict__.pop(key) + for key, val in class_obj.__dict__.items(): + if not val: + class_obj.__dict__.pop(key) + def rm_key_if_null(obj): - if isinstance(obj, dict): - return _rm_key(obj) - elif isinstance(obj, basestring): - res = _rm_key(json.loads(obj)) - return json.dumps(res) + if isinstance(obj, dict): + return _rm_key(obj) + elif isinstance(obj, basestring): + res = _rm_key(json.loads(obj)) + return json.dumps(res) + def _rm_key(_dict): - for key, val in _dict.items(): - if not val: - _dict.pop(key) - return _dict + for key, val in _dict.items(): + if not val: + _dict.pop(key) + return _dict + def progress_bar(self, custom_message=None): - message = custom_message if custom_message else '' - while 1: - barLength, status = 20, "" - progress = float(self.percent) / 100.0 - if progress >= 1.: - progress, status = 1, "\r\n" - block = int(round(barLength * progress)) - text = "\r{} [{}] {:.0f}% {}".format(message, - "#" * block + "-" * (barLength - block), round(progress * 100, 0), status) - sys.stdout.write(text) - sys.stdout.flush() - if self.task_completed: - break + message = custom_message if custom_message else '' + while 1: + barLength, status = 20, "" + progress = float(self.percent) / 100.0 + if progress >= 1.0: + progress, status = 1, "\r\n" + block = int(round(barLength * progress)) + text = "\r{} [{}] {:.0f}% {}".format( + message, + "#" * block + "-" * (barLength - block), + round(progress * 100, 0), + status, + ) + sys.stdout.write(text) + sys.stdout.flush() + if self.task_completed: + break + def log(self, path=None, name=None, log_format=None): - format = '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format - name = name if name else '{0}.log'.format(self.task.token) - path = path if path else '' - log_name = '{0}{1}'.format(path, name) - logging.basicConfig(filename=log_name, format=format()) - logging.getLogger().setLevel(logging.INFO) - log = logging.getLogger() - while 1: - log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) - if self.task_completed: - break + format = ( + '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format + ) + name = name if name else '{0}.log'.format(self.task.token) + path = path if path else '' + log_name = '{0}{1}'.format(path, name) + logging.basicConfig(filename=log_name, format=format()) + logging.getLogger().setLevel(logging.INFO) + log = logging.getLogger() + while 1: + log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) + if self.task_completed: + break + def get_tus_from_url(url=''): - try: - if url.find('tus:') == 0: - return url - else: - x = url.split('/')[-1] - if x == url: - return url + try: + if url.find('tus:') == 0: + return url else: - return 'tus:' + x - except: - return url \ No newline at end of file + x = url.split('/')[-1] + if x == url: + return url + else: + return 'tus:' + x + except: + return url diff --git a/setup.py b/setup.py index 028d1bc..2d00414 100644 --- a/setup.py +++ b/setup.py @@ -25,11 +25,10 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', - 'Programming Language :: Python :: 2.7' - + 'Programming Language :: Python :: 2.7', ], keywords='qencode, qencode.com, cloud.qencode.com', packages=['qencode', 'qencode.drm'], package_data={'qencode.drm': ['keys/buydrm_qencode_public_cert.pem']}, - include_package_data=True + include_package_data=True, ) diff --git a/tests/test_client.py b/tests/test_client.py index 3d92c88..599b5b6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,15 +2,15 @@ class TestQencodeApiClient(unittest.TestCase): - def setUp(self): - pass + pass def tearDown(self): - pass + pass def test_create_task(self): pass + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 38251e33302ab309e81d69727c32c6cab0609f50 Mon Sep 17 00:00:00 2001 From: Root BA Date: Mon, 25 Apr 2022 12:23:49 +0000 Subject: [PATCH 15/21] Fix soutce video file --- sample-code/start_custom_hls.py | 2 +- sample-code/start_custom_mp4.py | 2 +- sample-code/start_custom_with_dict.py | 2 +- sample-code/start_custom_with_json.py | 2 +- sample-code/start_with_callback.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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", From c32b9164bdf9ac5c6dd4d5667c0aa2c2c62ae045 Mon Sep 17 00:00:00 2001 From: Qencode Date: Tue, 6 Sep 2022 17:09:09 +0300 Subject: [PATCH 16/21] support for /v1/tasks method --- qencode/client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 From 9233d7f21c96346c7a22d00166873ee9f7858df2 Mon Sep 17 00:00:00 2001 From: Root BA Date: Wed, 26 Nov 2025 13:57:21 +0200 Subject: [PATCH 17/21] add support BuyDRM.v4 --- qencode/drm/buydrm_v4.py | 75 ++++++++++++++++++++ qencode/drm/const.py | 29 ++++++++ qencode/drm/keys/qencode-public_cert.pem | 31 +++++++++ sample-code/drm/buydrm/buydrm_v4.py | 87 ++++++++++++++++++++++++ sample-code/drm/buydrm/query.json | 18 +++++ 5 files changed, 240 insertions(+) create mode 100644 qencode/drm/buydrm_v4.py create mode 100644 qencode/drm/const.py create mode 100644 qencode/drm/keys/qencode-public_cert.pem create mode 100644 sample-code/drm/buydrm/buydrm_v4.py create mode 100644 sample-code/drm/buydrm/query.json diff --git a/qencode/drm/buydrm_v4.py b/qencode/drm/buydrm_v4.py new file mode 100644 index 0000000..d14f227 --- /dev/null +++ b/qencode/drm/buydrm_v4.py @@ -0,0 +1,75 @@ +# 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, public_cert, delivery_public_cert=None, + use_playready=False, use_widevine=False, use_fairplay=False, + nsmap=const.NSMAP + ): + if delivery_public_cert 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_v4.py b/sample-code/drm/buydrm/buydrm_v4.py new file mode 100644 index 0000000..bb71def --- /dev/null +++ b/sample-code/drm/buydrm/buydrm_v4.py @@ -0,0 +1,87 @@ +import base64 +import json +import uuid +import time +import qencode +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, +# for example create dir keys/ and put keys into +enduser_pvk, enduser_pub = [ + open('keys/' + p, 'r').read() for p in ( + 'user-private_key.pem', + 'user-public_cert.pem', +# 'qencode-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' } +] +# need for BuyDRM +media_id = 'my asset' +content_id = 'group21' +common_encryption = 'cenc' +# unified with the new BuyDRM API params +drm_list = { + 'PR': True, # use_playready + 'WV': True, # use_widevine + 'FP': False # use_fairplay +} +# for create log files: state-.json, xml +debug = True + +def start_encode(): + cpix_request = create_cpix_user_request( + key_ids, media_id, + content_id, common_encryption, + enduser_pvk, enduser_pub, #delivery_public_cert=None, # if None - get from SDK, else - set public Qenocde certificate + 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) + + task = client.create_task() + + if task.error: + raise QencodeTaskException(task.message) + + 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) + task_token = task.task_token + print('Start encode. Task: %s' % task_token) + + while True: + status = task.status() + 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 debug: + open('job-cpix_request-%s.xml' % task_token, 'w').write(cpix_request) + open('job-query-%s.json' % task_token, 'w').write(query) + open('job-result-%s.json' % task_token, 'w').write(json.dumps(status, indent=2, sort_keys=True)) + +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..f4edb15 --- /dev/null +++ b/sample-code/drm/buydrm/query.json @@ -0,0 +1,18 @@ +{ + "query": { + "encoder_version": "2", + "format": [ + { + "output": "advanced_dash", + "stream": [ +{ "video_codec": "libx264", "height": 360, "audio_bitrate": 128, "keyframe": 25, "bitrate": 950 }, +{ "video_codec": "libx264", "height": 720, "audio_bitrate": 128, "keyframe": 25, "bitrate": 2000 } + ], + "buydrm_drm": { + "request": "{cpix_request}" + } + } + ], + "source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4" + } +} From 8f9b400d897a73233e4f13bced51338792e5cdd6 Mon Sep 17 00:00:00 2001 From: Root BA Date: Wed, 26 Nov 2025 15:51:41 +0200 Subject: [PATCH 18/21] change buydrm_v4.py --- sample-code/drm/buydrm/buydrm_v4.py | 1 - sample-code/drm/buydrm/query.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sample-code/drm/buydrm/buydrm_v4.py b/sample-code/drm/buydrm/buydrm_v4.py index bb71def..b5d3014 100644 --- a/sample-code/drm/buydrm/buydrm_v4.py +++ b/sample-code/drm/buydrm/buydrm_v4.py @@ -23,7 +23,6 @@ # correspond to stream resolution in query.json key_ids = [ { 'kid': str(uuid.uuid4()), 'track_type': 'SD' }, - { 'kid': str(uuid.uuid4()), 'track_type': 'HD' } ] # need for BuyDRM media_id = 'my asset' diff --git a/sample-code/drm/buydrm/query.json b/sample-code/drm/buydrm/query.json index f4edb15..944e057 100644 --- a/sample-code/drm/buydrm/query.json +++ b/sample-code/drm/buydrm/query.json @@ -5,8 +5,7 @@ { "output": "advanced_dash", "stream": [ -{ "video_codec": "libx264", "height": 360, "audio_bitrate": 128, "keyframe": 25, "bitrate": 950 }, -{ "video_codec": "libx264", "height": 720, "audio_bitrate": 128, "keyframe": 25, "bitrate": 2000 } +{ "video_codec": "libx264", "height": 360, "audio_bitrate": 128, "keyframe": 25, "bitrate": 950 } ], "buydrm_drm": { "request": "{cpix_request}" From 605262a7480e5eb08dbc9f42add08c952dd1947e Mon Sep 17 00:00:00 2001 From: Root BA Date: Mon, 1 Dec 2025 17:31:54 +0200 Subject: [PATCH 19/21] change create_cpix_user_request from cert to cert_path --- qencode/drm/buydrm_v4.py | 6 ++++-- sample-code/drm/buydrm/buydrm_v4.py | 11 +++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/qencode/drm/buydrm_v4.py b/qencode/drm/buydrm_v4.py index d14f227..1b376d9 100644 --- a/qencode/drm/buydrm_v4.py +++ b/qencode/drm/buydrm_v4.py @@ -8,11 +8,13 @@ def create_cpix_user_request( key_ids, media_id, content_id, commonEncryptionScheme, - private_key, public_cert, delivery_public_cert=None, + private_key_path, public_cert_path, delivery_public_cert_path=None, use_playready=False, use_widevine=False, use_fairplay=False, nsmap=const.NSMAP ): - if delivery_public_cert is None: + 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() diff --git a/sample-code/drm/buydrm/buydrm_v4.py b/sample-code/drm/buydrm/buydrm_v4.py index b5d3014..d895e46 100644 --- a/sample-code/drm/buydrm/buydrm_v4.py +++ b/sample-code/drm/buydrm/buydrm_v4.py @@ -11,13 +11,8 @@ API_KEY = 'your-api-qencode-key' # specify path to your BuyDRM certificate files, # for example create dir keys/ and put keys into -enduser_pvk, enduser_pub = [ - open('keys/' + p, 'r').read() for p in ( - 'user-private_key.pem', - 'user-public_cert.pem', -# 'qencode-public_cert.pem', - ) -] +enduser_pvk_path = 'keys/user-private_key.pem' +enduser_pub_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 @@ -41,7 +36,7 @@ def start_encode(): cpix_request = create_cpix_user_request( key_ids, media_id, content_id, common_encryption, - enduser_pvk, enduser_pub, #delivery_public_cert=None, # if None - get from SDK, else - set public Qenocde certificate + enduser_pvk_path, enduser_pub_path, #delivery_public_cert_path=None, # if None - get from SDK, else - set public Qenocde certificate use_playready=drm_list['PR'], use_widevine=drm_list['WV'], use_fairplay=drm_list['FP'] ) From b16bc0a9dbf095d8cecfdc886eb5a340cb33ab37 Mon Sep 17 00:00:00 2001 From: Root BA Date: Mon, 1 Dec 2025 17:34:43 +0200 Subject: [PATCH 20/21] rename params in demo --- sample-code/drm/buydrm/buydrm_v4.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample-code/drm/buydrm/buydrm_v4.py b/sample-code/drm/buydrm/buydrm_v4.py index d895e46..e1f3874 100644 --- a/sample-code/drm/buydrm/buydrm_v4.py +++ b/sample-code/drm/buydrm/buydrm_v4.py @@ -11,8 +11,8 @@ API_KEY = 'your-api-qencode-key' # specify path to your BuyDRM certificate files, # for example create dir keys/ and put keys into -enduser_pvk_path = 'keys/user-private_key.pem' -enduser_pub_path = 'keys/user-public_cert.pem' +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 @@ -36,7 +36,7 @@ def start_encode(): cpix_request = create_cpix_user_request( key_ids, media_id, content_id, common_encryption, - enduser_pvk_path, enduser_pub_path, #delivery_public_cert_path=None, # if None - get from SDK, else - set public Qenocde certificate + USER_PVT_KEY_PATH, USER_PUB_CERT_PATH, #delivery_public_cert_path=None, # if None - get from SDK, else - set public Qenocde certificate use_playready=drm_list['PR'], use_widevine=drm_list['WV'], use_fairplay=drm_list['FP'] ) From 5aea7994983a0e07c1ade2ed73338c924278df7b Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 2 Dec 2025 22:25:04 +0200 Subject: [PATCH 21/21] Updates for BuyDRM v4 API --- sample-code/drm/buydrm/buydrm_v4.py | 81 ------------------- .../drm/buydrm/buydrm_widevine_playready.py | 80 +++++++++--------- sample-code/drm/buydrm/query.json | 12 ++- 3 files changed, 45 insertions(+), 128 deletions(-) delete mode 100644 sample-code/drm/buydrm/buydrm_v4.py diff --git a/sample-code/drm/buydrm/buydrm_v4.py b/sample-code/drm/buydrm/buydrm_v4.py deleted file mode 100644 index e1f3874..0000000 --- a/sample-code/drm/buydrm/buydrm_v4.py +++ /dev/null @@ -1,81 +0,0 @@ -import base64 -import json -import uuid -import time -import qencode -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, -# 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' }, -] -# need for BuyDRM -media_id = 'my asset' -content_id = 'group21' -common_encryption = 'cenc' -# unified with the new BuyDRM API params -drm_list = { - 'PR': True, # use_playready - 'WV': True, # use_widevine - 'FP': False # use_fairplay -} -# for create log files: state-.json, xml -debug = True - -def start_encode(): - cpix_request = create_cpix_user_request( - key_ids, media_id, - content_id, common_encryption, - USER_PVT_KEY_PATH, USER_PUB_CERT_PATH, #delivery_public_cert_path=None, # if None - get from SDK, else - set public Qenocde certificate - 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) - - task = client.create_task() - - if task.error: - raise QencodeTaskException(task.message) - - 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) - task_token = task.task_token - print('Start encode. Task: %s' % task_token) - - while True: - status = task.status() - 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 debug: - open('job-cpix_request-%s.xml' % task_token, 'w').write(cpix_request) - open('job-query-%s.json' % task_token, 'w').write(query) - open('job-result-%s.json' % task_token, 'w').write(json.dumps(status, indent=2, sort_keys=True)) - -if __name__ == '__main__': - start_encode() 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 index 944e057..476879a 100644 --- a/sample-code/drm/buydrm/query.json +++ b/sample-code/drm/buydrm/query.json @@ -1,11 +1,17 @@ { "query": { - "encoder_version": "2", + "encoder_version": "2", "format": [ { "output": "advanced_dash", "stream": [ -{ "video_codec": "libx264", "height": 360, "audio_bitrate": 128, "keyframe": 25, "bitrate": 950 } + { + "video_codec": "libx264", + "height": 360, + "audio_bitrate": 128, + "keyframe": 25, + "bitrate": 950 + } ], "buydrm_drm": { "request": "{cpix_request}" @@ -14,4 +20,4 @@ ], "source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4" } -} +} \ No newline at end of file