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

Feature/drm_support #2115

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 [#2115](https://github.com/react-native-community/react-native-video/pull/2115)

### Version 5.1.0-alpha6 [WIP]

- Fix iOS bug which would break size of views when video is displayed with controls on a non full-screen React view. [#1931](https://github.com/react-native-community/react-native-video/pull/1931)
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.

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.
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 @@ -415,6 +415,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 @@ -799,6 +804,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.
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
35 changes: 34 additions & 1 deletion 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 @@ -232,6 +233,27 @@ 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) {
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 @@ -304,6 +326,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 @@ -379,6 +402,16 @@ Video.propTypes = {
// 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
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