Skip to content
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

Add iOS and Android basic DRM support #1445

Merged
merged 118 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from 117 commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
5154cb6
support drm exoplayer
yaraht17 Sep 21, 2017
fee5c0e
Merge pull request #2 from react-native-community/master
danielmarino24i Jan 2, 2019
51b0085
Improvements + Merge branch 'yaraht17/without-surface' into feature/n…
Jan 11, 2019
8d62b78
Merge branch 'feature/nrv-update-plus-yaraht17'
Jan 11, 2019
8753111
change repo
Jan 11, 2019
ea7601e
correct wrong term
Jan 11, 2019
0755c8c
nest drm under source
Jan 11, 2019
a665eb1
missin semicolon
Jan 11, 2019
ad9ad49
fix getString -> getMap for headers
Jan 11, 2019
06bb389
drm headers processing
Jan 14, 2019
7ae01b3
disable textureView when DRM
Jan 14, 2019
870e007
Refactor with exoplayer util getDrmUuid
Jan 14, 2019
8506567
missing import
Jan 14, 2019
347d525
setDrmType does not throw anymore
Jan 14, 2019
fd7858e
Log msg that TextureView will be disabled when using DRM
Jan 14, 2019
b61e02a
Update Readme
Jan 14, 2019
dcbf469
avoid props change on textureView when using DRM
Jan 14, 2019
29f5ad3
remove unneeded tabulations
Jan 14, 2019
8f29f85
preparing DRM
Jan 15, 2019
e8da925
remove AVAssetResourceLoaderDelegate method from .h
Jan 16, 2019
7b63331
comment to test building process
Jan 16, 2019
34654f7
fixes
Jan 16, 2019
bceefc6
allow DRM object
Jan 17, 2019
e65aa2c
content id override
Jan 17, 2019
d2c2cdc
WIP cb
Jan 18, 2019
d262801
WIP abstraction
Jan 18, 2019
b66d345
DRM type as enum + propType
Jan 21, 2019
8d360fd
overrridable getLicense
Jan 21, 2019
69f7f38
to async method
Jan 21, 2019
aa07618
license override
Jan 21, 2019
9160c0b
error handling
Jan 21, 2019
f1d97c1
change error domain and protect error throwing
Jan 22, 2019
7367a90
set license error method
Jan 22, 2019
bcd9c7a
More error handling
Jan 22, 2019
e3801d6
error interface
Jan 22, 2019
d2f9641
process headers
Jan 22, 2019
4d2ffd2
Merge branch 'master' into feature/ios-get-license-cb
Jan 22, 2019
cad76c1
Export DRMType
Jan 22, 2019
13e7d1c
fix log android
Jan 22, 2019
f2f33a8
include DRMType to files exported to npm
Jan 22, 2019
4afb12b
bubble error to JS
Jan 23, 2019
8013450
Merge branch 'rnv-master' into feature/ios-drm-support
Jan 23, 2019
02fd425
Update README with iOS info
Jan 23, 2019
e11fe62
Provide more DRM info
Jan 23, 2019
2391747
more DOC
Jan 23, 2019
3cfdc7e
Merge branch 'feature/PR-android-and-ios-DRM-support-to-RNV' into fea…
Jan 23, 2019
eed8619
remove unneeded logs
Jan 23, 2019
cdfb630
unneeded toast
Jan 23, 2019
6cfcf3b
More info about SPC
Jan 23, 2019
a89ac0c
clean base64 + native method
Jan 23, 2019
a8cba98
return NO instead of false
Jan 23, 2019
7b1409c
return result of method (always NO)
Jan 23, 2019
3110864
add finishLoadingWithError to headers
Jan 23, 2019
7ccd4b6
change invocation of finisLoadingwithError
Jan 23, 2019
2713e8f
fix base64
Jan 24, 2019
56cbf9c
be more specific with base64 needed
Jan 24, 2019
ab900e5
Merge branch 'rnv-master' into feature/ios-drm-support
Jan 24, 2019
b813a84
alphabetically
Jan 24, 2019
7836797
Update CHANGELOG
Jan 24, 2019
403b01e
change to next the version
Jan 25, 2019
397d14e
Merge branch 'rnv-master' into feature/ios-drm-support
Jan 25, 2019
4e4d8b1
error enum
Jan 25, 2019
d5f1813
typo
Jan 25, 2019
c0cd82e
no spc error
Jan 25, 2019
9b3e4e7
fix for tvOS when using Pods
Jan 28, 2019
477607b
fix headers
Jan 29, 2019
5f6425f
fix for non playeritem errors
Jan 29, 2019
b8ccec8
bubble DRM error on Android
Feb 1, 2019
56d2af5
fix detox tests
Feb 4, 2019
4dd1c73
improve props
Feb 12, 2019
d759e97
Merge branch 'feature/ios-drm-support' of github.com:24i/react-native…
Feb 12, 2019
e5cd51a
allow base64 cer response
Feb 12, 2019
d981f80
Update iOS readme
Feb 12, 2019
e7f1732
Merge branch 'rnv-master' into feature/ios-drm-support
Feb 12, 2019
801dce4
more readable java version for gradle
Feb 14, 2019
fd16a93
Merge branch 'rnv-master' into feature/ios-drm-support-update
Feb 21, 2019
59b541c
fix merge
Feb 21, 2019
5b92ea0
remove extra curly brace
Feb 22, 2019
a7780f9
add missing curly after merge
Feb 22, 2019
decb56e
support for controls
CHaNGeTe Mar 5, 2019
9df4f89
Merge branch 'rnv-master' into feature/ios-drm-support
CHaNGeTe Mar 15, 2019
0c87b78
Merge branch 'rnv-master' into feature/update-rnv-and-refactor-src
CHaNGeTe Apr 11, 2019
9b5ced2
split DRM doc
CHaNGeTe Apr 15, 2019
6e3e653
decoupling drm from source prop
CHaNGeTe Apr 15, 2019
24730ed
ios preparePlayback
CHaNGeTe Apr 15, 2019
5725c62
fix var name
CHaNGeTe Apr 15, 2019
4d12d1d
drm prop
CHaNGeTe Apr 16, 2019
4210929
test async
CHaNGeTe Apr 16, 2019
3b952dc
preparing android
CHaNGeTe Apr 16, 2019
1b680c2
fixes android
CHaNGeTe Apr 16, 2019
a22bf45
protect
CHaNGeTe Apr 16, 2019
ce4dedf
preparePlayback not needed
CHaNGeTe Apr 17, 2019
8818b9a
revert
CHaNGeTe Apr 17, 2019
bdc599d
improvements shouldwait
CHaNGeTe Apr 17, 2019
a296fd9
extend doc
CHaNGeTe Apr 17, 2019
2fc3b77
finishLoading DRM request when changing source
CHaNGeTe Apr 26, 2019
2d0e402
fixes + unify indentation
CHaNGeTe Apr 26, 2019
9fa4046
update readme with more scenarios
CHaNGeTe Apr 26, 2019
88c5541
change stream fixes
Jun 4, 2019
91f200b
initializePlayback postDelayed
Jun 4, 2019
f6bc13e
Merge branch 'master' into feature/ios-drm-support
CHaNGeTe Jun 13, 2019
fb3175f
remove logs
Jun 15, 2019
4c4899b
Update DRM.md
CHaNGeTe Jul 1, 2019
a48fb69
Update DRM.md
CHaNGeTe Jul 1, 2019
c123d86
Update README.md
CHaNGeTe Jul 1, 2019
e6710f3
Update ReactExoplayerView.java
CHaNGeTe Jul 1, 2019
e9c978c
Update DRM.md
CHaNGeTe Jul 2, 2019
7525d4a
Update Video.js
CHaNGeTe Jul 2, 2019
f484bd8
Merge branch 'rnv-master' into feature/ios-drm-support
Jul 3, 2019
395b9b7
Merge master
Sep 28, 2019
d266b32
pass drm to exoplayer
Sep 28, 2019
03b2d4d
comment
Sep 28, 2019
62166a2
complete
Sep 28, 2019
725a15c
Merge branch 'react-native-video/master' into feature/ios-drm-support
Sep 28, 2019
c7a8a25
change to DRMType
CHaNGeTe Sep 30, 2019
8c40aaf
more params for onGetLicense (spcBase64 and contentId)
Oct 7, 2019
32748d5
base64 -> string
Oct 7, 2019
8c25828
Merge branch 'master' into feature/ios-drm-support
cobarx Aug 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Changelog

### next
* Basic support for DRM on iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445)

### Version 5.1.0-alpha1
* Fixed Exoplayer doesn't work with mute=true (Android). [#1696](https://github.com/react-native-community/react-native-video/pull/1696)
* Added support for automaticallyWaitsToMinimizeStalling property (iOS) [#1723](https://github.com/react-native-community/react-native-video/pull/1723)
Expand Down
139 changes: 139 additions & 0 deletions DRM.md
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.
CHaNGeTe marked this conversation as resolved.
Show resolved Hide resolved

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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
});
}
}
```
6 changes: 6 additions & 0 deletions DRMType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
WIDEVINE: 'widevine',
PLAYREADY: 'playready',
CLEARKEY: 'clearkey',
FAIRPLAY: 'fairplay'
};
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ Determines whether video audio should override background music/audio in Android

Platforms: Android Exoplayer

### DRM
To setup DRM please follow [this guide](./DRM.md)

Platforms: Android Exoplayer, iOS

#### filter
Add video filter
* **FilterType.NONE (default)** - No Filter
Expand Down Expand Up @@ -785,6 +790,17 @@ Note: Using this feature adding an entry for NSAppleMusicUsageDescription to you

Platforms: iOS

##### Explicit mimetype for the stream

Provide a member `type` with value (`mpd`/`m3u8`/`ism`) inside the source object.
CHaNGeTe marked this conversation as resolved.
Show resolved Hide resolved
Sometimes is needed when URL extension does not match with the mimetype that you are expecting, as seen on the next example. (Extension is .ism -smooth streaming- but file served is on format mpd -mpeg dash-)

Example:
```
source={{ uri: 'http://host-serving-a-type-different-than-the-extension.ism/manifest(format=mpd-time-csf)',
type: 'mpd' }}
```

###### Other protocols

The following other types are supported on some platforms, but aren't fully documented yet:
Expand Down
38 changes: 35 additions & 3 deletions Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -12,7 +13,7 @@ const styles = StyleSheet.create({
},
});

export { TextTrackType, FilterType };
export { TextTrackType, FilterType, DRMType };

export default class Video extends Component {

Expand Down Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.spc is empty in JavaScriptCore environment 🤷‍♂ I'm checking data.spcBase64 as workaround.

Copy link

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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];
Expand Down Expand Up @@ -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,
Expand All @@ -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,
});
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions android-exoplayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ android {
versionCode 1
versionName "1.0"
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private DataSourceUtil() {

private static DataSource.Factory rawDataSourceFactory = null;
private static DataSource.Factory defaultDataSourceFactory = null;
private static HttpDataSource.Factory defaultHttpDataSourceFactory = null;
private static String userAgent = null;

public static void setUserAgent(String userAgent) {
Expand Down Expand Up @@ -58,6 +59,17 @@ public static void setDefaultDataSourceFactory(DataSource.Factory factory) {
DataSourceUtil.defaultDataSourceFactory = factory;
}

public static HttpDataSource.Factory getDefaultHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
if (defaultHttpDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
defaultHttpDataSourceFactory = buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders);
}
return defaultHttpDataSourceFactory;
}

public static void setDefaultHttpDataSourceFactory(HttpDataSource.Factory factory) {
DataSourceUtil.defaultHttpDataSourceFactory = factory;
}

private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) {
return new RawResourceDataSourceFactory(context.getApplicationContext());
}
Expand Down
Loading