From c37507768cc1bff00a1e574c7fc5038a8a949983 Mon Sep 17 00:00:00 2001 From: Niklas Saers Date: Wed, 19 Sep 2018 12:02:55 +0200 Subject: [PATCH 001/261] Instead of crashing, show a warning and return --- ios/Video/RCTVideo.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9fe30b6357..0302960b3c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -436,6 +436,10 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; + if([uri isEqualToString:@""] || [type isEqualToString:@""]) { + DebugLog(@"Could not find video URL in source '%@'", source); + return; + } NSURL *url = isNetwork || isAsset ? [NSURL URLWithString:uri] From 865db711792aee133ceb2ab4edbb600513ce80d1 Mon Sep 17 00:00:00 2001 From: Niklas Saers Date: Thu, 20 Sep 2018 13:38:39 +0200 Subject: [PATCH 002/261] Doh, should of course be AND, not OR --- ios/Video/RCTVideo.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0302960b3c..dc1208a35d 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -436,7 +436,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; - if([uri isEqualToString:@""] || [type isEqualToString:@""]) { + if([uri isEqualToString:@""] && [type isEqualToString:@""]) { DebugLog(@"Could not find video URL in source '%@'", source); return; } From 4cc9a4d3748aa7b7bcf76bf53c3ab93388d2b86f Mon Sep 17 00:00:00 2001 From: Ibrahim Sulaiman Date: Fri, 4 Jan 2019 14:58:32 +0530 Subject: [PATCH 003/261] Support for controls in android exoplayer --- .../exoplayer/ReactExoplayerView.java | 55 +++++++++++++++++++ .../exoplayer/ReactExoplayerViewManager.java | 6 ++ 2 files changed, 61 insertions(+) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 64c8a4043f..1c80c90945 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -64,6 +64,8 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +//Import PlayerControlView +import com.google.android.exoplayer2.ui.PlayerControlView; import java.net.CookieHandler; import java.net.CookieManager; @@ -96,6 +98,8 @@ class ReactExoplayerView extends FrameLayout implements } private final VideoEventEmitter eventEmitter; + //Create playerControlView instance + private PlayerControlView playerControlView; private Handler mainHandler; private ExoPlayerView exoPlayerView; @@ -257,6 +261,41 @@ public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { } // Internal methods + + /** + * Toggling the visibility of the player control view + */ + private void togglePlayerControlVisibility() { + if(playerControlView.isVisible()) { + playerControlView.setVisibility(INVISIBLE); + } else { + playerControlView.setVisibility(VISIBLE); + } + } + + /** + * Initialising Player control + */ + private void initialisePlayerControl() { + playerControlView = new PlayerControlView(getContext()); + LayoutParams layoutParams = new LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + playerControlView.setLayoutParams(layoutParams); + addView(playerControlView, 1, layoutParams); + + //Setting the player for the playerControlView + playerControlView.setPlayer(player); + + //Invoking onClick event for exoplayerView + exoPlayerView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + togglePlayerControlVisibility(); + } + }); + } + private void initializePlayer() { if (player == null) { TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); @@ -517,6 +556,10 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { onBuffering(false); startProgressHandler(); videoLoaded(); + //Setting the visibility for the playerControlView + if(playerControlView != null) { + playerControlView.setVisibility(VISIBLE); + } break; case ExoPlayer.STATE_ENDED: text += "ended"; @@ -1056,4 +1099,16 @@ public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBuffe releasePlayer(); initializePlayer(); } + + /** + * Handling controls prop + * + * @param controls value of the controls prop passed from react-native + */ + public void setControls(boolean controls) { + if(controls && (exoPlayerView != null)) { + //Initialise playerControlView + initialisePlayerControl(); + } + } } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 31bd8e08be..b3c6ad12f5 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -57,6 +57,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Fri, 4 Jan 2019 15:31:49 +0530 Subject: [PATCH 004/261] Updated README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ee34399ce..708afab454 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ Note on iOS, controls are always shown when in fullscreen mode. Controls are not available Android because the system does not provide a stock set of controls. You will need to build your own or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-video-player](https://github.com/cornedor/react-native-video-player). -Platforms: iOS, react-native-dom +Platforms: iOS, Android, react-native-dom #### filter Add video filter From 617b046789d7c5ece4335f33bc85e485c9862e5c Mon Sep 17 00:00:00 2001 From: Abdulrahman Alzenki Date: Fri, 26 Oct 2018 13:33:03 -0700 Subject: [PATCH 005/261] Implement picture in picture for iOS Test Plan: - Run on ipad - test out onIsPictureInPictureSupported, onIsPictureInPictureActive, restoreUserInterfaceForPictureInPictureStop, startPictureInPicture, stopPictureInPicture --- README.md | 73 +++++++++++++++++++++++++++++++++++++ Video.js | 28 ++++++++++++++ ios/Video/RCTVideo.h | 4 +- ios/Video/RCTVideo.m | 73 +++++++++++++++++++++++++++++++++++++ ios/Video/RCTVideoManager.m | 4 ++ 5 files changed, 181 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ee34399ce..3e28061fac 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,8 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent) * [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss) * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) +* [onIsPictureInPictureActive](#onispictureinpictureactive) +* [onIsPictureInPictureSupported](#onispictureinpicturesupported) * [onLoad](#onload) * [onLoadStart](#onloadstart) * [onProgress](#onprogress) @@ -308,7 +310,10 @@ var styles = StyleSheet.create({ * [dismissFullscreenPlayer](#dismissfullscreenplayer) * [presentFullscreenPlayer](#presentfullscreenplayer) * [save](#save) +* [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop) +* [startPictureInPicture](#startpictureinpicture) * [seek](#seek) +* [stopPictureInPicture](#stoppictureinpicture) ### Configurable props @@ -861,6 +866,38 @@ Payload: none Platforms: Android ExoPlayer, Android MediaPlayer, iOS +#### onIsPictureInPictureActive +Callback function that is called when picture in picture becames active on inactive. + +Property | Type | Description +--- | --- | --- +active | boolean | Boolean indicating whether picture in picture is active + +Example: +``` +{ + active: true +} +``` + +Platforms: iOS + +#### onIsPictureInPictureSupported +Callback function that is called initially to determine whether or not picture in picture is supported. + +Property | Type | Description +--- | --- | --- +supported | boolean | Boolean indicating whether picture in picture is supported + +Example: +``` +{ + supported: true +} +``` + +Platforms: iOS + #### onLoad Callback function that is called when the media is loaded and ready to play. @@ -1057,6 +1094,30 @@ Future: Platforms: iOS +#### restoreUserInterfaceForPictureInPictureStop +`restoreUserInterfaceForPictureInPictureStop(restore)` + +This function corresponds to Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: After picture in picture stops, this function must be called. + +Example: +``` +this.player.restoreUserInterfaceForPictureInPictureStop(true); +``` + +Platforms: iOS + +#### startPictureInPicture +`startPictureInPicture()` + +Calling this function will start picture in picture if it is supported. + +Example: +``` +this.player.startPictureInPicture(); +``` + +Platforms: iOS + #### seek() `seek(seconds)` @@ -1086,6 +1147,18 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac Platforms: iOS +#### stopPictureInPicture +`stopPictureInPicture()` + +Calling this function will stop picture in picture if it is currently active. + +Example: +``` +this.player.stopPictureInPicture(); +``` + +Platforms: iOS + diff --git a/Video.js b/Video.js index 92544467c0..42e9731c93 100644 --- a/Video.js +++ b/Video.js @@ -78,6 +78,18 @@ export default class Video extends Component { return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); } + startPictureInPicture = () => { + this.setNativeProps({ pictureInPicture: true }); + }; + + stopPictureInPicture = () => { + this.setNativeProps({ pictureInPicture: false }); + }; + + restoreUserInterfaceForPictureInPictureStop = (restore) => { + this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restore }); + }; + _assignRoot = (component) => { this._root = component; }; @@ -198,6 +210,18 @@ export default class Video extends Component { } }; + _onIsPictureInPictureSupported = (event) => { + if (this.props.onIsPictureInPictureSupported) { + this.props.onIsPictureInPictureSupported(event.nativeEvent); + } + }; + + _onIsPictureInPictureActive = (event) => { + if (this.props.onIsPictureInPictureActive) { + this.props.onIsPictureInPictureActive(event.nativeEvent); + } + }; + _onAudioFocusChanged = (event) => { if (this.props.onAudioFocusChanged) { this.props.onAudioFocusChanged(event.nativeEvent); @@ -267,6 +291,8 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, + onIsPictureInPictureSupported: this._onIsPictureInPictureSupported, + onIsPictureInPictureActive: this._onIsPictureInPictureActive, }); const posterStyle = { @@ -420,6 +446,8 @@ Video.propTypes = { onPlaybackRateChange: PropTypes.func, onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, + onIsPictureInPictureSupported: PropTypes.func, + onIsPictureInPictureActive: PropTypes.func, onExternalPlaybackChange: PropTypes.func, /* Required by react-native */ diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 05527a57fe..9c84bc157d 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -16,7 +16,7 @@ #if __has_include() @interface RCTVideo : UIView #else -@interface RCTVideo : UIView +@interface RCTVideo : UIView #endif @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; @@ -38,6 +38,8 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; +@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureSupported; +@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureActive; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 52b0342a6b..c95e59090b 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -27,6 +27,8 @@ @implementation RCTVideo AVPlayer *_player; AVPlayerItem *_playerItem; NSDictionary *_source; + AVPictureInPictureController *_pipController; + void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL); BOOL _playerItemObserversSet; BOOL _playerBufferEmpty; AVPlayerLayer *_playerLayer; @@ -101,6 +103,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _allowsExternalPlayback = YES; _playWhenInactive = false; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey + _restoreUserInterfaceForPIPStopCompletionHandler = NULL; #if __has_include() _videoCache = [RCTVideoCache sharedInstance]; #endif @@ -379,6 +382,12 @@ - (void)setSrc:(NSDictionary *)source @"target": self.reactTag }); } + + if (@available(iOS 9, *)) { + if (self.onIsPictureInPictureSupported) { + self.onIsPictureInPictureSupported(@{@"supported": [NSNumber numberWithBool:(bool)[AVPictureInPictureController isPictureInPictureSupported]]}); + } + } }]; }); _videoLoadStarted = YES; @@ -780,6 +789,37 @@ - (void)setPlayWhenInactive:(BOOL)playWhenInactive _playWhenInactive = playWhenInactive; } +- (void)setPictureInPicture:(BOOL)pictureInPicture +{ + if (_pipController && pictureInPicture && ![_pipController isPictureInPictureActive]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_pipController startPictureInPicture]; + }); + } else if (_pipController && !pictureInPicture && [_pipController isPictureInPictureActive]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [_pipController stopPictureInPicture]; + }); + } +} + +- (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore +{ + if (_restoreUserInterfaceForPIPStopCompletionHandler != NULL) { + _restoreUserInterfaceForPIPStopCompletionHandler(restore); + _restoreUserInterfaceForPIPStopCompletionHandler = NULL; + } +} + +- (void)setupPipController { + if (@available(iOS 9, *)) { + if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { + // Create new controller passing reference to the AVPlayerLayer + _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; + _pipController.delegate = self; + } + } +} + - (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch { _ignoreSilentSwitch = ignoreSilentSwitch; @@ -1234,6 +1274,8 @@ - (void)usePlayerLayer [self.layer addSublayer:_playerLayer]; self.layer.needsDisplayOnBoundsChange = YES; + + [self setupPipController]; } } @@ -1490,4 +1532,35 @@ - (NSString *)cacheDirectoryPath { return array[0]; } +#pragma mark - Picture in Picture + +- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + if (self.onIsPictureInPictureActive && _pipController) { + self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:false]}); + } +} + +- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + if (self.onIsPictureInPictureActive && _pipController) { + self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:true]}); + } +} + +- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + +} + +- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + +} + +- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error { + +} + +- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { + NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited."); + _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler; +} + @end diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 1ca1b5b402..ebb6126b16 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -42,6 +42,8 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); +RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL); +RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock); @@ -77,6 +79,8 @@ - (dispatch_queue_t)methodQueue } }]; } +RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureSupported, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureActive, RCTBubblingEventBlock); - (NSDictionary *)constantsToExport { From 4a1615119554a607c48aabd8d91353a4dda83534 Mon Sep 17 00:00:00 2001 From: Abdulrahman AlZanki Date: Wed, 14 Nov 2018 10:23:56 -0800 Subject: [PATCH 006/261] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e28061fac..9d9e24f266 100644 --- a/README.md +++ b/README.md @@ -867,7 +867,7 @@ Payload: none Platforms: Android ExoPlayer, Android MediaPlayer, iOS #### onIsPictureInPictureActive -Callback function that is called when picture in picture becames active on inactive. +Callback function that is called when picture in picture becomes active or inactive. Property | Type | Description --- | --- | --- From 62dc913cb36f528ccb004cd24071b3299f233b4a Mon Sep 17 00:00:00 2001 From: Abdulrahman Alzenki Date: Mon, 26 Nov 2018 14:23:04 -0800 Subject: [PATCH 007/261] Address some of the feedback from the pull reqeust --- README.md | 101 +++++++++++++----------------------- Video.js | 33 +++++------- ios/Video/RCTVideo.h | 4 +- ios/Video/RCTVideo.m | 42 ++++++++------- ios/Video/RCTVideoManager.m | 6 +-- 5 files changed, 79 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 9d9e24f266..dc90f226a8 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,7 @@ var styles = StyleSheet.create({ * [maxBitRate](#maxbitrate) * [muted](#muted) * [paused](#paused) +* [pictureInPicture](#pictureinpicture) * [playInBackground](#playinbackground) * [playWhenInactive](#playwheninactive) * [poster](#poster) @@ -298,12 +299,12 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent) * [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss) * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) -* [onIsPictureInPictureActive](#onispictureinpictureactive) -* [onIsPictureInPictureSupported](#onispictureinpicturesupported) * [onLoad](#onload) * [onLoadStart](#onloadstart) +* [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) * [onProgress](#onprogress) * [onSeek](#onseek) +* [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop) * [onTimedMetadata](#ontimedmetadata) ### Methods @@ -311,9 +312,7 @@ var styles = StyleSheet.create({ * [presentFullscreenPlayer](#presentfullscreenplayer) * [save](#save) * [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop) -* [startPictureInPicture](#startpictureinpicture) * [seek](#seek) -* [stopPictureInPicture](#stoppictureinpicture) ### Configurable props @@ -491,6 +490,13 @@ Controls whether the media is paused Platforms: all +#### pictureInPicture +Determine whether the media should played as picture in picture. +* **false (default)** - Don't not play as picture in picture +* **true** - Play the media as picture in picture + +Platforms: iOS + #### playInBackground Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio. * **false (default)** - Don't continue playing the media @@ -866,38 +872,6 @@ Payload: none Platforms: Android ExoPlayer, Android MediaPlayer, iOS -#### onIsPictureInPictureActive -Callback function that is called when picture in picture becomes active or inactive. - -Property | Type | Description ---- | --- | --- -active | boolean | Boolean indicating whether picture in picture is active - -Example: -``` -{ - active: true -} -``` - -Platforms: iOS - -#### onIsPictureInPictureSupported -Callback function that is called initially to determine whether or not picture in picture is supported. - -Property | Type | Description ---- | --- | --- -supported | boolean | Boolean indicating whether picture in picture is supported - -Example: -``` -{ - supported: true -} -``` - -Platforms: iOS - #### onLoad Callback function that is called when the media is loaded and ready to play. @@ -963,6 +937,22 @@ Example: Platforms: all +#### onPictureInPictureStatusChanged +Callback function that is called when picture in picture becomes active or inactive. + +Property | Type | Description +--- | --- | --- +isActive | boolean | Boolean indicating whether picture in picture is active + +Example: +``` +{ +isActive: true +} +``` + +Platforms: iOS + #### onProgress Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing. @@ -1006,6 +996,13 @@ Both the currentTime & seekTime are reported because the video player may not se Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP +#### onRestoreUserInterfaceForPictureInPictureStop +Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface. + +Payload: none + +Platforms: iOS + #### onTimedMetadata Callback function that is called when timed metadata becomes available @@ -1094,26 +1091,14 @@ Future: Platforms: iOS -#### restoreUserInterfaceForPictureInPictureStop -`restoreUserInterfaceForPictureInPictureStop(restore)` +#### restoreUserInterfaceForPictureInPictureStopCompleted +`restoreUserInterfaceForPictureInPictureStopCompleted(restored)` -This function corresponds to Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: After picture in picture stops, this function must be called. +This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called. Example: ``` -this.player.restoreUserInterfaceForPictureInPictureStop(true); -``` - -Platforms: iOS - -#### startPictureInPicture -`startPictureInPicture()` - -Calling this function will start picture in picture if it is supported. - -Example: -``` -this.player.startPictureInPicture(); +this.player.restoreUserInterfaceForPictureInPictureStopCompleted(true); ``` Platforms: iOS @@ -1147,18 +1132,6 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac Platforms: iOS -#### stopPictureInPicture -`stopPictureInPicture()` - -Calling this function will stop picture in picture if it is currently active. - -Example: -``` -this.player.stopPictureInPicture(); -``` - -Platforms: iOS - diff --git a/Video.js b/Video.js index 42e9731c93..f33d102fba 100644 --- a/Video.js +++ b/Video.js @@ -78,16 +78,8 @@ export default class Video extends Component { return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); } - startPictureInPicture = () => { - this.setNativeProps({ pictureInPicture: true }); - }; - - stopPictureInPicture = () => { - this.setNativeProps({ pictureInPicture: false }); - }; - - restoreUserInterfaceForPictureInPictureStop = (restore) => { - this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restore }); + restoreUserInterfaceForPictureInPictureStopCompleted = (restored) => { + this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restored }); }; _assignRoot = (component) => { @@ -210,15 +202,15 @@ export default class Video extends Component { } }; - _onIsPictureInPictureSupported = (event) => { - if (this.props.onIsPictureInPictureSupported) { - this.props.onIsPictureInPictureSupported(event.nativeEvent); + _onPictureInPictureStatusChanged = (event) => { + if (this.props.onPictureInPictureStatusChanged) { + this.props.onPictureInPictureStatusChanged(event.nativeEvent); } }; - _onIsPictureInPictureActive = (event) => { - if (this.props.onIsPictureInPictureActive) { - this.props.onIsPictureInPictureActive(event.nativeEvent); + _onRestoreUserInterfaceForPictureInPictureStop = (event) => { + if (this.props.onRestoreUserInterfaceForPictureInPictureStop) { + this.props.onRestoreUserInterfaceForPictureInPictureStop(); } }; @@ -291,8 +283,8 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, - onIsPictureInPictureSupported: this._onIsPictureInPictureSupported, - onIsPictureInPictureActive: this._onIsPictureInPictureActive, + onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, + onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, }); const posterStyle = { @@ -415,6 +407,7 @@ Video.propTypes = { }), stereoPan: PropTypes.number, rate: PropTypes.number, + pictureInPicture: PropTypes.bool, playInBackground: PropTypes.bool, playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), @@ -446,8 +439,8 @@ Video.propTypes = { onPlaybackRateChange: PropTypes.func, onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, - onIsPictureInPictureSupported: PropTypes.func, - onIsPictureInPictureActive: PropTypes.func, + onPictureInPictureStatusChanged: PropTypes.func, + needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func, onExternalPlaybackChange: PropTypes.func, /* Required by react-native */ diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 9c84bc157d..ad98db7ffa 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -38,8 +38,8 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; -@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureSupported; -@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureActive; +@property (nonatomic, copy) RCTBubblingEventBlock onPictureInPictureStatusChanged; +@property (nonatomic, copy) RCTBubblingEventBlock onRestoreUserInterfaceForPictureInPictureStop; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index c95e59090b..60a42bc9b7 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -66,6 +66,7 @@ @implementation RCTVideo BOOL _playbackStalled; BOOL _playInBackground; BOOL _playWhenInactive; + BOOL _pictureInPicture; NSString * _ignoreSilentSwitch; NSString * _resizeMode; BOOL _fullscreen; @@ -102,6 +103,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _playInBackground = false; _allowsExternalPlayback = YES; _playWhenInactive = false; + _pictureInPicture = false; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey _restoreUserInterfaceForPIPStopCompletionHandler = NULL; #if __has_include() @@ -382,12 +384,6 @@ - (void)setSrc:(NSDictionary *)source @"target": self.reactTag }); } - - if (@available(iOS 9, *)) { - if (self.onIsPictureInPictureSupported) { - self.onIsPictureInPictureSupported(@{@"supported": [NSNumber numberWithBool:(bool)[AVPictureInPictureController isPictureInPictureSupported]]}); - } - } }]; }); _videoLoadStarted = YES; @@ -791,11 +787,16 @@ - (void)setPlayWhenInactive:(BOOL)playWhenInactive - (void)setPictureInPicture:(BOOL)pictureInPicture { - if (_pipController && pictureInPicture && ![_pipController isPictureInPictureActive]) { + if (_pictureInPicture == pictureInPicture) { + return; + } + + _pictureInPicture = pictureInPicture; + if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController startPictureInPicture]; }); - } else if (_pipController && !pictureInPicture && [_pipController isPictureInPictureActive]) { + } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController stopPictureInPicture]; }); @@ -811,12 +812,10 @@ - (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore } - (void)setupPipController { - if (@available(iOS 9, *)) { - if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { - // Create new controller passing reference to the AVPlayerLayer - _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; - _pipController.delegate = self; - } + if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { + // Create new controller passing reference to the AVPlayerLayer + _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; + _pipController.delegate = self; } } @@ -1535,14 +1534,18 @@ - (NSString *)cacheDirectoryPath { #pragma mark - Picture in Picture - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onIsPictureInPictureActive && _pipController) { - self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:false]}); + if (self.onPictureInPictureStatusChanged) { + self.onPictureInPictureStatusChanged(@{ + @"isActive": [NSNumber numberWithBool:false] + }); } } - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onIsPictureInPictureActive && _pipController) { - self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:true]}); + if (self.onPictureInPictureStatusChanged) { + self.onPictureInPictureStatusChanged(@{ + @"isActive": [NSNumber numberWithBool:true] + }); } } @@ -1560,6 +1563,9 @@ - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPict - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited."); + if (self.onRestoreUserInterfaceForPictureInPictureStop) { + self.onRestoreUserInterfaceForPictureInPictureStop(@{}); + } _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler; } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index ebb6126b16..a608f32e6b 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -32,6 +32,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(volume, float); RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL); RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL); +RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL); RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); @@ -42,7 +43,6 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); -RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); @@ -79,8 +79,8 @@ - (dispatch_queue_t)methodQueue } }]; } -RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureSupported, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureActive, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); - (NSDictionary *)constantsToExport { From 8f29f85c19ae321ee8c9d2d8cbb59c372c2bfce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 15 Jan 2019 09:20:48 +0100 Subject: [PATCH 008/261] preparing DRM --- ios/Video/RCTVideo.h | 4 +++- ios/Video/RCTVideo.m | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 05527a57fe..cecf9a3239 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -16,7 +16,7 @@ #if __has_include() @interface RCTVideo : UIView #else -@interface RCTVideo : UIView +@interface RCTVideo : UIView #endif @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; @@ -45,4 +45,6 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 6_0); + @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 52b0342a6b..0bea1912a0 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -33,6 +33,9 @@ @implementation RCTVideo BOOL _playerLayerObserverSet; RCTVideoPlayerViewController *_playerViewController; NSURL *_videoURL; + + /* DRM */ + NSDictionary *_drm; /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; @@ -372,9 +375,11 @@ - (void)setSrc:(NSDictionary *)source if (self.onVideoLoadStart) { id uri = [source objectForKey:@"uri"]; id type = [source objectForKey:@"type"]; + _drm = [source objectForKey:@"drm"]; self.onVideoLoadStart(@{@"src": @{ @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], + @"drm": drm ? drm : [NSNull null], @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, @"target": self.reactTag }); @@ -465,6 +470,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; + NSDictionary *drm = [source objectForKey:@"drm"]; NSURL *url = isNetwork || isAsset ? [NSURL URLWithString:uri] @@ -1490,4 +1496,33 @@ - (NSString *)cacheDirectoryPath { return array[0]; } +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { + NSURL *url = loadingRequest.request.URL; + NSString *identifier = url.host; + if (_fairplayCertificate != nil && _contentId != nil) { + NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; + NSData* certificateData = [_fairplayCertificate dataUsingEncoding:NSUTF8StringEncoding]; + NSError* error = nil; + [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&error]; + } +} + +- (NSString *) getDataFrom:(NSData *)url{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"GET"]; + [request setURL:[NSURL URLWithString:url]]; + + NSError *error = nil; + NSHTTPURLResponse *responseCode = nil; + + NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; + + if([responseCode statusCode] != 200){ + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + return nil; + } + + return oResponseData; +} + @end From e8da925d84743542a5a3673f33b6946e8c437fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 16 Jan 2019 13:06:31 +0100 Subject: [PATCH 009/261] remove AVAssetResourceLoaderDelegate method from .h --- ios/Video/RCTVideo.h | 4 +--- ios/Video/RCTVideo.m | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index cecf9a3239..9c2b7ceb97 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -14,7 +14,7 @@ @class RCTEventDispatcher; #if __has_include() -@interface RCTVideo : UIView +@interface RCTVideo : UIView #else @interface RCTVideo : UIView #endif @@ -45,6 +45,4 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 6_0); - @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0bea1912a0..deb4d63fb8 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1496,6 +1496,8 @@ - (NSString *)cacheDirectoryPath { return array[0]; } +#pragma mark - AVAssetResourceLoaderDelegate + - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { NSURL *url = loadingRequest.request.URL; NSString *identifier = url.host; From 7b63331e51c4b0711942c34cf614f2b8a9601475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 16 Jan 2019 14:54:12 +0100 Subject: [PATCH 010/261] comment to test building process --- ios/Video/RCTVideo.m | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index deb4d63fb8..6d60faf045 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1501,30 +1501,30 @@ - (NSString *)cacheDirectoryPath { - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { NSURL *url = loadingRequest.request.URL; NSString *identifier = url.host; - if (_fairplayCertificate != nil && _contentId != nil) { - NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; - NSData* certificateData = [_fairplayCertificate dataUsingEncoding:NSUTF8StringEncoding]; - NSError* error = nil; - [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&error]; - } + // if (_fairplayCertificate != nil && _contentId != nil) { + // NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; + // NSData* certificateData = [_fairplayCertificate dataUsingEncoding:NSUTF8StringEncoding]; + // NSError* error = nil; + // [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&error]; + // } } -- (NSString *) getDataFrom:(NSData *)url{ - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"GET"]; - [request setURL:[NSURL URLWithString:url]]; +// - (NSString *) getDataFrom:(NSData *)url{ +// NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; +// [request setHTTPMethod:@"GET"]; +// [request setURL:[NSURL URLWithString:url]]; - NSError *error = nil; - NSHTTPURLResponse *responseCode = nil; +// NSError *error = nil; +// NSHTTPURLResponse *responseCode = nil; - NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; +// NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; - if([responseCode statusCode] != 200){ - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); - return nil; - } +// if([responseCode statusCode] != 200){ +// NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); +// return nil; +// } - return oResponseData; -} +// return oResponseData; +// } @end From 34654f7b6eb7ac95be77ffb26a9668518e5a5f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 16 Jan 2019 15:16:50 +0100 Subject: [PATCH 011/261] fixes --- ios/Video/RCTVideo.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 6d60faf045..050389ea6f 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -379,7 +379,7 @@ - (void)setSrc:(NSDictionary *)source self.onVideoLoadStart(@{@"src": @{ @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], - @"drm": drm ? drm : [NSNull null], + @"drm": _drm ? _drm : [NSNull null], @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, @"target": self.reactTag }); @@ -1501,6 +1501,7 @@ - (NSString *)cacheDirectoryPath { - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { NSURL *url = loadingRequest.request.URL; NSString *identifier = url.host; + return true; // if (_fairplayCertificate != nil && _contentId != nil) { // NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; // NSData* certificateData = [_fairplayCertificate dataUsingEncoding:NSUTF8StringEncoding]; From e05c1c3c64c547439dc23f4c233e8ac537e2d8ac Mon Sep 17 00:00:00 2001 From: Ibrahim Sulaiman Date: Wed, 16 Jan 2019 23:47:32 +0530 Subject: [PATCH 012/261] Resolved the review comments --- README.md | 2 +- .../com/brentvatne/exoplayer/ReactExoplayerView.java | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 708afab454..546244cd45 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ Note on iOS, controls are always shown when in fullscreen mode. Controls are not available Android because the system does not provide a stock set of controls. You will need to build your own or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-video-player](https://github.com/cornedor/react-native-video-player). -Platforms: iOS, Android, react-native-dom +Platforms: iOS, Android ExoPlayer, react-native-dom #### filter Add video filter diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 1c80c90945..3d500d1726 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -64,7 +64,6 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -//Import PlayerControlView import com.google.android.exoplayer2.ui.PlayerControlView; import java.net.CookieHandler; @@ -98,7 +97,6 @@ class ReactExoplayerView extends FrameLayout implements } private final VideoEventEmitter eventEmitter; - //Create playerControlView instance private PlayerControlView playerControlView; private Handler mainHandler; @@ -274,9 +272,9 @@ private void togglePlayerControlVisibility() { } /** - * Initialising Player control + * Initializing Player control */ - private void initialisePlayerControl() { + private void initializePlayerControl() { playerControlView = new PlayerControlView(getContext()); LayoutParams layoutParams = new LayoutParams( LayoutParams.MATCH_PARENT, @@ -1107,8 +1105,8 @@ public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBuffe */ public void setControls(boolean controls) { if(controls && (exoPlayerView != null)) { - //Initialise playerControlView - initialisePlayerControl(); + //Initialize playerControlView + initializePlayerControl(); } } } From bceefc65966e9cc548dedd8d794734f29904ae71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 17 Jan 2019 15:43:47 +0100 Subject: [PATCH 013/261] allow DRM object --- Video.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Video.js b/Video.js index b05238a939..9090ad63c8 100644 --- a/Video.js +++ b/Video.js @@ -244,7 +244,8 @@ 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) : {}, + drm: source.drm }, onVideoLoadStart: this._onLoadStart, onVideoLoad: this._onLoad, From e65aa2c0ae301e7d5889752592a8daa797547698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 17 Jan 2019 16:00:12 +0100 Subject: [PATCH 014/261] content id override --- ios/Video/RCTVideo.m | 276 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 21 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 050389ea6f..29c7309bec 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -471,6 +471,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; NSDictionary *drm = [source objectForKey:@"drm"]; + AVURLAsset *asset; NSURL *url = isNetwork || isAsset ? [NSURL URLWithString:uri] @@ -500,16 +501,15 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye } #endif - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; - return; + asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; } else if (isAsset) { - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; - return; + asset = [AVURLAsset URLAssetWithURL:url options:nil]; + } else { + asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; } + dispatch_queue_t queue = dispatch_queue_create("recordingQueue", nil); + [asset.resourceLoader setDelegate:self queue:queue]; - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; } @@ -1500,7 +1500,93 @@ - (NSString *)cacheDirectoryPath { - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { NSURL *url = loadingRequest.request.URL; - NSString *identifier = url.host; + NSString *contentId = url.host; + if (_drm != nil) { + NSString *contentIdOverride = (NSString *)[_drm objectForKey:@"contentId"]; + if (contentIdOverride != nil) { + contentId = contentIdOverride; + } + NSString *drmType = (NSString *)[_drm objectForKey:@"type"]; + if ([drmType isEqualToString:@"fairplay"]) { + NSString *certificateStringUrl = (NSString *)[_drm objectForKey:@"certificateUrl"]; + if (certificateStringUrl != nil) { + NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; + if (certificateData != nil) { + NSData *contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding]; + AVAssetResourceLoadingDataRequest *dataRequest = [loadingRequest dataRequest]; + if (dataRequest != nil) { + NSError *spcError = nil; + NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; + // Request CKC to the server + NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; + if (licenseServer != nil && spcData != nil) { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:licenseServer]]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + NSString *spcData64 = [self base64forData:spcData]; + + NSDictionary *jsonBodyDict = @{@"releasePid":contentId, @"spcMessage":spcData64}; + NSDictionary *getLicenseBody = @{@"getFairplayLicense":jsonBodyDict}; + + [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:getLicenseBody options:kNilOptions error:nil]]; + + NSError *error = nil; + NSHTTPURLResponse *responseCode = nil; + + NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; + + if([responseCode statusCode] != 200){ + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + return nil; + } + + if (oResponseData != nil) { + // The CKC is correctly returned and is now send to the `AVPlayer` instance so we + // can continue to play the stream. + NSError* error; + NSDictionary* json = [NSJSONSerialization JSONObjectWithData:oResponseData + options:kNilOptions + error:&error]; + + NSDictionary* getFairplayLicenseResponse = [json objectForKey:@"getFairplayLicenseResponse"]; + NSString* ckcResponse = [getFairplayLicenseResponse objectForKey:@"ckcResponse"]; + NSData *respondData = [self base64DataFromString:ckcResponse]; + + [dataRequest respondWithData:respondData]; + [loadingRequest finishLoading]; + } + + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + + return true; // if (_fairplayCertificate != nil && _contentId != nil) { // NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; @@ -1510,22 +1596,170 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad // } } -// - (NSString *) getDataFrom:(NSData *)url{ -// NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; -// [request setHTTPMethod:@"GET"]; -// [request setURL:[NSURL URLWithString:url]]; +- (NSString*)base64forData:(NSData*)theData { + const uint8_t* input = (const uint8_t*)[theData bytes]; + NSInteger length = [theData length]; + + static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + NSMutableData* data = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; + uint8_t* output = (uint8_t*)data.mutableBytes; + + NSInteger i; + for (i=0; i < length; i += 3) { + NSInteger value = 0; + NSInteger j; + for (j = i; j < (i + 3); j++) { + value <<= 8; + + if (j < length) { + value |= (0xFF & input[j]); + } + } + + NSInteger theIndex = (i / 3) * 4; + output[theIndex + 0] = table[(value >> 18) & 0x3F]; + output[theIndex + 1] = table[(value >> 12) & 0x3F]; + output[theIndex + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; + output[theIndex + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; + } + + return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; +} -// NSError *error = nil; -// NSHTTPURLResponse *responseCode = nil; +- (NSData *)base64DataFromString: (NSString *)string +{ + unsigned long ixtext, lentext; + unsigned char ch, inbuf[4], outbuf[3]; + short i, ixinbuf; + Boolean flignore, flendtext = false; + const unsigned char *tempcstring; + NSMutableData *theData; + + if (string == nil) + { + return [NSData data]; + } + + ixtext = 0; + + tempcstring = (const unsigned char *)[string UTF8String]; + + lentext = [string length]; + + theData = [NSMutableData dataWithCapacity: lentext]; + + ixinbuf = 0; + + while (true) + { + if (ixtext >= lentext) + { + break; + } + + ch = tempcstring [ixtext++]; + + flignore = false; + + if ((ch >= 'A') && (ch <= 'Z')) + { + ch = ch - 'A'; + } + else if ((ch >= 'a') && (ch <= 'z')) + { + ch = ch - 'a' + 26; + } + else if ((ch >= '0') && (ch <= '9')) + { + ch = ch - '0' + 52; + } + else if (ch == '+') + { + ch = 62; + } + else if (ch == '=') + { + flendtext = true; + } + else if (ch == '/') + { + ch = 63; + } + else + { + flignore = true; + } + + if (!flignore) + { + short ctcharsinbuf = 3; + Boolean flbreak = false; + + if (flendtext) + { + if (ixinbuf == 0) + { + break; + } + + if ((ixinbuf == 1) || (ixinbuf == 2)) + { + ctcharsinbuf = 1; + } + else + { + ctcharsinbuf = 2; + } + + ixinbuf = 3; + + flbreak = true; + } + + inbuf [ixinbuf++] = ch; + + if (ixinbuf == 4) + { + ixinbuf = 0; + + outbuf[0] = (inbuf[0] << 2) | ((inbuf[1] & 0x30) >> 4); + outbuf[1] = ((inbuf[1] & 0x0F) << 4) | ((inbuf[2] & 0x3C) >> 2); + outbuf[2] = ((inbuf[2] & 0x03) << 6) | (inbuf[3] & 0x3F); + + for (i = 0; i < ctcharsinbuf; i++) + { + [theData appendBytes: &outbuf[i] length: 1]; + } + } + + if (flbreak) + { + break; + } + } + } + + return theData; +} + +- (NSData *) postDataTo:(NSURL *)url andData:(NSData *)data { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:url]]; + [request setHTTPBody:data]; -// NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; + NSError *error = nil; + NSHTTPURLResponse *responseCode = nil; -// if([responseCode statusCode] != 200){ -// NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); -// return nil; -// } + NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; + + if([responseCode statusCode] != 200){ + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + return nil; + } -// return oResponseData; -// } + return oResponseData; + } @end From d2c2cdc3fc36b353a015b289940a32da4a0a9adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 18 Jan 2019 11:07:53 +0100 Subject: [PATCH 015/261] WIP cb --- Video.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Video.js b/Video.js index 9090ad63c8..681c375b0e 100644 --- a/Video.js +++ b/Video.js @@ -210,6 +210,15 @@ export default class Video extends Component { } }; + _onGetLicense = (getLicense) => { + if (getLicense instanceof Function) { + const result = getLicense(); + if (result !== undefined) { + return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); + } + } + } + render() { const resizeMode = this.props.resizeMode; const source = resolveAssetSource(this.props.source) || {}; @@ -270,6 +279,10 @@ export default class Video extends Component { onAudioBecomingNoisy: this._onAudioBecomingNoisy, }); + if (nativeProps.src && nativeProps.src.drm && nativeProps.src.drm.getLicense) { + this._onGetLicense(nativeProps.src.drm.getLicense) + } + const posterStyle = { ...StyleSheet.absoluteFillObject, resizeMode: this.props.posterResizeMode || 'contain', From d2628015d52dc0b38ba2810bd048667e3fd90ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 18 Jan 2019 11:12:17 +0100 Subject: [PATCH 016/261] WIP abstraction --- ios/Video/RCTVideo.m | 86 ++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 29c7309bec..ac4dc7a4fa 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -507,7 +507,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye } else { asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; } - dispatch_queue_t queue = dispatch_queue_create("recordingQueue", nil); + dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); [asset.resourceLoader setDelegate:self queue:queue]; [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; @@ -1520,43 +1520,61 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; - if (licenseServer != nil && spcData != nil) { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"POST"]; - [request setURL:[NSURL URLWithString:licenseServer]]; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - NSString *spcData64 = [self base64forData:spcData]; - - NSDictionary *jsonBodyDict = @{@"releasePid":contentId, @"spcMessage":spcData64}; - NSDictionary *getLicenseBody = @{@"getFairplayLicense":jsonBodyDict}; - - [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:getLicenseBody options:kNilOptions error:nil]]; - - NSError *error = nil; - NSHTTPURLResponse *responseCode = nil; - - NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; - - if([responseCode statusCode] != 200){ - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); - return nil; - } - - if (oResponseData != nil) { - // The CKC is correctly returned and is now send to the `AVPlayer` instance so we - // can continue to play the stream. - NSError* error; - NSDictionary* json = [NSJSONSerialization JSONObjectWithData:oResponseData - options:kNilOptions - error:&error]; + NSString *getLicenseMethod = (NSString *)[_drm objectForKey:@"getLicense"]; + if (spcData != nil && licenseServer != nil || getLicenseMethod != nil) { + NSData *respondData; + if (getLicenseMethod != nil) { - NSDictionary* getFairplayLicenseResponse = [json objectForKey:@"getFairplayLicenseResponse"]; - NSString* ckcResponse = [getFairplayLicenseResponse objectForKey:@"ckcResponse"]; - NSData *respondData = [self base64DataFromString:ckcResponse]; + } else { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:licenseServer]]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + NSString *spcData64 = [self base64forData:spcData]; +// PFX specific +// NSDictionary *jsonBodyDict = @{@"releasePid":contentId, @"spcMessage":spcData64}; +// NSDictionary *getLicenseBody = @{@"getFairplayLicense":jsonBodyDict}; + + [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:spcData64 options:kNilOptions error:nil]]; + + NSError *error = nil; + NSHTTPURLResponse *responseCode = nil; + // TODO: async + NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; + + if([responseCode statusCode] != 200){ + // TODO: Error + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + [loadingRequest finishLoadingWithError:nil]; + return false; + } + respondData = oResponseData; + } + +// if (oResponseData != nil) { +// // The CKC is correctly returned and is now send to the `AVPlayer` instance so we +// // can continue to play the stream. +// NSError* error; +// NSDictionary* json = [NSJSONSerialization JSONObjectWithData:oResponseData +// options:kNilOptions +// error:&error]; +// +// NSDictionary* getFairplayLicenseResponse = [json objectForKey:@"getFairplayLicenseResponse"]; +// NSString* ckcResponse = [getFairplayLicenseResponse objectForKey:@"ckcResponse"]; +// respondData = [self base64DataFromString:ckcResponse]; +// +// [dataRequest respondWithData:respondData]; +// [loadingRequest finishLoading]; +// } + + if (respondData != nil) { [dataRequest respondWithData:respondData]; [loadingRequest finishLoading]; + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; } } else { From 8d360fda3ae093fb1425703dd065082e47f4b0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 21 Jan 2019 10:05:24 +0100 Subject: [PATCH 017/261] overrridable getLicense --- Video.js | 13 ++++++++----- ios/Video/RCTVideo.m | 19 +++++++++++++------ ios/Video/RCTVideoManager.m | 13 +++++++++++++ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Video.js b/Video.js index 681c375b0e..f9ab1534dd 100644 --- a/Video.js +++ b/Video.js @@ -211,11 +211,14 @@ export default class Video extends Component { }; _onGetLicense = (getLicense) => { - if (getLicense instanceof Function) { - const result = getLicense(); - if (result !== undefined) { - return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); - } + if (this.props.source && this.props.source.drm && this.props.source.drm.getLicense instanceof Function) { + const getLicenseOverride = this.props.source.drm.getLicense(); + const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. + getLicensePromise.then((result => { + if (result !== undefined) { + NativeModules.VideoManager.setLicense(result, findNodeHandle(this._root)); + } + })); } } diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index ac4dc7a4fa..9cadf56b31 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1472,6 +1472,10 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve rej } } +- (void)setLicenseResult:(NSString * )license { + +} + - (BOOL)ensureDirExistsWithPath:(NSString *)path { BOOL isDir = NO; NSError *error; @@ -1521,23 +1525,26 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; NSString *getLicenseMethod = (NSString *)[_drm objectForKey:@"getLicense"]; - if (spcData != nil && licenseServer != nil || getLicenseMethod != nil) { + if (spcData != nil && (licenseServer != nil || getLicenseMethod != nil)) { NSData *respondData; if (getLicenseMethod != nil) { - + NSString* spcStr = [[NSString alloc] initWithData:spcData encoding:NSUTF8StringEncoding]; + getLicenseMethod(spcStr); + return true; } else { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setHTTPMethod:@"POST"]; [request setURL:[NSURL URLWithString:licenseServer]]; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - NSString *spcData64 = [self base64forData:spcData]; // PFX specific + // [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + // [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + // NSString *spcData64 = [self base64forData:spcData]; // NSDictionary *jsonBodyDict = @{@"releasePid":contentId, @"spcMessage":spcData64}; // NSDictionary *getLicenseBody = @{@"getFairplayLicense":jsonBodyDict}; - [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:spcData64 options:kNilOptions error:nil]]; + // [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:spcData64 options:kNilOptions error:nil]]; + [request setHTTPBody: spcData]; NSError *error = nil; NSHTTPURLResponse *responseCode = nil; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 1ca1b5b402..f5f83f9ae1 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -77,6 +77,19 @@ - (dispatch_queue_t)methodQueue } }]; } +RCT_REMAP_METHOD(setLicenseResult, + license:(NSString *)license), + reactTag:(nonnull NSNumber *)reactTag +{ + [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTVideo *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTVideo class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); + } else { + [view setLicenseResult:result]; + } + }]; +} - (NSDictionary *)constantsToExport { From 69f7f3824f3ba8bc2b9ad113d8cc8800bacad1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 21 Jan 2019 10:17:49 +0100 Subject: [PATCH 018/261] to async method --- ios/Video/RCTVideo.m | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9cadf56b31..5b6ed9e80c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1545,7 +1545,33 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad // [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:spcData64 options:kNilOptions error:nil]]; [request setHTTPBody: spcData]; - + + NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error != nil) { + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + [loadingRequest finishLoadingWithError:nil]; + return false; + } else { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; + if([httpResponse statusCode] != 200){ + // TODO: Error + NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + [loadingRequest finishLoadingWithError:nil]; + return false; + } + respondData = data; + if (respondData != nil) { + [dataRequest respondWithData:respondData]; + [loadingRequest finishLoading]; + } else { + [loadingRequest finishLoadingWithError:nil]; + return false; + } + + } + }]; + [postDataTask resume]; + return true; NSError *error = nil; NSHTTPURLResponse *responseCode = nil; // TODO: async @@ -1576,14 +1602,6 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad // [loadingRequest finishLoading]; // } - if (respondData != nil) { - [dataRequest respondWithData:respondData]; - [loadingRequest finishLoading]; - } else { - [loadingRequest finishLoadingWithError:nil]; - return false; - } - } else { [loadingRequest finishLoadingWithError:nil]; return false; From aa076188d6aa0293643a5c24e5a29d86a13e0f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 21 Jan 2019 17:10:38 +0100 Subject: [PATCH 019/261] license override --- Video.js | 33 ++++---- ios/Video/RCTVideo.h | 2 + ios/Video/RCTVideo.m | 165 ++++++++++++------------------------ ios/Video/RCTVideoManager.m | 11 +-- 4 files changed, 81 insertions(+), 130 deletions(-) diff --git a/Video.js b/Video.js index f9ab1534dd..11e9e64b16 100644 --- a/Video.js +++ b/Video.js @@ -210,15 +210,22 @@ export default class Video extends Component { } }; - _onGetLicense = (getLicense) => { + _onGetLicense = (event) => { if (this.props.source && this.props.source.drm && this.props.source.drm.getLicense instanceof Function) { - const getLicenseOverride = this.props.source.drm.getLicense(); - const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. - getLicensePromise.then((result => { - if (result !== undefined) { - NativeModules.VideoManager.setLicense(result, findNodeHandle(this._root)); - } - })); + const data = event.nativeEvent; + if (data && data.spc) { + const getLicenseOverride = this.props.source.drm.getLicense(data.spc, 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)); + } + })).catch((error) => { + // NativeModules.VideoManager.setLicenseError(result, findNodeHandle(this._root)); + }); + } + } else { + // NativeModules.VideoManager.setLicenseError(result, findNodeHandle(this._root)); } } @@ -280,12 +287,9 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, + onGetLicense: this._onGetLicense, }); - - if (nativeProps.src && nativeProps.src.drm && nativeProps.src.drm.getLicense) { - this._onGetLicense(nativeProps.src.drm.getLicense) - } - + const posterStyle = { ...StyleSheet.absoluteFillObject, resizeMode: this.props.posterResizeMode || 'contain', @@ -406,9 +410,6 @@ Video.propTypes = { }), stereoPan: PropTypes.number, rate: PropTypes.number, - drmUrl: PropTypes.string, - drmName: PropTypes.string, - drmHeader: PropTypes.object, playInBackground: PropTypes.bool, playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 9c2b7ceb97..8e5b751ac1 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -38,11 +38,13 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; +@property (nonatomic, copy) RCTBubblingEventBlock onGetLicense; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void)setLicenseResult:(NSString * )license; @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 5b6ed9e80c..2fa1e2fde2 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -36,6 +36,7 @@ @implementation RCTVideo /* DRM */ NSDictionary *_drm; + AVAssetResourceLoadingRequest *_loadingRequest; /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; @@ -1473,7 +1474,12 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve rej } - (void)setLicenseResult:(NSString * )license { - + NSData *respondData = [self base64DataFromString:license]; + if (_loadingRequest) { + AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; + [dataRequest respondWithData:respondData]; + [_loadingRequest finishLoading]; + } } - (BOOL)ensureDirExistsWithPath:(NSString *)path { @@ -1503,6 +1509,7 @@ - (NSString *)cacheDirectoryPath { #pragma mark - AVAssetResourceLoaderDelegate - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { + _loadingRequest = loadingRequest; NSURL *url = loadingRequest.request.URL; NSString *contentId = url.host; if (_drm != nil) { @@ -1524,84 +1531,49 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; - NSString *getLicenseMethod = (NSString *)[_drm objectForKey:@"getLicense"]; - if (spcData != nil && (licenseServer != nil || getLicenseMethod != nil)) { - NSData *respondData; - if (getLicenseMethod != nil) { - NSString* spcStr = [[NSString alloc] initWithData:spcData encoding:NSUTF8StringEncoding]; - getLicenseMethod(spcStr); + if (spcError != nil) { + //TODO: Error + [loadingRequest finishLoadingWithError:nil]; + return false; + } + if (spcData != nil) { + if(self.onGetLicense) { + NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; + self.onGetLicense(@{@"spc": spcStr, + @"target": self.reactTag}); return true; } else { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setHTTPMethod:@"POST"]; [request setURL:[NSURL URLWithString:licenseServer]]; - -// PFX specific - // [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - // [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - // NSString *spcData64 = [self base64forData:spcData]; -// NSDictionary *jsonBodyDict = @{@"releasePid":contentId, @"spcMessage":spcData64}; -// NSDictionary *getLicenseBody = @{@"getFairplayLicense":jsonBodyDict}; - - // [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:spcData64 options:kNilOptions error:nil]]; - [request setHTTPBody: spcData]; + [request setHTTPBody: spcData]; + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if (error != nil) { - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + NSLog(@"Error getting %@, HTTP status code %i", url, [httpResponse statusCode]); [loadingRequest finishLoadingWithError:nil]; - return false; } else { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if([httpResponse statusCode] != 200){ // TODO: Error - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); + NSLog(@"Error getting %@, HTTP status code %i", url, [httpResponse statusCode]); [loadingRequest finishLoadingWithError:nil]; - return false; } - respondData = data; - if (respondData != nil) { - [dataRequest respondWithData:respondData]; + if (data != nil) { + [dataRequest respondWithData:data]; [loadingRequest finishLoading]; } else { [loadingRequest finishLoadingWithError:nil]; - return false; } } }]; [postDataTask resume]; return true; - NSError *error = nil; - NSHTTPURLResponse *responseCode = nil; - // TODO: async - NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; - - if([responseCode statusCode] != 200){ - // TODO: Error - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); - [loadingRequest finishLoadingWithError:nil]; - return false; - } - respondData = oResponseData; } -// if (oResponseData != nil) { -// // The CKC is correctly returned and is now send to the `AVPlayer` instance so we -// // can continue to play the stream. -// NSError* error; -// NSDictionary* json = [NSJSONSerialization JSONObjectWithData:oResponseData -// options:kNilOptions -// error:&error]; -// -// NSDictionary* getFairplayLicenseResponse = [json objectForKey:@"getFairplayLicenseResponse"]; -// NSString* ckcResponse = [getFairplayLicenseResponse objectForKey:@"ckcResponse"]; -// respondData = [self base64DataFromString:ckcResponse]; -// -// [dataRequest respondWithData:respondData]; -// [loadingRequest finishLoading]; -// } - } else { [loadingRequest finishLoadingWithError:nil]; return false; @@ -1631,43 +1603,6 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad return true; - // if (_fairplayCertificate != nil && _contentId != nil) { - // NSData* contentIdData = [_contentId dataUsingEncoding:NSUTF8StringEncoding]; - // NSData* certificateData = [_fairplayCertificate dataUsingEncoding:NSUTF8StringEncoding]; - // NSError* error = nil; - // [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&error]; - // } -} - -- (NSString*)base64forData:(NSData*)theData { - const uint8_t* input = (const uint8_t*)[theData bytes]; - NSInteger length = [theData length]; - - static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - NSMutableData* data = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; - uint8_t* output = (uint8_t*)data.mutableBytes; - - NSInteger i; - for (i=0; i < length; i += 3) { - NSInteger value = 0; - NSInteger j; - for (j = i; j < (i + 3); j++) { - value <<= 8; - - if (j < length) { - value |= (0xFF & input[j]); - } - } - - NSInteger theIndex = (i / 3) * 4; - output[theIndex + 0] = table[(value >> 18) & 0x3F]; - output[theIndex + 1] = table[(value >> 12) & 0x3F]; - output[theIndex + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; - output[theIndex + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; - } - - return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; } - (NSData *)base64DataFromString: (NSString *)string @@ -1786,23 +1721,35 @@ - (NSData *)base64DataFromString: (NSString *)string return theData; } -- (NSData *) postDataTo:(NSURL *)url andData:(NSData *)data { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"POST"]; - [request setURL:[NSURL URLWithString:url]]; - [request setHTTPBody:data]; - - NSError *error = nil; - NSHTTPURLResponse *responseCode = nil; - - NSData *oResponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&responseCode error:&error]; - - if([responseCode statusCode] != 200){ - NSLog(@"Error getting %@, HTTP status code %i", url, [responseCode statusCode]); - return nil; - } - - return oResponseData; - } +- (NSString*)base64forData:(NSData*)theData { + const uint8_t* input = (const uint8_t*)[theData bytes]; + NSInteger length = [theData length]; + + static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + NSMutableData* data = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; + uint8_t* output = (uint8_t*)data.mutableBytes; + + NSInteger i; + for (i=0; i < length; i += 3) { + NSInteger value = 0; + NSInteger j; + for (j = i; j < (i + 3); j++) { + value <<= 8; + + if (j < length) { + value |= (0xFF & input[j]); + } + } + + NSInteger theIndex = (i / 3) * 4; + output[theIndex + 0] = table[(value >> 18) & 0x3F]; + output[theIndex + 1] = table[(value >> 12) & 0x3F]; + output[theIndex + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; + output[theIndex + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; + } + + return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; +} @end diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index f5f83f9ae1..084b2eabba 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -62,6 +62,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTBubblingEventBlock); RCT_REMAP_METHOD(save, options:(NSDictionary *)options reactTag:(nonnull NSNumber *)reactTag @@ -76,20 +77,20 @@ - (dispatch_queue_t)methodQueue [view save:options resolve:resolve reject:reject]; } }]; -} +}; RCT_REMAP_METHOD(setLicenseResult, - license:(NSString *)license), - reactTag:(nonnull NSNumber *)reactTag + license:(NSString *)license + reactTag:(nonnull NSNumber *)reactTag) { [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTVideo *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTVideo class]]) { RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); } else { - [view setLicenseResult:result]; + [view setLicenseResult:license]; } }]; -} +}; - (NSDictionary *)constantsToExport { From 9160c0bd777ac72faeade069d73d51008847035a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 21 Jan 2019 18:32:25 +0100 Subject: [PATCH 020/261] error handling --- Video.js | 6 ++- ios/Video/RCTVideo.m | 112 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/Video.js b/Video.js index 11e9e64b16..7615a450a9 100644 --- a/Video.js +++ b/Video.js @@ -219,13 +219,15 @@ export default class Video extends Component { getLicensePromise.then((result => { if (result !== undefined) { NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); + } else { + NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); } })).catch((error) => { - // NativeModules.VideoManager.setLicenseError(result, findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); }); } } else { - // NativeModules.VideoManager.setLicenseError(result, findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError("No enough data for license override", findNodeHandle(this._root)); } } diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 2fa1e2fde2..0878e8241f 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -371,12 +371,13 @@ - (void)setSrc:(NSDictionary *)source _isExternalPlaybackActiveObserverRegistered = YES; [self addPlayerTimeObserver]; + + _drm = [source objectForKey:@"drm"]; //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { id uri = [source objectForKey:@"uri"]; id type = [source objectForKey:@"type"]; - _drm = [source objectForKey:@"drm"]; self.onVideoLoadStart(@{@"src": @{ @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], @@ -648,6 +649,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N [self applyModifiers]; } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code], + @"message": [_playerItem.error localizedDescription], @"domain": _playerItem.error.domain}, @"target": self.reactTag}); } @@ -1482,6 +1484,21 @@ - (void)setLicenseResult:(NSString * )license { } } +- (BOOL)setLicenseResultError:(NSString * )error { + if (_loadingRequest) { + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1336 + userInfo: @{ + NSLocalizedDescriptionKey: error, + NSLocalizedFailureReasonErrorKey: error, + NSLocalizedRecoverySuggestionErrorKey: error + } + ]; + [_loadingRequest finishLoadingWithError:licenseError]; + } + return false; +} + - (BOOL)ensureDirExistsWithPath:(NSString *)path { BOOL isDir = NO; NSError *error; @@ -1532,8 +1549,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; if (spcError != nil) { - //TODO: Error - [loadingRequest finishLoadingWithError:nil]; + [loadingRequest finishLoadingWithError:spcError]; return false; } if (spcData != nil) { @@ -1553,19 +1569,33 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if (error != nil) { - NSLog(@"Error getting %@, HTTP status code %i", url, [httpResponse statusCode]); - [loadingRequest finishLoadingWithError:nil]; + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + [loadingRequest finishLoadingWithError:error]; } else { if([httpResponse statusCode] != 200){ - // TODO: Error - NSLog(@"Error getting %@, HTTP status code %i", url, [httpResponse statusCode]); - [loadingRequest finishLoadingWithError:nil]; - } - if (data != nil) { + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1337 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"License server responded with status code %li", (long)[httpResponse statusCode]], + NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; + } else if (data != nil) { [dataRequest respondWithData:data]; [loadingRequest finishLoading]; } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1338 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; } } @@ -1575,29 +1605,77 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1339 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: @"No spc received.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1340 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1341 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1342 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1343 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } } else { - [loadingRequest finishLoadingWithError:nil]; + NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + code: -1344 + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No drm object found.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" + } + ]; + [loadingRequest finishLoadingWithError:licenseError]; return false; } From f1d97c112e18e00546ce792a5b536bd358580a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 09:44:14 +0100 Subject: [PATCH 021/261] change error domain and protect error throwing --- ios/Video/RCTVideo.m | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0878e8241f..0451fe9594 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -649,7 +649,9 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N [self applyModifiers]; } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code], - @"message": [_playerItem.error localizedDescription], + @"localizedDescription": [_playerItem.error localizedDescription] == nil ? @"" : [_playerItem.error localizedDescription], + @"localizedFailureReason": [_playerItem.error localizedFailureReason] == nil ? @"" : [_playerItem.error localizedFailureReason], + @"localizedRecoverySuggestion": [_playerItem.error localizedRecoverySuggestion] == nil ? @"" : [_playerItem.error localizedRecoverySuggestion], @"domain": _playerItem.error.domain}, @"target": self.reactTag}); } @@ -1486,7 +1488,7 @@ - (void)setLicenseResult:(NSString * )license { - (BOOL)setLicenseResultError:(NSString * )error { if (_loadingRequest) { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1336 userInfo: @{ NSLocalizedDescriptionKey: error, @@ -1525,7 +1527,15 @@ - (NSString *)cacheDirectoryPath { #pragma mark - AVAssetResourceLoaderDelegate +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { + return [self loadingRequestHandling:renewalRequest]; +} + - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { + return [self loadingRequestHandling:loadingRequest]; +} + +- (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { _loadingRequest = loadingRequest; NSURL *url = loadingRequest.request.URL; NSString *contentId = url.host; @@ -1558,7 +1568,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad self.onGetLicense(@{@"spc": spcStr, @"target": self.reactTag}); return true; - } else { + } else if(licenseServer != nil) { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setHTTPMethod:@"POST"]; [request setURL:[NSURL URLWithString:licenseServer]]; @@ -1574,7 +1584,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad } else { if([httpResponse statusCode] != 200){ NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1337 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining license.", @@ -1587,7 +1597,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad [dataRequest respondWithData:data]; [loadingRequest finishLoading]; } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1338 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", @@ -1605,7 +1615,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1339 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining license.", @@ -1618,7 +1628,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1340 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", @@ -1630,7 +1640,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad return false; } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1341 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", @@ -1642,7 +1652,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad return false; } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1342 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM License.", @@ -1654,7 +1664,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad return false; } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1343 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", @@ -1667,7 +1677,7 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoad } } else { - NSError *licenseError = [NSError errorWithDomain:_playerItem.error.domain + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1344 userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", From 7367a90b010f6d1fe739cd468ebf697047674089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 09:45:32 +0100 Subject: [PATCH 022/261] set license error method --- ios/Video/RCTVideoManager.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 084b2eabba..30a95c7e7b 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -92,6 +92,20 @@ - (dispatch_queue_t)methodQueue }]; }; +RCT_REMAP_METHOD(setLicenseResultError, + error:(NSString *)error + reactTag:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTVideo *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTVideo class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); + } else { + [view setLicenseResultError:error]; + } + }]; +}; + - (NSDictionary *)constantsToExport { return @{ From 59783f012f5a4cf63c72ea286cc0c8d57e62283c Mon Sep 17 00:00:00 2001 From: Rinon Beselica Date: Tue, 22 Jan 2019 11:10:47 +0100 Subject: [PATCH 023/261] fixed a mistake in the documentation about the headers. (#1420) --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ee34399ce..3d24de3401 100644 --- a/README.md +++ b/README.md @@ -420,15 +420,18 @@ Platforms: iOS Platforms: iOS #### headers -Pass headers to the HTTP client. Can be used for authorization. +Pass headers to the HTTP client. Can be used for authorization. Headers must be a part of the source object. To enable this on iOS, you will need to manually edit RCTVideo.m and uncomment the header code in the playerItemForSource function. This is because the code used a private API and may cause your app to be rejected by the App Store. Use at your own risk. Example: ``` -headers={{ - Authorization: 'bearer some-token-value', - 'X-Custom-Header': 'some value' +source={{ + uri: "https://www.example.com/video.mp4", + headers: { + Authorization: 'bearer some-token-value', + 'X-Custom-Header': 'some value' + } }} ``` From bcd9c7abdfe55987f4eab66ff72d25c614fe0076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 11:29:08 +0100 Subject: [PATCH 024/261] More error handling --- ios/Video/RCTVideo.m | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0451fe9594..bf291e646c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -723,6 +723,13 @@ - (void)attachListeners selector:@selector(handleAVPlayerAccess:) name:AVPlayerItemNewAccessLogEntryNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name: AVPlayerItemFailedToPlayToEndTimeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didFailToFinishPlaying:) + name: AVPlayerItemFailedToPlayToEndTimeNotification + object:nil]; } @@ -737,6 +744,16 @@ - (void)handleAVPlayerAccess:(NSNotification *)notification { */ } +- (void)didFailToFinishPlaying:(NSNotification *)notification { + NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey]; + self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], + @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], + @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], + @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], + @"domain": error.domain}, + @"target": self.reactTag}); +} + - (void)playbackStalled:(NSNotification *)notification { if(self.onPlaybackStalled) { @@ -1534,6 +1551,11 @@ - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRene - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { return [self loadingRequestHandling:loadingRequest]; } + + - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader + didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { + NSLog(@"didCancelLoadingRequest"); +} - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { _loadingRequest = loadingRequest; From e3801d662ee900cacc3ae65566f13a81fb9a0a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 11:42:56 +0100 Subject: [PATCH 025/261] error interface --- ios/Video/RCTVideo.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 8e5b751ac1..cdffa0d11d 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -46,5 +46,6 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; +- (void)setLicenseResultError:(NSString * )error; @end From d2f96411137eb2c002cb679210ebf95fd198aef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 11:43:12 +0100 Subject: [PATCH 026/261] process headers --- ios/Video/RCTVideo.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index bf291e646c..0b5c3cf1c4 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1594,6 +1594,15 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setHTTPMethod:@"POST"]; [request setURL:[NSURL URLWithString:licenseServer]]; + // HEADERS + NSDictionary *headers = (NSDictionary *)[_drm objectForKey:@"headers"]; + if (headers != nil) { + for (NSString *key in headers) { + NSString *value = headers[key]; + [request setValue:value forHTTPHeaderField:key]; + } + } + // [request setHTTPBody: spcData]; NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; From 4dc4db3a83d725264fb0b181ce2a8a73e9707ef2 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 22 Jan 2019 11:48:53 +0100 Subject: [PATCH 027/261] fix: set the correct git url (#1439) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 069dd1bdec..8e5e3ddc06 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ ], "repository": { "type": "git", - "url": "git@github.com:brentvatne/react-native-video.git" + "url": "git@github.com:react-native-community/react-native-video.git" }, "devDependencies": { "babel-eslint": "5.0.0-beta8", From cad76c1bc2ee39b58b2dfd4d85412a0683089e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 16:13:47 +0100 Subject: [PATCH 028/261] Export DRMType --- Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Video.js b/Video.js index 70b1a7db8f..21bdc900c5 100644 --- a/Video.js +++ b/Video.js @@ -13,7 +13,7 @@ const styles = StyleSheet.create({ }, }); -export { TextTrackType, FilterType }; +export { TextTrackType, FilterType, DRMType }; export default class Video extends Component { From 13e7d1cc8c99d0fc2c58186fd92b65f77d35c952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 16:14:05 +0100 Subject: [PATCH 029/261] fix log android --- .../com/brentvatne/exoplayer/ReactExoplayerViewManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index c4be6ca261..e95cfa62fe 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -139,7 +139,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src } videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); } - Log.d("Disabling TextureView (needed for DRM)"); + Log.d("setDrm", "Disabling TextureView (needed for DRM)"); videoView.setUseTextureView(false); } } From f2f33a8309b34853156eb51e2fe902da1a3eb1f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 22 Jan 2019 17:02:04 +0100 Subject: [PATCH 030/261] include DRMType to files exported to npm --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 33b437c718..8d16cbf147 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "ios", "windows", "FilterType.js", + "DRMType.js", "TextTrackType.js", "VideoResizeMode.js", "react-native-video.podspec" From 4afb12ba98e74f6610161f675c698ae37a1cc538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 09:32:30 +0100 Subject: [PATCH 031/261] bubble error to JS --- ios/Video/RCTVideo.m | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0b5c3cf1c4..dcf5690be9 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1494,7 +1494,7 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve rej } } -- (void)setLicenseResult:(NSString * )license { +- (void)setLicenseResult:(NSString *)license { NSData *respondData = [self base64DataFromString:license]; if (_loadingRequest) { AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; @@ -1503,8 +1503,8 @@ - (void)setLicenseResult:(NSString * )license { } } -- (BOOL)setLicenseResultError:(NSString * )error { - if (_loadingRequest) { +- (BOOL)setLicenseResultError:(NSString *)error { + if (_loadingRequest != nil) { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: -1336 userInfo: @{ @@ -1513,11 +1513,26 @@ - (BOOL)setLicenseResultError:(NSString * )error { NSLocalizedRecoverySuggestionErrorKey: error } ]; - [_loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); } return false; } +- (BOOL)finishLoadingWithError:(NSError *)error { + if (_loadingRequest && error != nil) { + NSError *licenseError = error; + [_loadingRequest finishLoadingWithError:licenseError]; + if (self.onVideoError) { + self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], + @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], + @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], + @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], + @"domain": _playerItem.error.domain}, + @"target": self.reactTag}); + } + } +} + - (BOOL)ensureDirExistsWithPath:(NSString *)path { BOOL isDir = NO; NSError *error; @@ -1581,7 +1596,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; if (spcError != nil) { - [loadingRequest finishLoadingWithError:spcError]; + self.finishLoadingWithError(spcError); return false; } if (spcData != nil) { @@ -1611,7 +1626,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if (error != nil) { NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - [loadingRequest finishLoadingWithError:error]; + self.finishLoadingWithError(error); } else { if([httpResponse statusCode] != 200){ NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); @@ -1623,7 +1638,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); } else if (data != nil) { [dataRequest respondWithData:data]; [loadingRequest finishLoading]; @@ -1636,7 +1651,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); } } @@ -1654,7 +1669,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } @@ -1667,7 +1682,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } } else { @@ -1679,7 +1694,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } } else { @@ -1691,7 +1706,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } } else { @@ -1703,7 +1718,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } @@ -1716,7 +1731,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" } ]; - [loadingRequest finishLoadingWithError:licenseError]; + self.finishLoadingWithError(licenseError); return false; } From 02fd42529a7d3343903567c572f80d973249fe06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:03:16 +0100 Subject: [PATCH 032/261] Update README with iOS info --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b591d44303..d104d85eee 100644 --- a/README.md +++ b/README.md @@ -722,11 +722,12 @@ This feature will disable the use of `TextureView` on Android. DRM options are `type`, `licenseServer`, `headers`. Example: -``` + +```js source={{ uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', drm: { - type: 'widevine', + type: 'widevine', //or DRMType.WIDEVINE licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', headers: { 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' @@ -735,7 +736,13 @@ source={{ }} ``` -Platforms: Android +iOS specific fields for `drm`: + +* `certificateUrl` Url to the .cer file. +* `contentId` (optional) (overridable, otherwise it will take the value at `loadingRequest.request.URL.host`) +* `getLicense` Overridable method, `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];` + +Platforms: Android, iOS ###### Other protocols From e11fe627556d6658a398f4013904f846838ec61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:09:21 +0100 Subject: [PATCH 033/261] Provide more DRM info --- README.md | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d104d85eee..09bc91a046 100644 --- a/README.md +++ b/README.md @@ -715,7 +715,7 @@ source={{ uri: 'http://host-serving-a-type-different-than-the-extension.ism/mani type: 'mpd' }} ``` -##### Provide DRM data +##### 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. @@ -738,9 +738,44 @@ source={{ iOS specific fields for `drm`: -* `certificateUrl` Url to the .cer file. -* `contentId` (optional) (overridable, otherwise it will take the value at `loadingRequest.request.URL.host`) -* `getLicense` Overridable method, `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];` +* `certificateUrl` - Url to the .cer file. +* `contentId` (optional) - (overridable, otherwise it will take the value at `loadingRequest.request.URL.host`) +* `getLicense` - `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` 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`, 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 = btoa(spcString); + return fetch(YOUR_LICENSE_SERVER, { + method: 'POST', + // Control the headers + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + // Build the data as the server specs it + body: JSON.stringify({ + getFairplayLicense: { + releasePid: myPid, + spcMessage: base64spc, + } + }) + }) + .then(response => response.json()) + .then((response) => { + // Handle the response as you desire, f.e. when the server does not respond directly with the CKC + if (response && response.getFairplayLicenseResponse + && response.getFairplayLicenseResponse.ckcResponse) { + return response.getFairplayLicenseResponse.ckcResponse; + } + throw new Error('No correct response'); + }) + .catch((error) => { + console.error('CKC error', error); + }); +} +``` Platforms: Android, iOS From 2391747514b34514ecb0148228051d83b1a79568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:35:07 +0100 Subject: [PATCH 034/261] more DOC --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 09bc91a046..1a3c1641cb 100644 --- a/README.md +++ b/README.md @@ -721,6 +721,20 @@ You can provide some configuration to allow DRM playback. This feature will disable the use of `TextureView` on Android. DRM options are `type`, `licenseServer`, `headers`. +###### 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 + +###### licenseServer + +The URL pointing to the licenseServer that will provide the authorization to play the protected stream. + +###### headers + +You can customize headers send to the licenseServer. + Example: ```js From eed861976aaf19afdd2fcd630bd92d52dc917f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:38:04 +0100 Subject: [PATCH 035/261] remove unneeded logs --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 2b83ce7dbb..7758432a36 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -224,7 +224,6 @@ private void createViews() { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - Log.d("onAttachedToWindow", "drm url"); initializePlayer(); } @@ -275,7 +274,6 @@ public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { // Internal methods private void initializePlayer() { - Log.d("initializePlayer", "drm url"); if (player == null) { TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); From cdfb6305abf2d42c035a46d87a3b23e5faa17f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:39:39 +0100 Subject: [PATCH 036/261] unneeded toast --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 7758432a36..c242c4296d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -13,7 +13,6 @@ import android.view.Window; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; -import android.widget.Toast; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; From 6cfcf3b51f1f4bc9c67ce39c8f0d8217dbe83016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 10:44:48 +0100 Subject: [PATCH 037/261] More info about SPC --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd8a9b5cdd..cbe2cdd4e4 100644 --- a/README.md +++ b/README.md @@ -757,7 +757,7 @@ iOS specific fields for `drm`: * `certificateUrl` - Url to the .cer file. * `contentId` (optional) - (overridable, otherwise it will take the value at `loadingRequest.request.URL.host`) -* `getLicense` - `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`. +* `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`, 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: From a89ac0c76f1a223ac6cdab03c7957e96c3731815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 16:44:30 +0100 Subject: [PATCH 038/261] clean base64 + native method --- ios/Video/RCTVideo.m | 154 +++---------------------------------------- 1 file changed, 10 insertions(+), 144 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index dcf5690be9..e26a71b03c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1739,151 +1739,17 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { return true; } -- (NSData *)base64DataFromString: (NSString *)string -{ - unsigned long ixtext, lentext; - unsigned char ch, inbuf[4], outbuf[3]; - short i, ixinbuf; - Boolean flignore, flendtext = false; - const unsigned char *tempcstring; - NSMutableData *theData; - - if (string == nil) - { - return [NSData data]; - } - - ixtext = 0; - - tempcstring = (const unsigned char *)[string UTF8String]; - - lentext = [string length]; - - theData = [NSMutableData dataWithCapacity: lentext]; - - ixinbuf = 0; - - while (true) - { - if (ixtext >= lentext) - { - break; - } - - ch = tempcstring [ixtext++]; - - flignore = false; - - if ((ch >= 'A') && (ch <= 'Z')) - { - ch = ch - 'A'; - } - else if ((ch >= 'a') && (ch <= 'z')) - { - ch = ch - 'a' + 26; - } - else if ((ch >= '0') && (ch <= '9')) - { - ch = ch - '0' + 52; - } - else if (ch == '+') - { - ch = 62; - } - else if (ch == '=') - { - flendtext = true; - } - else if (ch == '/') - { - ch = 63; - } - else - { - flignore = true; - } - - if (!flignore) - { - short ctcharsinbuf = 3; - Boolean flbreak = false; - - if (flendtext) - { - if (ixinbuf == 0) - { - break; - } - - if ((ixinbuf == 1) || (ixinbuf == 2)) - { - ctcharsinbuf = 1; - } - else - { - ctcharsinbuf = 2; - } - - ixinbuf = 3; - - flbreak = true; - } - - inbuf [ixinbuf++] = ch; - - if (ixinbuf == 4) - { - ixinbuf = 0; - - outbuf[0] = (inbuf[0] << 2) | ((inbuf[1] & 0x30) >> 4); - outbuf[1] = ((inbuf[1] & 0x0F) << 4) | ((inbuf[2] & 0x3C) >> 2); - outbuf[2] = ((inbuf[2] & 0x03) << 6) | (inbuf[3] & 0x3F); - - for (i = 0; i < ctcharsinbuf; i++) - { - [theData appendBytes: &outbuf[i] length: 1]; - } - } - - if (flbreak) - { - break; - } - } - } - - return theData; -} - -- (NSString*)base64forData:(NSData*)theData { - const uint8_t* input = (const uint8_t*)[theData bytes]; - NSInteger length = [theData length]; - - static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - NSMutableData* data = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; - uint8_t* output = (uint8_t*)data.mutableBytes; - - NSInteger i; - for (i=0; i < length; i += 3) { - NSInteger value = 0; - NSInteger j; - for (j = i; j < (i + 3); j++) { - value <<= 8; - - if (j < length) { - value |= (0xFF & input[j]); - } - } - - NSInteger theIndex = (i / 3) * 4; - output[theIndex + 0] = table[(value >> 18) & 0x3F]; - output[theIndex + 1] = table[(value >> 12) & 0x3F]; - output[theIndex + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; - output[theIndex + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; - } +- (NSData *)base64DataFromString: (NSString *)string { + // Create NSData object + NSData *nsdata = [string + dataUsingEncoding:NSUTF8StringEncoding]; + // Get NSString from NSData object in Base64 + NSString *base64Encoded = [nsdata base64EncodedStringWithOptions:0]; - return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; + // NSData from the Base64 encoded str + NSData *nsdataFromBase64String = [[NSData alloc] + initWithBase64EncodedString:base64Encoded options:0]; + return nsdataFromBase64String; } @end From a8cba981c10357ef45215b36659b5be81bff3c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 16:45:01 +0100 Subject: [PATCH 039/261] return NO instead of false --- ios/Video/RCTVideo.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index e26a71b03c..99f797f936 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1531,6 +1531,7 @@ - (BOOL)finishLoadingWithError:(NSError *)error { @"target": self.reactTag}); } } + return NO; } - (BOOL)ensureDirExistsWithPath:(NSString *)path { From 7b1409c17fbd01ce7f08d4a94e2e5b9365314c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 16:47:15 +0100 Subject: [PATCH 040/261] return result of method (always NO) --- ios/Video/RCTVideo.m | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 99f797f936..5e1cc0c298 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1515,7 +1515,7 @@ - (BOOL)setLicenseResultError:(NSString *)error { ]; self.finishLoadingWithError(licenseError); } - return false; + return NO; } - (BOOL)finishLoadingWithError:(NSError *)error { @@ -1597,15 +1597,14 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; if (spcError != nil) { - self.finishLoadingWithError(spcError); - return false; + return self.finishLoadingWithError(spcError); } if (spcData != nil) { if(self.onGetLicense) { NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; self.onGetLicense(@{@"spc": spcStr, @"target": self.reactTag}); - return true; + return YES; } else if(licenseServer != nil) { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setHTTPMethod:@"POST"]; @@ -1658,7 +1657,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } }]; [postDataTask resume]; - return true; + return YES; } } else { @@ -1670,8 +1669,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } } else { @@ -1683,8 +1681,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1695,8 +1692,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1707,8 +1703,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1719,8 +1714,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } } else { @@ -1732,12 +1726,11 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" } ]; - self.finishLoadingWithError(licenseError); - return false; + return self.finishLoadingWithError(licenseError); } - return true; + return NO; } - (NSData *)base64DataFromString: (NSString *)string { From 3110864639a81d00a472e33d95ae8bd5251fa34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 17:13:06 +0100 Subject: [PATCH 041/261] add finishLoadingWithError to headers --- ios/Video/RCTVideo.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index cdffa0d11d..fff65a20f8 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -47,5 +47,6 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; - (void)setLicenseResultError:(NSString * )error; +- (BOOL)finishLoadingWithError:(NSError *)error @end From 7ccd4b69c08ae4489a9824dbc4bf9483426c4e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 23 Jan 2019 17:18:15 +0100 Subject: [PATCH 042/261] change invocation of finisLoadingwithError --- ios/Video/RCTVideo.m | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 5e1cc0c298..6c68cd4263 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1513,7 +1513,7 @@ - (BOOL)setLicenseResultError:(NSString *)error { NSLocalizedRecoverySuggestionErrorKey: error } ]; - self.finishLoadingWithError(licenseError); + [self finishLoadingWithError:licenseError]; } return NO; } @@ -1597,7 +1597,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { // Request CKC to the server NSString *licenseServer = (NSString *)[_drm objectForKey:@"licenseServer"]; if (spcError != nil) { - return self.finishLoadingWithError(spcError); + return [self finishLoadingWithError:spcError]; } if (spcData != nil) { if(self.onGetLicense) { @@ -1626,7 +1626,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if (error != nil) { NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - self.finishLoadingWithError(error); + [self finishLoadingWithError:error]; } else { if([httpResponse statusCode] != 200){ NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); @@ -1638,7 +1638,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" } ]; - self.finishLoadingWithError(licenseError); + [self finishLoadingWithError:licenseError]; } else if (data != nil) { [dataRequest respondWithData:data]; [loadingRequest finishLoading]; @@ -1651,7 +1651,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." } ]; - self.finishLoadingWithError(licenseError); + [self finishLoadingWithError:licenseError]; } } @@ -1669,7 +1669,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } } else { @@ -1681,7 +1681,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1692,7 +1692,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1703,7 +1703,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" @@ -1714,7 +1714,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } } else { @@ -1726,7 +1726,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" } ]; - return self.finishLoadingWithError(licenseError); + return [self finishLoadingWithError:licenseError]; } From 0a1605f11c159f39dff94c182f53892cae2ceb45 Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Wed, 23 Jan 2019 10:14:25 -0800 Subject: [PATCH 043/261] Make it more clear that Android SDK changes are to target SDK --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d24de3401..05337141f5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Version 3.x requires react-native >= 0.40.0 ### Version 4.0.0 breaking changes Version 4.0.0 changes some behaviors and may require updates to your Gradle files. See [Updating](#updating) for details. -Version 4.0.0 now requires Android SDK 26+ and Gradle 3 plugin in order to support ExoPlayer 2.9.0. Google is dropping support for apps using SDKs older than 26 as of October 2018 and Gradle 2 as of January 2019. React Native 0.57 defaults to Gradle 3 & SDK 27. +Version 4.0.0 now requires Android target SDK 26+ and Gradle 3 plugin in order to support ExoPlayer 2.9.0. Google is dropping support for apps using target SDKs older than 26 as of October 2018 and Gradle 2 as of January 2019. React Native 0.57 defaults to Gradle 3 & SDK 27. If you need to support an older React Native version, you should use react-native-video 3.2.1. @@ -1173,8 +1173,8 @@ To enable audio to play in background on iOS the audio session needs to be set t ### Version 4.0.0 -#### Gradle 3 and SDK 26 requirement -In order to support ExoPlayer 2.9.0, you must use version 3 or higher of the Gradle plugin. This is included by default in React Native 0.57. ExoPlayer +#### Gradle 3 and target SDK 26 requirement +In order to support ExoPlayer 2.9.0, you must use version 3 or higher of the Gradle plugin. This is included by default in React Native 0.57. #### ExoPlayer 2.9.0 Java 1.8 requirement ExoPlayer 2.9.0 uses some Java 1.8 features, so you may need to enable support for Java 1.8 in your app/build.gradle file. If you get an error, compiling with ExoPlayer like: @@ -1209,7 +1209,7 @@ Previously, on Android MediaPlayer if you setup an AppState event when the app w Note, Windows does not have a concept of an app going into the background, so this doesn't apply there. -#### Use Android SDK 27 by default +#### Use Android target SDK 27 by default Version 3.0 updates the Android build tools and SDK to version 27. React Native is in the process of [switchting over](https://github.com/facebook/react-native/issues/18095#issuecomment-395596130) to SDK 27 in preparation for Google's requirement that new Android apps [use SDK 26](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html) by August 2018. You will either need to install the version 27 SDK and version 27.0.3 buildtools or modify your build.gradle file to configure react-native-video to use the same build settings as the rest of your app as described below. From 91e0206a41b365502ff17e9b5780c49ee2d7b44c Mon Sep 17 00:00:00 2001 From: sridhar Date: Thu, 24 Jan 2019 15:44:45 +0530 Subject: [PATCH 044/261] Exoplayer gradle changes Exoplayer gradle changes --- android-exoplayer/build.gradle | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index a532956d85..4c7c3f0653 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -18,18 +18,26 @@ android { dependencies { compileOnly "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - implementation('com.google.android.exoplayer:exoplayer:2.9.3') { - exclude group: 'com.android.support' - } + // implementation('com.google.android.exoplayer:exoplayer:2.9.3') { + // exclude group: 'com.android.support' + // } + + implementation project(':exoplayer-library-core') + implementation project(':exoplayer-library-dash') + implementation project(':exoplayer-library-ui') + implementation project(':exoplayer-library-smoothstreaming') + implementation project(':exoplayer-library-hls') + implementation project(':exoplayer-extension-okhttp') // All support libs must use the same version implementation "com.android.support:support-annotations:${safeExtGet('supportLibVersion', '+')}" implementation "com.android.support:support-compat:${safeExtGet('supportLibVersion', '+')}" implementation "com.android.support:support-media-compat:${safeExtGet('supportLibVersion', '+')}" - implementation('com.google.android.exoplayer:extension-okhttp:2.9.3') { - exclude group: 'com.squareup.okhttp3', module: 'okhttp' - } + // implementation('com.google.android.exoplayer:extension-okhttp:2.9.3') { + // exclude group: 'com.squareup.okhttp3', module: 'okhttp' + // } + implementation 'com.squareup.okhttp3:okhttp:3.12.1' } From 2713e8f6be784ae1ff58f12e6c64fcb12c6b909b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 24 Jan 2019 11:27:44 +0100 Subject: [PATCH 045/261] fix base64 --- ios/Video/RCTVideo.h | 1 - ios/Video/RCTVideo.m | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index fff65a20f8..cdffa0d11d 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -47,6 +47,5 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; - (void)setLicenseResultError:(NSString * )error; -- (BOOL)finishLoadingWithError:(NSError *)error @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 6c68cd4263..e454422664 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1495,11 +1495,13 @@ - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve rej } - (void)setLicenseResult:(NSString *)license { - NSData *respondData = [self base64DataFromString:license]; - if (_loadingRequest) { + NSData *respondData = [self base64DataFromBase64String:license]; + if (_loadingRequest != nil && respondData != nil) { AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; [dataRequest respondWithData:respondData]; [_loadingRequest finishLoading]; + } else { + [self setLicenseResultError:@"No data from JS license response"]; } } @@ -1733,17 +1735,13 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { return NO; } -- (NSData *)base64DataFromString: (NSString *)string { - // Create NSData object - NSData *nsdata = [string - dataUsingEncoding:NSUTF8StringEncoding]; - // Get NSString from NSData object in Base64 - NSString *base64Encoded = [nsdata base64EncodedStringWithOptions:0]; - - // NSData from the Base64 encoded str - NSData *nsdataFromBase64String = [[NSData alloc] - initWithBase64EncodedString:base64Encoded options:0]; - return nsdataFromBase64String; +- (NSData *)base64DataFromBase64String: (NSString *)base64String { + if (base64String != nil) { + // NSData from the Base64 encoded str + NSData *base64Data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSASCIIStringEncoding]; + return base64Data; + } + return nil; } @end From 56cbf9cb17717af2b121e9f653ba927bcb7839fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 24 Jan 2019 12:08:13 +0100 Subject: [PATCH 046/261] be more specific with base64 needed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe2cdd4e4..b7c86dca54 100644 --- a/README.md +++ b/README.md @@ -758,7 +758,7 @@ iOS specific fields for `drm`: * `certificateUrl` - Url to the .cer file. * `contentId` (optional) - (overridable, otherwise it will take the value at `loadingRequest.request.URL.host`) * `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`, either by just returning it or returning a `Promise` that resolves with the `CKC`. + 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 From b813a8449f1f018f6a27705200d0e38140bcd108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 24 Jan 2019 12:12:09 +0100 Subject: [PATCH 047/261] alphabetically --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index baf9aeb8f9..9cd4e10277 100644 --- a/README.md +++ b/README.md @@ -724,16 +724,6 @@ You can provide some configuration to allow DRM playback. This feature will disable the use of `TextureView` on Android. DRM options are `type`, `licenseServer`, `headers`. -###### 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 - -###### licenseServer - -The URL pointing to the licenseServer that will provide the authorization to play the protected stream. - ###### headers You can customize headers send to the licenseServer. @@ -753,6 +743,10 @@ source={{ }} ``` +###### licenseServer + +The URL pointing to the licenseServer that will provide the authorization to play the protected stream. + iOS specific fields for `drm`: * `certificateUrl` - Url to the .cer file. @@ -796,6 +790,12 @@ iOS specific fields for `drm`: Platforms: Android, iOS +###### 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 + ###### Other protocols The following other types are supported on some platforms, but aren't fully documented yet: From 783679794f813d975584462f0afccf6decdc985f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 24 Jan 2019 12:22:20 +0100 Subject: [PATCH 048/261] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a19ba40c..45ad0cb24f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Changelog +### Version 4.3.1 +* Support DRM for iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445) + ### Version 4.3.0 * Fix iOS video not displaying after switching source [#1395](https://github.com/react-native-community/react-native-video/pull/1395) * Add the filterEnabled flag, fixes iOS video start time regression [#1384](https://github.com/react-native-community/react-native-video/pull/1384) From 125d5dc9c54dd243742a204746b5a227d7b1e723 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 24 Jan 2019 13:15:58 +0100 Subject: [PATCH 049/261] fix: omit packager assets from caching (#1438) --- CHANGELOG.md | 4 ++ Video.js | 2 + examples/video-caching/App.ios.js | 78 ++++++++++++++++++------- examples/video-caching/ios/Podfile | 2 +- examples/video-caching/ios/Podfile.lock | 10 ++-- examples/video-caching/rn-cli.config.js | 16 ++++- ios/Video/RCTVideo.m | 5 +- 7 files changed, 87 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a19ba40c..280ada24fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### next + +* Fix loading package resolved videos when using video-caching [#1438](https://github.com/react-native-community/react-native-video/pull/1438) + ### Version 4.3.0 * Fix iOS video not displaying after switching source [#1395](https://github.com/react-native-community/react-native-video/pull/1395) * Add the filterEnabled flag, fixes iOS video start time regression [#1384](https://github.com/react-native-community/react-native-video/pull/1384) diff --git a/Video.js b/Video.js index 92544467c0..ff893efb29 100644 --- a/Video.js +++ b/Video.js @@ -213,6 +213,7 @@ export default class Video extends Component { render() { const resizeMode = this.props.resizeMode; const source = resolveAssetSource(this.props.source) || {}; + const shouldCache = !Boolean(source.__packager_asset) let uri = source.uri || ''; if (uri && uri.match(/^\//)) { @@ -241,6 +242,7 @@ export default class Video extends Component { uri, isNetwork, isAsset, + shouldCache, type: source.type || '', mainVer: source.mainVer || 0, patchVer: source.patchVer || 0, diff --git a/examples/video-caching/App.ios.js b/examples/video-caching/App.ios.js index 20f69745ea..f38f945a01 100644 --- a/examples/video-caching/App.ios.js +++ b/examples/video-caching/App.ios.js @@ -5,39 +5,70 @@ */ import React, { Component } from "react"; -import { StyleSheet, Text, View, Dimensions, TouchableOpacity } from "react-native"; +import { Alert, StyleSheet, Text, View, Dimensions, TouchableOpacity } from "react-native"; import Video from "react-native-video"; const { height, width } = Dimensions.get("screen"); type Props = {}; -export default class App extends Component { + +type State = { + showLocal: boolean +}; + +function Button({ text, onPress }: { text: string, onPress: () => void }) { + return ( + + {text} + + ) +} + +export default class App extends Component { + state = { + showLocal: false + } render() { return (