devine-dl / pywidevine Goto Github PK
View Code? Open in Web Editor NEWPython implementation of Google's Widevine DRM CDM (Content Decryption Module)
License: GNU General Public License v3.0
Python implementation of Google's Widevine DRM CDM (Content Decryption Module)
License: GNU General Public License v3.0
I only found PlayReady's mspr:pro
in the pmd
file, how can I convert it to cenc:pssh
?
<?xml version='1.0' ?>
<MPD mediaPresentationDuration='PT12M9.033S' minBufferTime='PT2.0S' profiles='urn:mpeg:dash:profile:isoff-on-demand:2011' type='static' xmlns='urn:mpeg:dash:schema:mpd:2011' xmlns:cenc='urn:mpeg:cenc:2013' xmlns:mspr='urn:microsoft:playready'>
<Period>
<AdaptationSet maxHeight='1080' maxWidth='1920' minHeight='1080' minWidth='1920' mimeType='video/mp4' segmentAlignment='true' startWithSAP='1'>
<Representation bandwidth='4848906' codecs='avc1.640028' frameRate='30' height='1080' id='video-avc1' scanType='progressive' width='1920'>
<ContentProtection cenc:default_KID='c2bc9cc9-1376-400f-8bde-732b9cf519ba' schemeIdUri='urn:mpeg:dash:mp4protection:2011' value='cenc'/>
<ContentProtection schemeIdUri='urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'><mspr:pro>pAIAAAEAAQCaAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AeQBaAHkAOAB3AG4AWQBUAEQAMABDAEwAMwBuAE0AcgBuAFAAVQBaAHUAZwA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBKAHMAWgBBAEEARgBRAFkANgBsAGMAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcwB0AHIAZQBhAG0AaQBuAGcAMQAuAHgAYwByAGUAYQBtAC4AbgBlAHQAOgA4ADAAOAAwAC8AcABsAGEAeQByAGUAYQBkAHkALwBSAGkAZwBoAHQAcwBNAGEAbgBhAGcAZQByAC4AYQBzAG0AeAA8AC8ATABBAF8AVQBSAEwAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</mspr:pro></ContentProtection>
<ContentProtection schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED'/>
<BaseURL>https://xxxx/xxxxxx/xxxxxx.rb?r=1DyFJbi5cVYEynbTt9w7gzc5TGPmiCjyYcfgym9B5cGIg33LfBpuMrsK0A3p7L7_xWhui4ruzYGMtWvip7T29uOIJUrBEItq09sWjkZXalbYrI4UTLNocnr6Ji1Lw_KPAUfhy-tMmSG4HFcNdRD8FwK3e0PJGDiODg6F8kha8Aj_TXRVaiyh_KKBC2u5kHC0</BaseURL>
<SegmentBase indexRange='1623-6034'>
<Initialization range='0-1622'/>
</SegmentBase>
</Representation></AdaptationSet>
</Period></MPD>
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from base64 import b64encode
import base64
import requests
import json
url = "https://cox-mds.az.cox.comcast.net/license"
#requestc = b64encode(challenge)
#response = requests.request("POST", url, headers=headers, data=payload)
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsSJDk5NjllYWJjLWE3ZmUtMDk2Yi1jZjQzLWZmNzYwNGUwZjljNSITODI5MTA3NDQwMzczMDYyNDE2Mw==")
device = Device.load("device.wvd")
cdm = Cdm.from_device(device)
session_id = cdm.open()
challenge = cdm.get_license_challenge(session_id, pssh)
print(b64encode(challenge))
payload = {"keySystem":"widevine",
"licenseRequest": b64encode(challenge),
"contentMetadata":"","mediaUsage":"stream","accessToken":""}
headers = {
"Accept": "application/vnd.xcal.mds.licenseResponse+json; version=1",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "keep-alive",
"Content-Type": "application/vnd.xcal.mds.licenseRequest+json; version=1",
"Origin": "https://watchtv.cox.com",
"Referer": "https://watchtv.cox.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"X-MoneyTrace": ""
}
licence = requests.post(url, headers=headers, data=payload)
#print(license.text)
licence.raise_for_status()
l = response.json()['license']
print(l)
cdm.parse_license(session_id, l)
for key in cdm.get_keys(session_id):
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
cdm.close(session_id)
this code is giving 400 error if you can correct it
I want to contact a license url which requires a client token before issuing a license.
It seems the overall process is very similar to what is described here: https://pallycon.com/docs/en/multidrm/license/license-token/
When analyzing the data flow between the client and the streaming site I can nowhere see this token request-response.
Is this something which is done by the CDM when initializing?
As far as I understand the file "license_protocol.proto" contains definition for those security policy fields like if HDCP is required etc.
How can I a) decode a token I see in the browsers devtools to see what's in there and b) generate my own?
Hi @rlaphoenix ,
Sorry for bothering again, Is the VMP a required parameter ?
Can't do the wvd generation if we didn't pass the VMP arguments.
Is it related with this required=True code ? (https://github.com/rlaphoenix/pywidevine/blob/e1951d20d0e03bcb5698fa8c4f72a5d880617caf/pywidevine/main.py#L168)
Thank you
I use the latest version of pywidevine: '1.5.3'
The license request requires the server certificate so I run the code as following and both successful return the license as expected compared to the requested with Web-browser on Desktop or on Android App.
session_id = cdm.open()
#First license request to get cert
response = requests.post(lic_url, headers=headers, data=cdm.service_certificate_challenge)
print("First lic content request to get cert", response.content)
service_cert = response.json()['license']
provider_id = cdm.set_service_certificate(session_id, service_cert)
cdm.set_service_certificate(session_id, service_cert)
challenge = cdm.get_license_challenge(session_id, PSSH(pssh_i), privacy_mode=True)
#second request to get actual license to parse license
licence_res = requests.post(lic_url, headers=headers, data=challenge)
print("The lic to parse:", licence_res.json()['license'])
cdm.parse_license(session_id, licence_res.json()['license'])
Here is the result:
First lic content request to get cert b'{"ec":0,"license":"CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEEQa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKdO7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ="}'
The lic to parse: U01XVgAAA1MAAACLAiUQCQQRCAMMDwcCCwYFDhASCrWq5aAv9o6CC2z1BG/VsxQAAABgzrgfFeTCUq+nl+rBaFqggUAD8BxGtQm0XRVr6rd6oIlZEJBf3VFBqxPrrcPZTmXzUAatwns7fNMGKSGBNgWl+0InqA4R0EGSHa3ME68M4kHJk6VQ1xiDZZxkpZ4LgALFJwfyqzHnL+nf5ugAA1BeDavncl0a3MwoDX4ILrkRHDuyURjOqmngxNaSNV4GrJrCjbQ8QR/ME0LNMGspeq6UT5nYwA0EUmGuaq735UqZzJS0goVcj0ATXOD3td/jaUFmI8ntIR93vzhfeoI6p5DszxffHJl5kTiU/XsGtWrb0becPYzT0ZOgsj3VPYI7i+mxPXBC/BsTTElQA1S/aZG0GeVuv8yfWMIYi200Wxw6FybdZ2+Z8wv8/j4viuHkrec5HA0tCFUlNMHWWUWiiyi4dET1NutCBzmZeaoI8VjrPqOMYow0m9Kft33tBzeVOC2jEZRlF2nEoi1rdZe7sJqRO7fNLgJpos4wd4fioCMrrMJIudvudOkhsQucXY+lPj2FqMqGZHuYfVOs1OH7jNdpu728BXefUU09rSrltbMCqr4hjZ8/oWVlg/3X40KwsE3sccjMdeylw1Vc3MGxpxaH7eDo3Ln7wNiP94TTIlAC+MtI5NDZWmDkHSG4z8tSvgHgK1ATdrQzPt679iF7V7aDPUabxfo9w0j0C7ZWzONm83Y+y74sg1vg8MpWbjAT+OwPFanhi7xoh1uk2Bc7yvYNik+heDJc26DNwkOnuWRoy8iWJYnqxDjhwESXXror+GvcBmWlSn5/13wn7gXKyI0hAViImBHXwoFE3XntEpyt4NoKGoJOR50s6lH5Y2+Vm7AyQZZKJXkoplP7Zi1QlvjpiHnx1LV64P5n5vERlXw3FY0ya2vtKQV2mj4fGDZurJr/NZQpMnnMD/hOjncFJU+CKFYMRJo8cys88S5iiCSEW3FBIc3bEYo42a3uvPgIql3iLNqb22yXWDGnBWlctqoHbNWD9XtCNsdWZtM8EUc9ATzUUI9VxsT1xQAMNvvgNqA+LQJbMNv7t5Jdujg7AkV9rT9SBUROrmDbpp1anqrQP7o=
Here is the result of parsing the license:
File /usr/local/lib/python3.11/site-packages/pywidevine/cdm.py:392, in Cdm.parse_license(self, session_id, license_message)
390 raise DecodeError(license_message)
391 except DecodeError as e:
--> 392 raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
393 license_message = signed_message
395 if not isinstance(license_message, SignedMessage):
InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message
Could you please check this case and suggest me how to solve this problem? Thank you!
First of all thanks for this awesome project and your work.
I am working with some mpd files which only contains a default_KID.
A PSSH for Widevine/Playready is not given.
<ContentProtection
schemeIdUri="urn:mpeg:dash:mp4protection:2011"
value="cenc"
cenc:default_KID="79F8D3B2-3E0B-D27E-09E0-38435167AE13">
</ContentProtection>
<!-- Common Encryption -->
<ContentProtection
schemeIdUri="urn:mpeg:dash:mp4protection:2011"
value="cenc"
cenc:default_KID="2E7F8229-C1C6-012D-3BAF-8778CBA29C69">
</ContentProtection>
In the shaka-project there is a fuction to generate a widevine pssh Box only by default_KID's:
https://github.com/shaka-project/shaka-packager/blob/main/packager/tools/pssh/pssh-box.py#L171
The needed proto file is located here: https://github.com/shaka-project/shaka-packager/blob/main/packager/media/base/widevine_pssh_data.proto
I am using it now like this in combination with your project:
widevine_key_video = "79F8D3B2-3E0B-D27E-09E0-38435167AE13"
widevine_key_audio = "2E7F8229-C1C6-012D-3BAF-8778CBA29C69"
def generate_pssh(self):
import base64
import struct
import widevine_pssh_data_pb2 as widevine_pssh_data_pb2
from pywidevine.pssh import PSSH
wv = widevine_pssh_data_pb2.WidevinePsshData()
wv.key_id.extend([base64.b16decode(widevine_key_video).replace('-','')])
wv.key_id.extend([base64.b16decode(widevine_key_audio).replace('-','')])
# 'cenc' is the default, so omitted to save bytes.
wv.protection_scheme = struct.unpack('>L', 'cenc'.encode())[0]
pssh_box = base64.b64encode(wv.SerializeToString()).decode("utf-8")
pssh = PSSH(pssh_box)
return pssh.dumps()
Is it be possible to implement the option to generate a pssh only by default_KID's into pywidevine ?
Thanks in advance.
where did this file come from?
https://github.com/rlaphoenix/pywidevine/blob/master/pywidevine/license_protocol.proto
is Widevine specified somewhere?
Is your feature request related to a problem? Please describe.
This would help tremendously with user-experience of quick calls of the CDM through manual means, as well as the possibility to manage and list the devices the user has access to, and perhaps also an admin side to manage permissions as well.
Describe the solution you'd like
A web UI with some fields to make the serve API calls.
So let's say your serve API is hosted at serve.domain.tld:80, you could head to https://serve.domain.tld, input your secret key, then make various calls like opening a session, getting challenges, parsing, and so on.
Describe alternatives you've considered
N/A
Additional context
N/A
Is your feature request related to a problem? Please describe.
Client code for pywidevine serve functionality currently has no way of getting information like required security level, or other misc information contained within licenses.
Describe the solution you'd like
We should add a /load endpoint that parses a License Response SignedMessage and stores the parsed License message with the keys decrypted in the session.
From there /keys could load the parsed and decrypted License message from the session, and return the keys as JSON. Realistically we don't need /keys endpoint in this scenario as the code should handle the response from /load to get keys.
Describe alternatives you've considered
One alternative to all this is if the client needs that information it could just parse the license message it sent to /keys itself, and then read that information as it isn't protected.
Additional context
In a secure Cdm implementation, this misc information would be required. In fact, returning the keys in plain text at all, or even passing alone a decryption key would not be safe.
Is your feature request related to a problem? Please describe.
Users could abuse by spamming requests for all sorts of reasons.
Describe the solution you'd like
Something like Flask-Limiter but for aiohttp.
Describe alternatives you've considered
N/A
Additional context
N/A
eme logger outputs pssh containing both playready and widevine at some sites
currently only the first pssh box is parsed
parse all box and if widevine pssh is found give priority to that one
sample
AAACsnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAApKSAgAAAQABAIgCPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBXAFoAYQAzAGUAOQB3ADMAQgA5AGIAWgB2AEIAbAA5AHoAYwB0ACsAaABnAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AC8AdQBVAEYARgBwADkAMgAyAE8ARQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcAA6AC8ALwBwAGwAYQB5AHIAZQBhAGQAeQB3AGUAYgAuAGMAbABhAHIAbwB2AGkAZABlAG8ALgBuAGUAdAAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4AAAAASXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACkIARIQWZa3e9w3B9bZvBl9zct+hhoDZGxhIghwZS5saWdhMSoCU0QyAA==
Describe the bug
Since the context data is cleared once a license message is parsed, you have one change to use /keys endpoint of serve.
Since /keys has no way of returning more than one type of keys, we can never have both Content keys and enc/mac keys.
To Reproduce
Steps to reproduce the behavior:
pywidevine serve <config>
Step 5 here will fail as the context data for that License Message's respective challenge would have been cleared.
Expected behavior
The two types of keys to be returned on the two separate requests.
A good solution would be a /parse endpoint, which you can then get keys from a loaded and parsed
session with /keys. This would also remove unnecessary re-parsing of a challenge in the first place.
Screenshots
N/A
Additional context
N/A
Hello greetings I have several questions I hope I can solve them by this means.
Is the Provisioned CDM Path, using same style with others ?
Thank you
Describe the bug
When running create-device
, it raises an exception. I've been fiddling around and it's because at the Device __init__
step, the function ClientIdentification
returns an almost empty object.
The original code is this:
def __init__(...):
...
self.client_id = ClientIdentification()
After that instruction, self.client_id
prints as an empty string, its ByteSize
method returns 0, the ClientCapabilities
prints as an empty string, and the __sizeof__()
is 64.
To Reproduce
I've run this command:
pywidevine create-device -t CHROME -l 3 -k ./my/key -c my/client_blob -v ./my/vmp_blob -o dv
The same happens when the -v
argument is ommited.
Expected behavior
A .wvd file should be created.
Android devices are provisioned in factory using keyboxes, not with certificates. See for instance https://github.com/zybpp/Python/blob/master/Python/keybox/widevine/Lenovo%20TB-X705-TAB510PLUS-04020003.bin.secure
As far as I understand, a device can not make requests to service providers with that keybox (even though this seems to be a valid ClientIdentification looking at license_protocol.proto?). If I understand correctly, a device first needs to call a provisioning endpoint (at least that's what the MediaDrm Android API make it look like). Checking a strings libwvhidl.so |grep provision
I'm guessing the URL is https://www.googleapis.com/certificateprovisioning/v1/devicecertificates/create?key=AIzaSyB-5OLKTx2iU5mko18DfdwK5611JIjbUhE (but that can confirmed that by interception provisioning calls). I see in other reversed pb there is a ProvisioningRequest message (though that other source doesn't give the content of ProvisioningRequest)
Would it be possible for pywidevine to implement provisioning of device certificates based on keyboxes?
Describe the bug
For example, <html><body>Server error, sorry</body></html>
(as UTF8 bytes) will be parsed successfully as a SignedMessage
with ParseFromString()
into a partial message (with minimal data supplied).
In this specific example, it will parse as a very bare LICENSE_REQUEST
type SignedMessage. If this is supplied to the parse_license
method it will fail in a weird way. It of course expects a LICENSE
type SignedMessage, but gets a LICENSE_REQUEST
type SignedMessage which would be very unexpected.
To Reproduce
Steps to reproduce the behavior:
pip install pywidevine
<html><body>Server error, sorry</body></html>
to parse_license
instead of a real license response.InvalidLicenseMessage
exception stating, Expecting a LICENSE message, not a 'LICENSE_REQUEST' message
.Expected behavior
It should have failed to parse the message entirely stating a different error, being more accurate to the validity of the input data.
Screenshots
N/A
Additional context
N/A
You did not mention if this require a widevine to be install first ?
does it require the widevine ? example i have ubuntu do i need to install first the widevine.so to make this work ? or it doesn't matter if i have a widevine or not.
I have error but requests response is work but cant find key
pywidevine\cdm.py", line 393, in parse_license
raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0',
'Accept': '*/*',
'Accept-Language': 'en',
# 'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/json',
'Referer': 'https://astrogo.astro.com.my/',
'cache-control': 'no-cache , no-store',
'Origin': 'https://astrogo.astro.com.my',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site',
'authorization': 'Bearer eyJraWQiOiI2OWM2MzRkMi0zNjBiLTQ0YzEtODUwZC05',
'Connection': 'keep-alive',
}
json_data = {
'contentID': '601',
'contentType': 2,
'authorizationToken': 'AAAAQAAAAKEAAAB',
'authorizationTokenType': '1',
'licenseChallenge': 'CAES7SsSIwohCgsiAzYwMUjj3JWl0Hfy5q6ho/eyujdwdo6nYgGsbLgpkoUAAAAAQAAABQABQAQ3hym9dgCxVs=',
'playbackSessionCookie': None,
}
I fully admit, I'm out of my depth here, but I have my CDM and wvd file, I know where to find PSSH and the licensing URL, and I was successfully able to pull keys from bitmovin and tg4.ie as tests... but when I went to pull from a live site I wanted the video from I got a "Could not parse license_message as a SignedMessage, Error parsing message" response.
I also noticed that the PSSH value was MUCH longer than both of my test sites...
What am I doing wrong?
Full response here:
INFO:root:pywidevine version 1.8.0 Copyright (c) 2022-2024 rlaphoenix
INFO:root:https://github.com/devine-dl/pywidevine
INFO:license:[+] Loaded Device (7283 L3)
INFO:license:[+] Loaded CDM
INFO:license:[+] Opened CDM Session: 2438e9eb69e80503839dbad22c8e3282
INFO:license:[+] Created License Request Message (Challenge)
INFO:license:[+] Got License Message
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/cdm.py", line 407, in parse_license
signed_message.ParseFromString(license_message)
google.protobuf.message.DecodeError: Error parsing message
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.12/bin/pywidevine", line 8, in <module>
sys.exit(main())
^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1078, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/click/core.py", line 783, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/main.py", line 116, in license_
cdm.parse_license(session_id, licence)
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pywidevine/cdm.py", line 411, in parse_license
raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message
It would be great if remote cdm can select random device in user's whitelist automatically.
Thus, users no need to provide specific device in api request if they have multiple device key.
Describe the bug
Error while using .dumps() on some PSSH, for testing used: https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd (AAAARHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACQIARIBNRoNd2lkZXZpbmVfdGVzdCIKMjAxNV90ZWFycyoCU0Q=
), this is only video with that problem as far as I tested
To Reproduce
Steps to reproduce the behavior:
from pywidevine.pssh import PSSH
# https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd
pssh = PSSH('AAAARHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACQIARIBNRoNd2lkZXZpbmVfdGVzdCIKMjAxNV90ZWFycyoCU0Q=')
print(pssh.dumps())
Expected behavior
pssh.dumps() works fine
Additional context
Traceback (most recent call last):
File "test\pssh.py", line 8, in <module>
print(pssh.dumps())
File "lib\site-packages\pywidevine\pssh.py", line 240, in dumps
return base64.b64encode(self.dump()).decode()
File "lib\site-packages\pywidevine\pssh.py", line 234, in dump
key_IDs=self.key_ids,
File "lib\site-packages\pywidevine\pssh.py", line 200, in key_ids
return [
File "lib\site-packages\pywidevine\pssh.py", line 202, in <listcomp>
UUID(bytes=key_id) if len(key_id) == 16 else UUID(hex=key_id.decode())
File "lib\uuid.py", line 177, in __init__
raise ValueError('badly formed hexadecimal UUID string')
ValueError: badly formed hexadecimal UUID string
Could there be more specific information provided in the proto comment:
// The core message is the simple serialization of fields used by OEMCrypto.
// This field was introduced in OEMCrypto API v16.
Or perhaps a reference to where we could learn more about what this is?
I've noticed that on Chrome license's responses, the session key which is normally 256 bytes long to match the length of the device private key, now is 128 bytes. It seems to suggest it is decrypted with a less secure private key (very unlikely) or the device private key is used elsewhere and differently.
I am not sure if I am missing anything or I am misunderstanding, but has this been noted or researched? or maybe this is on web based CDM's only? Padding the session key is not an option that works.
Installed PYthon 3.12 on my windows 10. When I try to do pip install pywidevine i get this:
ERROR: Ignored the following versions that require a different python version: 1.0.0 Requires-Python >=3.7,<3.11; 1.0.1 Requires-Python >=3.7,<3.11; 1.1.0 Requires-Python >=3.7,<3.11; 1.1.1 Requires-Python >=3.7,<3.11; 1.2.0 Requires-Python >=3.7,<3.11; 1.2.1 Requires-Python >=3.7,<3.11; 1.3.0 Requires-Python >=3.7,<3.11; 1.3.1 Requires-Python >=3.7,<3.11; 1.4.0 Requires-Python >=3.7,<3.11; 1.4.1 Requires-Python >=3.7,<3.11; 1.4.2 Requires-Python >=3.7,<3.11; 1.4.3 Requires-Python >=3.7,<3.11; 1.4.4 Requires-Python >=3.7,<3.11; 1.5.0 Requires-Python >=3.7,<3.11; 1.5.1 Requires-Python >=3.7,<3.11; 1.5.2 Requires-Python >=3.7,<3.11; 1.5.3 Requires-Python >=3.7,<3.11; 1.6.0 Requires-Python >=3.7,<3.12
ERROR: Could not find a version that satisfies the requirement pywidevine (from versions: none)
ERROR: No matching distribution found for pywidevine
Need help,
i got this error
ERROR:license:[-] Failed to send challenge: [403] {"code":403,"message":"Headers mismatch"}
I think this need header for lic url,
which folder to place headers.py ?
i use pywidevine version 1.8.0
I am using a CDM dumped from my Xiaomi device, which works fine for various services. However, for a certain service, I am experiencing the issue mentioned in the title.
I am using disposable bearer tokens for this service, which I can generate on my own and which work properly, so I don't think there is an issue with the headers.
What should I do in this case?
.
.
The following is the response for the license:
'{"service_version_info":{"license_sdk_version":"17.0.1","license_service_version":"DRMtoday"},"supported_tracks":[{"type":"HD","key_id":"Q8eOvWDlSx+MIAvU8K44CQ=="}],"message_type":"LICENSE_REQUEST","status":"OK","license":"CAISiQMKXAogRkI3MTcxNzMwMDAwMDAwMDAxMDAwMDAwMDAwMDAwMDASJgoQPLpOHP3/S2q450lWhjqM/hABMhA6VmJ0Nfyg/h/6+aO8TxPwIAEoADjl/UhA5f1ISNG1rKEGEiIIARABGAAg5f1IKOX9SDDl/Ug4AEIASABQAFgAYAFwAHgAGmYSEAjdFgyJTJzU4FFEUqqU7nIaUG3PpL20gH8pXs5xKXEgFHAHUdNw+G8PHZBrHk9VI4SaPO0nCE2dvs+s72kQ1DlIjM3lEiB2xO8ppwXRYA6lf5CUOiwQptzlxz8+kPG739E7IAEalAEKEEPHjr1g5UsfjCAL1PCuOAkSEKNPXhmgNWGwksn2VEwboZUaIPBguUWx1+7/ys4zHNoI6LcFTecpab8hUvJXrK0i+z5GIAIoATIICAAQKhgAIAA6CAgAECoYACAAQjQKIIwlBSr23yYYmBxNtbfatK3uXtZ82m9ER5Tryl7JnEUxEhD+EYrd5QlDNzh8qhDJrWFIING1rKEGOAAaIA5YaN4hEbbkvgVbzcl+v5hvGMZ63X2eenSLJDoH6sJLIoACpY8LxEV2FRwtalxAnHHBru0zcTG06ZTKKt60bHonCw4LJl/RpNNuepQfdP4Kx7pYY1pobhj45/dPGP8/Zm6IAfnJT3aW0e+nC/e+dKsbE3aDg2NhclpNF7SlAtt+v+vkLpeualRb8AoXyAc5+iiGv1PAMtYxU2vduqg0jpx6nUyCFNllJ8H8BatBMvuvUqgLsfjo43hjuEeq3iscptm4ziJJ4e5Uta5p3ZHvUBVBJBJKZ/divJl22J/VFt5NG/ZSh8jZzw/SA963QS9Hogg3LK94pEn5pJNGTT4TXrNa1NTBRLjSVbkKaHJ4V+QQp3vjSRDnKwpyvPWssE6RkEPH+zoICgYxNy4wLjFAAVgA","platform":"android"}'
The following are the details of the exception:
Could not parse license_message as a SignedMessage, Error parsing message
google.protobuf.message.DecodeError: Error parsing message
During handling of the above exception, another exception occurred:
File "F:\getkeys.py", line 38, in
cdm.parse_license(session_id, licence.content)
pywidevine.exceptions.InvalidLicenseMessage: Could not parse license_message as a SignedMessage, Error parsing message
.
.
Lastly, thank you very much for developing such an amazing program!
Is your feature request related to a problem? Please describe.
I'm trying to fetch key from a server but it fails with "partial decode" error when validating the certificate.
It appears that the server is returning 2 certificates. I managed to decode the message using the following declaration.
message SignedDrmCertificates {
repeated SignedDrmCertificate drm_certificate = 1;
}
Unfortunately, I don't have enough knowledge about the widevine underlying to tell how this kind of answer should be handled.
Here is a base64 license server response: cert.txt
Describe the bug
Cannot dump v0 pssh when init_data contain key_id
To Reproduce
Steps to reproduce the behavior:
from pywidevine.pssh import PSSH
# v0 pssh not contain key_id
a = 'AAAAR3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACcIARoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQ='
b = PSSH(a)
print(b.dump().hex())
# dump v0 pssh
# >>000000477073736800000000edef8ba979d64acea3c827dcd51d21ed0000002708011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a024844
print(b.key_ids)
# >>[]
c = PSSH.new(key_ids=None, init_data=b.init_data, version=0, flags=0)
print(c.dump().hex())
# dump v0 pssh
# >>000000477073736800000000edef8ba979d64acea3c827dcd51d21ed0000002708011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a024844
print(c.key_ids)
# >>[]
# v0 pssh contain key_id
d = 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
e = PSSH(d)
print(e.dump().hex())
# dump v1 pssh
# >>0000006f7073736801000000edef8ba979d64acea3c827dcd51d21ed00000001eb676abbcb345e96bbcf616630f1a3da0000003b08011210eb676abbcb345e96bbcf616630f1a3da1a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200
print(e.key_ids)
# >>[UUID('eb676abb-cb34-5e96-bbcf-616630f1a3da')]
f = PSSH.new(key_ids=None, init_data=e.init_data, version=0, flags=0)
print(f.dump().hex())
# dump v1 pssh
# >>0000006f7073736801000000edef8ba979d64acea3c827dcd51d21ed00000001eb676abbcb345e96bbcf616630f1a3da0000003b08011210eb676abbcb345e96bbcf616630f1a3da1a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200
print(f.key_ids)
# >>[UUID('eb676abb-cb34-5e96-bbcf-616630f1a3da')]
f.set_key_ids([])
print(f.dump().hex())
# dump v0 pssh but key_id is removed from init_data
# >>000000497073736800000000edef8ba979d64acea3c827dcd51d21ed0000002908011a0d7769646576696e655f746573742210666b6a336c6a61536466616c6b72336a2a0248443200
Expected behavior
all should be dump with v0 pssh
I tried it on Arch Linux with:
pipx install pywidevine
and on MacOS with:
pip install pywidevine
After installing it, I run pywidevine -h
and this is the output:
Traceback (most recent call last):
File "/opt/homebrew/bin/pywidevine", line 5, in <module>
from pywidevine.main import main
File "/opt/homebrew/lib/python3.11/site-packages/pywidevine/main.py", line 9, in <module>
import yaml
ModuleNotFoundError: No module named 'yaml'
Is your feature request related to a problem? Please describe.
You may want to set up your environment for the Python Cdm to be usable by another user, but have the sensitive provision RSA key and Client ID blob kept out of reach.
Currently, there is no option for this sort of scenario without manually altering the Cdm code entirely, or wrapping it in an API (e.g., fastapi or flask).
Describe the solution you'd like
There are a few ways I can do this and I cannot decide between them for the best way.
The first would be to implement an abstracted RemoteCdm
class in which developers can implement the different methods get_license_challenge
and so on, linking them to their own different API schemas or remote connection methods. This would likely be the easiest on my part, but provide almost no benefit to the actual ender user using this package as they could pretty much already do this if they implement exactly this themselves and ignore type-hint errors.
The second method would be to implement RemoteCdm
in full, as a specific API schema. I would have to also add code to serve one of these Remote CDM APIs, perhaps through aiohttp, flask, or such, so that the user can actually create and use a RemoteCdm without doing the guesswork themselves on serving the API. This would all result in almost no work for the user other than needing to deal with using pywidevine to run the API server, and to manage authentication/API tokens.
Describe alternatives you've considered
An alternative method would be to look at making Abstract Device classes rather than Cdm classes. Since the Device
class is semantic to the root device keys/access, it would be more ideal. The problem is if it is remote, we shouldn't be providing the Private Keys and such as plain text, but we need to as the Cdm class expects there to be a private_key class instance attribute.
We could move the code that deals with the generation and parsing of license acquisition to the Device class, and therefore abstract it for Local/Remote Device classes, but that reduces the actual workload of the Cdm class to almost nothing. It would end up being more of a proxy to the Device classes' methods, and make using the Cdm class almost pointless. Dealing with storage and fetching of session related data would also be complicated under such a scenario.
Has anyone ever managed to figure out what oemcrypto_core_message is?
I've been looking into this project which includes info on OEMCrypto function calls, but I don't have an Android to try it with.
Example from a license request:
00 00 00 01 00 00 00 14 00 05 00 10 28 a8 d7 b9 03 7e b7 93
{uint32; 1 for request, 2 for response} {uint32; length of entire string} 00 05 00 10 {8-byte nonce}
Its corresponding response:
00 00 00 02 00 00 01 c8 00 05 00 10 28 a8 d7 b9 03 7e b7 93 00 00 00 50 00 00 00 10 00 00 00 62 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 51 80 00 00 00 00 00 01 51 80 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 a8 00 00 00 10 00 00 00 ba 00 00 00 10 00 00 00 cc 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 00 00 00 00 10 00 00 01 12 00 00 00 10 00 00 01 24 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 58 00 00 00 10 00 00 01 6a 00 00 00 10 00 00 01 7c 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 b3 00 00 00 10 00 00 01 c5 00 00 00 10 00 00 01 d7 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 0b 00 00 00 10 00 00 02 1d 00 00 00 10 00 00 02 2f 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 63 00 00 00 10 00 00 02 75 00 00 00 10 00 00 02 87 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 bb 00 00 00 10 00 00 02 cd 00 00 00 10 00 00 02 df 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 03 13 00 00 00 10 00 00 03 25 00 00 00 10 00 00 03 37 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 ca df 6b 58 bd 1d 6b 08 98 c5 c9 d6 81 b9 48 99 51 c0 7e 12 e0 32 ea db be af 5e 36 4e 3a 2d 2f
{uint32; 1 for request, 2 for response} {uint32; length of entire string} 00 05 00 10 {the same 8-byte nonce from the request} 00 00 00 50 00 00 00 10 00 00 00 62 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 51 80 00 00 00 00 00 01 51 80 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 a8 00 00 00 10 00 00 00 ba 00 00 00 10 00 00 00 cc 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 00 00 00 00 10 00 00 01 12 00 00 00 10 00 00 01 24 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 58 00 00 00 10 00 00 01 6a 00 00 00 10 00 00 01 7c 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 01 b3 00 00 00 10 00 00 01 c5 00 00 00 10 00 00 01 d7 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 0b 00 00 00 10 00 00 02 1d 00 00 00 10 00 00 02 2f 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 63 00 00 00 10 00 00 02 75 00 00 00 10 00 00 02 87 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 02 bb 00 00 00 10 00 00 02 cd 00 00 00 10 00 00 02 df 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 00 00 03 13 00 00 00 10 00 00 03 25 00 00 00 10 00 00 03 37 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 10 {seems like a sha256 hash, possibly a hmac}
It does not seem to be encrypted.
Originally posted by @0xmerp in #19 (comment)
Is your feature request related to a problem? Please describe.
When using /challenge, specifying a service certificate (if you intend to use one) is required on every /challenge call which is simply unnecessary. We shouldn't need to set a service certificate explicitly on each /challenge call. Instead, set it only when you want to set the initial service certificate or change it to another.
Describe the solution you'd like
Have a /service_certificate (or similar) endpoint to call the underlying cdm.set_service_certificate() method instead of doing this within /challenge. This reduces the amount of data that passes through /challenge, reduces the complexity of /challenge, and allows users to only change service certificates when needed.
Describe alternatives you've considered
There's not really any alternative I can think of. This would be something the API schema would need to change.
Additional context
When implementing client code for the API schema, it's quite strange to see that the original set_service_certificate method can be used as-is, considering the service certificate is more so set with /challenge (during get_license_challenge()). It's just not correct.
Describe the bug
500 Server Error when using RemoteCDM on BitMovin demo.
To Reproduce
Steps to reproduce the behavior:
set_service_certificate(session_id, Cdm.common_privacy_cert)
get_license_challenge
successful.Expected behavior
Challenge is valid.
Additional context
Works fine without privacy mode, same provision on same machine.
Testing using local WVD through the CLI works as well.
Relevant code. Redacted for privacy
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
#license URL
license_url = "https://cwip-shaka-proxy.appspot.com/no_auth"
cdm = RemoteCdm("ANDROID", 1234, 3, "server_url", "secret_key", "test_device_01")
# open cdm session
session_id = cdm.open()
cdm.set_service_certificate(session_id, Cdm.common_privacy_cert)
# get license challenge
challenge = cdm.get_license_challenge(session_id, pssh, "STREAMING", True)
# send license challenge (assuming a generic license server SDK with no API front)
license = requests.post(license_url, data=challenge)
license.raise_for_status()
Is your feature request related to a problem? Please describe.
Currently RemoteCdm needs the Device Type, System ID and Security Level specified manually with absolutely no context to base off of.
When /open is called, it has to verify the values to be correct. However, what if you need to find out the values beforehand? No options without asking whoever is serving.
Describe the solution you'd like
A get call to /device e.g., /device/test_device_001 will return the Device Type, System ID, and Security Level for test_device_001. This endpoint could even be automated as part of the RemoteCdm construction procedure, before the super call.
Describe alternatives you've considered
N/A
Additional context
N/A
Hi,
I need to pass user-agent in request header
I tried to create a header.py and added user-agent: Mozilla... as header in header.py
and then import header but its not working...
Does someone know how to pass user-agent? in header request?
send license challenge (assuming a generic license server SDK with no API front)
licence = requests.post("https://...", data=challenge)
licence.raise_for_status()
Describe the bug
License request constructed by pywidevine gets rejected by license server while the device itself plays the video without issue. The situation indicates that the license server has a means to distinguish the license request constructed by pywidevine comparing to chrome on the device.
The request to license server is constructed correctly, otp, token, headers. Confirmed by more users. So started to wonder if there is anything that the license server can distinguish other than the key+blob since these are correct.
To Reproduce
Create Android device in emulator, Pixel 6 Pro, Android 11. Extract keys for CDM version 16.0.0. Construct license challenge for vdocipher license server. Challenge has token that contains otp, playback info, href, tech, and license request. Make a post request.
Expected behavior
A correctly constructed license request that will in request to license server return the license.
Additional context
pywidevine test succeeds.
The context may look like out of concern of pywidevine at first sight. However, the case indicates that the license server can distinguish license request from pywidevine from android 11's chrome. Therefore it should be a concern of pywidevine construct license request that is identical to the browser on the device.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.