-
Notifications
You must be signed in to change notification settings - Fork 3k
Add iOS and Android basic DRM support #1445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 117 commits
5154cb6
fee5c0e
51b0085
8d62b78
8753111
ea7601e
0755c8c
a665eb1
ad9ad49
06bb389
7ae01b3
870e007
8506567
347d525
fd7858e
b61e02a
dcbf469
29f5ad3
8f29f85
e8da925
7b63331
34654f7
bceefc6
e65aa2c
d2c2cdc
d262801
b66d345
8d360fd
69f7f38
aa07618
9160c0b
f1d97c1
7367a90
bcd9c7a
e3801d6
d2f9641
4d2ffd2
cad76c1
13e7d1c
f2f33a8
4afb12b
8013450
02fd425
e11fe62
2391747
3cfdc7e
eed8619
cdfb630
6cfcf3b
a89ac0c
a8cba98
7b1409c
3110864
7ccd4b6
2713e8f
56cbf9c
ab900e5
b813a84
7836797
403b01e
397d14e
4e4d8b1
d5f1813
c0cd82e
9b3e4e7
477607b
5f6425f
b8ccec8
56d2af5
4dd1c73
d759e97
e5cd51a
d981f80
e7f1732
801dce4
fd16a93
59b541c
5b92ea0
a7780f9
decb56e
9df4f89
0c87b78
9b5ced2
6e3e653
24730ed
5725c62
4d12d1d
4210929
3b952dc
1b680c2
a22bf45
ce4dedf
8818b9a
bdc599d
a296fd9
2fc3b77
2d0e402
9fa4046
88c5541
91f200b
f6bc13e
fb3175f
4c4899b
a48fb69
c123d86
e6710f3
e9c978c
7525d4a
f484bd8
395b9b7
d266b32
03b2d4d
62166a2
725a15c
c7a8a25
8c40aaf
32748d5
8c25828
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| # DRM | ||
|
|
||
| ## Provide DRM data (only tested with http/https assets) | ||
|
|
||
| You can provide some configuration to allow DRM playback. | ||
| This feature will disable the use of `TextureView` on Android. | ||
|
|
||
| DRM object allows this members: | ||
|
|
||
| | Property | Type | Default | Platform | Description | | ||
| | --- | --- | --- | --- | --- | | ||
| | [`type`](#type) | DRMType | undefined | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) | | ||
| | [`licenseServer`](#licenseserver) | string | undefined | iOS/Android | Specifies the license server URL | | ||
| | [`headers`](#headers) | Object | undefined | iOS/Android | Specifies the headers send to the license server URL on license acquisition | | ||
| | [`contentId`](#contentid) | string | undefined | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) | | ||
| | [`certificateUrl`](#certificateurl) | string | undefined | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file | | ||
| | [`base64Certificate`](#base64certificate) | bool | false | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 | | ||
| | [`getLicense`](#getlicense)| function | undefined | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream | | ||
|
|
||
| ### `base64Certificate` | ||
|
|
||
| Whether or not the certificate url returns it on base64. | ||
|
|
||
| Platforms: iOS | ||
|
|
||
| ### `certificateUrl` | ||
|
|
||
| URL to fetch a valid certificate for FairPlay. | ||
|
|
||
| Platforms: iOS | ||
|
|
||
| ### `getLicense` | ||
|
|
||
| `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` (as ASCII string, you will probably need to convert it to base 64) obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`. | ||
| You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. | ||
|
|
||
| With this prop you can override the license acquisition flow, as an example: | ||
|
|
||
| ```js | ||
| getLicense: (spcString) => { | ||
| const base64spc = Base64.encode(spcString); | ||
| const formData = new FormData(); | ||
| formData.append('spc', base64spc); | ||
| return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'pallycon-customdata-v2': 'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==', | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, | ||
| body: formData | ||
| }).then(response => response.text()).then((response) => { | ||
| return response; | ||
| }).catch((error) => { | ||
| console.error('Error', error); | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| Platforms: iOS | ||
|
|
||
| ### `headers` | ||
|
|
||
| You can customize headers send to the licenseServer. | ||
|
|
||
| Example: | ||
|
|
||
| ```js | ||
| source={{ | ||
| uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', | ||
| }} | ||
| drm={{ | ||
| type: DRMType.WIDEVINE, | ||
| licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', | ||
| headers: { | ||
| 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' | ||
| }, | ||
| }} | ||
| ``` | ||
|
|
||
| ### `licenseServer` | ||
|
|
||
| The URL pointing to the licenseServer that will provide the authorization to play the protected stream. | ||
|
|
||
| ### `type` | ||
|
|
||
| You can specify the DRM type, either by string or using the exported DRMType enum. | ||
| Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't look like you have a way to specify ClearKey keys, so we shouldn't mention it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have never work with clearkey, but if we get a sample stream for that, maybe it can be implemented, as exoplayer seems to support it |
||
| for iOS: DRMType.FAIRPLAY | ||
|
|
||
| ## Common Usage Scenarios | ||
|
|
||
| ### Send cookies to license server | ||
|
|
||
| You can send Cookies to the license server via `headers` prop. Example: | ||
|
|
||
| ```js | ||
| drm: { | ||
| type: DRMType.WIDEVINE | ||
| licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', | ||
| headers: { | ||
| 'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar' | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ### Custom License Acquisition (only iOS for now) | ||
|
|
||
| ```js | ||
| drm: { | ||
| type: DRMType.FAIRPLAY, | ||
| getLicense: (spcString) => { | ||
| const base64spc = Base64.encode(spcString); | ||
| return fetch('YOUR LICENSE SERVER HERE', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Accept: 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| getFairplayLicense: { | ||
| foo: 'bar', | ||
| spcMessage: base64spc, | ||
| } | ||
| }) | ||
| }) | ||
| .then(response => response.json()) | ||
| .then((response) => { | ||
| if (response && response.getFairplayLicenseResponse | ||
| && response.getFairplayLicenseResponse.ckcResponse) { | ||
| return response.getFairplayLicenseResponse.ckcResponse; | ||
| } | ||
| throw new Error('No correct response'); | ||
| }) | ||
| .catch((error) => { | ||
| console.error('CKC error', error); | ||
| }); | ||
| } | ||
| } | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| WIDEVINE: 'widevine', | ||
| PLAYREADY: 'playready', | ||
| CLEARKEY: 'clearkey', | ||
| FAIRPLAY: 'fairplay' | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, | |
| import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; | ||
| import TextTrackType from './TextTrackType'; | ||
| import FilterType from './FilterType'; | ||
| import DRMType from './DRMType'; | ||
| import VideoResizeMode from './VideoResizeMode.js'; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
|
|
@@ -12,7 +13,7 @@ const styles = StyleSheet.create({ | |
| }, | ||
| }); | ||
|
|
||
| export { TextTrackType, FilterType }; | ||
| export { TextTrackType, FilterType, DRMType }; | ||
|
|
||
| export default class Video extends Component { | ||
|
|
||
|
|
@@ -229,6 +230,26 @@ export default class Video extends Component { | |
| } | ||
| }; | ||
|
|
||
| _onGetLicense = (event) => { | ||
| if (this.props.drm && this.props.drm.getLicense instanceof Function) { | ||
| const data = event.nativeEvent; | ||
| if (data && data.spc) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally I've noticed this last Friday, I don't know why but data.spc is empty in JavaScriptCore except when JS Remote Debugger is enabled 🤔 I had to implement a fallback using the B64 version of spc
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will take a look at this, thank you so much! To be honest I always checked with debugger on, so I didn't expect this to come |
||
| const getLicenseOverride = this.props.drm.getLicense(data.spc, data.contentId, data.spcBase64, this.props); | ||
| const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. | ||
| getLicensePromise.then((result => { | ||
| if (result !== undefined) { | ||
| NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); | ||
| } else { | ||
| NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); | ||
| } | ||
| })).catch((error) => { | ||
| NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); | ||
| }); | ||
| } else { | ||
| NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); | ||
| } | ||
| } | ||
| } | ||
| getViewManagerConfig = viewManagerName => { | ||
| if (!NativeModules.UIManager.getViewManagerConfig) { | ||
| return NativeModules.UIManager[viewManagerName]; | ||
|
|
@@ -278,7 +299,7 @@ export default class Video extends Component { | |
| type: source.type || '', | ||
| mainVer: source.mainVer || 0, | ||
| patchVer: source.patchVer || 0, | ||
| requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {} | ||
| requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, | ||
| }, | ||
| onVideoLoadStart: this._onLoadStart, | ||
| onVideoLoad: this._onLoad, | ||
|
|
@@ -301,6 +322,7 @@ export default class Video extends Component { | |
| onPlaybackRateChange: this._onPlaybackRateChange, | ||
| onAudioFocusChanged: this._onAudioFocusChanged, | ||
| onAudioBecomingNoisy: this._onAudioBecomingNoisy, | ||
| onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense, | ||
| onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, | ||
| onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, | ||
| }); | ||
|
|
@@ -371,11 +393,21 @@ Video.propTypes = { | |
| /* Wrapper component */ | ||
| source: PropTypes.oneOfType([ | ||
| PropTypes.shape({ | ||
| uri: PropTypes.string | ||
| uri: PropTypes.string, | ||
| }), | ||
| // Opaque type returned by require('./video.mp4') | ||
| PropTypes.number | ||
| ]), | ||
| drm: PropTypes.shape({ | ||
| type: PropTypes.oneOf([ | ||
| DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY | ||
| ]), | ||
| licenseServer: PropTypes.string, | ||
| headers: PropTypes.shape({}), | ||
| base64Certificate: PropTypes.bool, | ||
| certificateUrl: PropTypes.string, | ||
| getLicense: PropTypes.func, | ||
| }), | ||
| minLoadRetryCount: PropTypes.number, | ||
| maxBitRate: PropTypes.number, | ||
| resizeMode: PropTypes.string, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.