From 5154cb6edf9e5ca9b550d365ef902c990be04336 Mon Sep 17 00:00:00 2001 From: yaraht17 Date: Thu, 21 Sep 2017 11:08:42 +0700 Subject: [PATCH 001/101] support drm exoplayer --- Video.js | 3 + .../exoplayer/ReactExoplayerView.java | 138 ++++++++++++++++-- .../exoplayer/ReactExoplayerViewManager.java | 47 +++++- .../src/main/res/values/strings.xml | 13 +- 4 files changed, 182 insertions(+), 19 deletions(-) diff --git a/Video.js b/Video.js index b90c030e4f..33345614a1 100644 --- a/Video.js +++ b/Video.js @@ -277,6 +277,9 @@ Video.propTypes = { muted: PropTypes.bool, volume: 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/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 52bf24e64c..d11fa0e317 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -9,6 +9,7 @@ import android.text.TextUtils; import android.util.Log; import android.widget.FrameLayout; +import android.widget.Toast; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; @@ -17,13 +18,21 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; @@ -46,12 +55,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.lang.Math; +import java.util.UUID; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -59,7 +71,8 @@ class ReactExoplayerView extends FrameLayout implements ExoPlayer.EventListener, BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, - MetadataRenderer.Output { + MetadataRenderer.Output, + DefaultDrmSessionManager.EventListener{ private static final String TAG = "ReactExoplayerView"; @@ -96,6 +109,9 @@ class ReactExoplayerView extends FrameLayout implements private boolean disableFocus; private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; + private UUID drmUUID = null; + private String drmLicenseUrl = null; + private String[] drmLicenseHeader = null; // \ End props // React @@ -131,7 +147,7 @@ public ReactExoplayerView(ThemedReactContext context) { themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); - initializePlayer(); +// initializePlayer(); } @@ -161,6 +177,7 @@ private void createViews() { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + Log.d("onAttachedToWindow", "drm url"); initializePlayer(); } @@ -197,14 +214,32 @@ public void cleanUpResources() { stopPlayback(); } - // 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); - player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, new DefaultLoadControl()); + + DrmSessionManager drmSessionManager = null; + if (this.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(this.drmUUID, this.drmLicenseUrl, + this.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + Log.d("Drm Info", getResources().getString(errorStringId)); + return; + } + } + + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext(), + drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + + player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); player.setMetadataOutput(this); exoPlayerView.setPlayer(player); @@ -230,6 +265,35 @@ private void initializePlayer() { } } + + private DrmSessionManager buildDrmSessionManager(UUID uuid, + String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + if (Util.SDK_INT < 18) { + return null; + } + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } + return new DefaultDrmSessionManager<>(uuid, + FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, this); + } + + /** + * Returns a new HttpDataSource factory. + * + * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new + * DataSource factory. + * @return A new HttpDataSource factory. + */ + private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { + return new DefaultHttpDataSourceFactory("sctv", useBandwidthMeter ? BANDWIDTH_METER : null); + } + private MediaSource buildMediaSource(Uri uri, String overrideExtension) { int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension : uri.getLastPathSegment()); @@ -616,12 +680,12 @@ public void seekTo(long positionMs) { } public void setRateModifier(float newRate) { - rate = newRate; + rate = newRate; - if (player != null) { - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); - } + if (player != null) { + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } } @@ -632,4 +696,60 @@ public void setPlayInBackground(boolean playInBackground) { public void setDisableFocus(boolean disableFocus) { this.disableFocus = disableFocus; } + + /** + * + * @param drmName + */ + public void setDrmName(String drmName) throws ParserException{ + switch (Util.toLowerInvariant(drmName)) { + case "widevine": + this.drmUUID = C.WIDEVINE_UUID; + break; + case "playready": + this.drmUUID = C.PLAYREADY_UUID; + break; + case "cenc": + this.drmUUID = C.CLEARKEY_UUID; + break; + default: + try { + this.drmUUID = UUID.fromString(drmName); + } catch (RuntimeException e) { + throw new ParserException("Unsupported drm type: " + drmName); + } + } + Log.d("setDrmLicenseUrl", drmName); + } + + public void setDrmLicenseUrl(String licenseUrl){ + Log.d("setDrmLicenseUrl", licenseUrl); + this.drmLicenseUrl = licenseUrl; + } + + public void setDrmLicenseHeader(String[] header){ + Log.d("setDrmLicenseHeader", header.toString()); + this.drmLicenseHeader = header; + } + + + @Override + public void onDrmKeysLoaded() { + Log.d("DRM Info", "onDrmKeysLoaded"); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + Log.d("DRM Info", "onDrmSessionManagerError"); + } + + @Override + public void onDrmKeysRestored() { + Log.d("DRM Info", "onDrmKeysRestored"); + } + + @Override + public void onDrmKeysRemoved() { + Log.d("DRM Info", "onDrmKeysRestored"); + } } 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 dda8d00036..9a40e673d4 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -3,14 +3,17 @@ import android.content.Context; import android.net.Uri; import android.text.TextUtils; +import android.util.Log; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; import com.google.android.exoplayer2.upstream.RawResourceDataSource; +import java.util.ArrayList; import java.util.Map; import javax.annotation.Nullable; @@ -32,6 +35,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager 0) { @@ -155,6 +162,34 @@ public void setDisableFocus(final ReactExoplayerView videoView, final boolean di videoView.setDisableFocus(disableFocus); } + @ReactProp(name = PROP_DRM_LICENSE_URL) + public void setDrmUrl(final ReactExoplayerView videoView, @Nullable String licenseUrl){ + Log.d("setDrmUrl", licenseUrl); + videoView.setDrmLicenseUrl(licenseUrl); + } + + @ReactProp(name = PROP_DRM_LICENSE_HEADER) + public void setDrmHeader(final ReactExoplayerView videoView, @Nullable ReadableMap headers){ + ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); + ReadableMapKeySetIterator itr = headers.keySetIterator(); + while (itr.hasNextKey()) { + String key = itr.nextKey(); + drmKeyRequestPropertiesList.add(key); + drmKeyRequestPropertiesList.add(headers.getString(key)); + } + videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + } + + @ReactProp(name = PROP_DRM_NAME) + public void setDrmName(final ReactExoplayerView videoView, final String drmName){ + try { + videoView.setDrmName(drmName); + } + catch (Exception ex){ + Log.e("DRM Info", ex.toString()); + } + } + private boolean startsWithValidScheme(String uriString) { return uriString.startsWith("http://") || uriString.startsWith("https://") diff --git a/android-exoplayer/src/main/res/values/strings.xml b/android-exoplayer/src/main/res/values/strings.xml index b92a238a2f..a4a6f7a287 100644 --- a/android-exoplayer/src/main/res/values/strings.xml +++ b/android-exoplayer/src/main/res/values/strings.xml @@ -1,12 +1,17 @@ - This device does not provide a decoder for %1$s + This device does not provide a decoder for %1$s - This device does not provide a secure decoder for %1$s + This device does not provide a secure decoder for %1$s - Unable to query device decoders + Unable to query device decoders - Unable to instantiate decoder %1$s + Unable to instantiate decoder %1$s + + Protected content not supported on API levels below 18 + This device does not support the required DRM scheme + + An unknown DRM error occurred From 875311163354f2668da82e2c2900e65affe95b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 11 Jan 2019 11:25:06 +0100 Subject: [PATCH 002/101] change repo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 069dd1bdec..33b437c718 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:24i/react-native-video.git" }, "devDependencies": { "babel-eslint": "5.0.0-beta8", From ea7601e270d716cf3d634e3f240d5f074f6e41d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 11 Jan 2019 17:05:07 +0100 Subject: [PATCH 003/101] correct wrong term --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 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 5d4f3dc634..b9a15d58c4 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1135,7 +1135,7 @@ public void setDrmName(String drmName) throws ParserException{ case "playready": this.drmUUID = C.PLAYREADY_UUID; break; - case "cenc": + case "clearkey": this.drmUUID = C.CLEARKEY_UUID; break; default: From 0755c8cf5fdfc2578207c297b0e33c9724aaf496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 11 Jan 2019 17:05:36 +0100 Subject: [PATCH 004/101] nest drm under source --- .../exoplayer/ReactExoplayerViewManager.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 2fb4f62b9d..bbe6d1a3a7 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -29,6 +29,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; + ReadableMap drm = src.hasKey(PROP_SRC_DRM) ? src.getMap(PROP_SRC_DRM) : null; if (TextUtils.isEmpty(uriString)) { @@ -116,6 +118,14 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src if (srcUri != null) { videoView.setSrc(srcUri, extension, headers); + if (drm != null && drm.hasKey(PROP_SRC_DRM_TYPE)) { + String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; + String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; + Map drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? toStringMap(drm.getString(PROP_SRC_DRM_HEADERS)) : null; + videoView.setDrmName(drmType); + videoView.setDrmLicenseUrl(drmLicenseServer); + videoView.setDrmLicenseHeader(drmHeaders) + } } } else { int identifier = context.getResources().getIdentifier( From a665eb1c940895495837a1d70288c580ce79b1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 11 Jan 2019 17:25:06 +0100 Subject: [PATCH 005/101] missin semicolon --- .../exoplayer/ReactExoplayerViewManager.java | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) 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 bbe6d1a3a7..5252f1c502 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -124,7 +124,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src Map drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? toStringMap(drm.getString(PROP_SRC_DRM_HEADERS)) : null; videoView.setDrmName(drmType); videoView.setDrmLicenseUrl(drmLicenseServer); - videoView.setDrmLicenseHeader(drmHeaders) + videoView.setDrmLicenseHeader(drmHeaders); } } } else { @@ -290,34 +290,6 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs); } } - - @ReactProp(name = PROP_DRM_LICENSE_URL) - public void setDrmUrl(final ReactExoplayerView videoView, @Nullable String licenseUrl){ - Log.d("setDrmUrl", licenseUrl); - videoView.setDrmLicenseUrl(licenseUrl); - } - - @ReactProp(name = PROP_DRM_LICENSE_HEADER) - public void setDrmHeader(final ReactExoplayerView videoView, @Nullable ReadableMap headers){ - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - ReadableMapKeySetIterator itr = headers.keySetIterator(); - while (itr.hasNextKey()) { - String key = itr.nextKey(); - drmKeyRequestPropertiesList.add(key); - drmKeyRequestPropertiesList.add(headers.getString(key)); - } - videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); - } - - @ReactProp(name = PROP_DRM_NAME) - public void setDrmName(final ReactExoplayerView videoView, final String drmName){ - try { - videoView.setDrmName(drmName); - } - catch (Exception ex){ - Log.e("DRM Info", ex.toString()); - } - } private boolean startsWithValidScheme(String uriString) { return uriString.startsWith("http://") From ad9ad49295b6d15acb760022214be3988e447650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 11 Jan 2019 18:24:17 +0100 Subject: [PATCH 006/101] fix getString -> getMap for headers --- .../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 5252f1c502..86d14f58fb 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -121,7 +121,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src if (drm != null && drm.hasKey(PROP_SRC_DRM_TYPE)) { String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; - Map drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? toStringMap(drm.getString(PROP_SRC_DRM_HEADERS)) : null; + Map drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? toStringMap(drm.getMap(PROP_SRC_DRM_HEADERS)) : null; videoView.setDrmName(drmType); videoView.setDrmLicenseUrl(drmLicenseServer); videoView.setDrmLicenseHeader(drmHeaders); From 06bb389634d7f53430d327d6de7f8384bc1cc0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 12:26:44 +0100 Subject: [PATCH 007/101] drm headers processing --- .../exoplayer/ReactExoplayerViewManager.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 86d14f58fb..0d60d57763 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -121,10 +121,19 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src if (drm != null && drm.hasKey(PROP_SRC_DRM_TYPE)) { String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; - Map drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? toStringMap(drm.getMap(PROP_SRC_DRM_HEADERS)) : null; + ReadableMap drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? drm.getMap(PROP_SRC_DRM_HEADERS) : null; videoView.setDrmName(drmType); videoView.setDrmLicenseUrl(drmLicenseServer); - videoView.setDrmLicenseHeader(drmHeaders); + if (drmHeaders != null) { + ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); + ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); + while (itr.hasNextKey()) { + String key = itr.nextKey(); + drmKeyRequestPropertiesList.add(key); + drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + } + videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + } } } } else { From 7ae01b3bef6f9a4ab4231b77002a669d3ec4d44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 12:38:39 +0100 Subject: [PATCH 008/101] disable textureView when DRM --- .../exoplayer/ReactExoplayerViewManager.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) 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 0d60d57763..d15c51c8ee 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -122,17 +122,21 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; ReadableMap drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? drm.getMap(PROP_SRC_DRM_HEADERS) : null; - videoView.setDrmName(drmType); - videoView.setDrmLicenseUrl(drmLicenseServer); - if (drmHeaders != null) { - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); - while (itr.hasNextKey()) { - String key = itr.nextKey(); - drmKeyRequestPropertiesList.add(key); - drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + if (drmType != null && drmLicenseServer != null) { + videoView.setDrmName(drmType); + videoView.setDrmLicenseUrl(drmLicenseServer); + if (drmHeaders != null) { + ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); + ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); + while (itr.hasNextKey()) { + String key = itr.nextKey(); + drmKeyRequestPropertiesList.add(key); + drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + } + videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + videoView.setUseTextureView(false); } - videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + } } } From 870e0072f40af0f0f8f89ea2c4f2d812bee027ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 13:09:45 +0100 Subject: [PATCH 009/101] Refactor with exoplayer util getDrmUuid --- .../exoplayer/ReactExoplayerView.java | 26 +++---------------- .../exoplayer/ReactExoplayerViewManager.java | 7 +++-- 2 files changed, 8 insertions(+), 25 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 b9a15d58c4..946a8d99e2 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1123,29 +1123,9 @@ public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBuffe releasePlayer(); initializePlayer(); } - /** - * - * @param drmName - */ - public void setDrmName(String drmName) throws ParserException{ - switch (Util.toLowerInvariant(drmName)) { - case "widevine": - this.drmUUID = C.WIDEVINE_UUID; - break; - case "playready": - this.drmUUID = C.PLAYREADY_UUID; - break; - case "clearkey": - this.drmUUID = C.CLEARKEY_UUID; - break; - default: - try { - this.drmUUID = UUID.fromString(drmName); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + drmName); - } - } - Log.d("setDrmLicenseUrl", drmName); + + public void setDrmType(UUID drmType) throws ParserException{ + this.drmUUID = drmType; } public void setDrmLicenseUrl(String licenseUrl){ 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 d15c51c8ee..2d7d408053 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -13,6 +13,7 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.upstream.RawResourceDataSource; @@ -122,8 +123,10 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; ReadableMap drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? drm.getMap(PROP_SRC_DRM_HEADERS) : null; - if (drmType != null && drmLicenseServer != null) { - videoView.setDrmName(drmType); + if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { + Log.d("setDrmLicenseUrl", drmType); + UUID drmUUID = Util.getDrmUuid(drmType); + videoView.setDrmType(drmUUID); videoView.setDrmLicenseUrl(drmLicenseServer); if (drmHeaders != null) { ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); From 8506567707ab81a084e99574e68437b5cf09b9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 13:23:45 +0100 Subject: [PATCH 010/101] missing import --- .../java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java | 1 + 1 file changed, 1 insertion(+) 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 2d7d408053..b5220154ae 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.ArrayList; import java.util.Map; +import java.util.UUID; import javax.annotation.Nullable; From 347d525e1f609efcf2cf0762063d11f93701ea50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 16:15:44 +0100 Subject: [PATCH 011/101] setDrmType does not throw anymore --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 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 946a8d99e2..e8ae17685d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1124,7 +1124,7 @@ public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBuffe initializePlayer(); } - public void setDrmType(UUID drmType) throws ParserException{ + public void setDrmType(UUID drmType) { this.drmUUID = drmType; } From fd7858e210c73998144412bd48b177af8d61b21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 17:22:20 +0100 Subject: [PATCH 012/101] Log msg that TextureView will be disabled when using DRM --- .../com/brentvatne/exoplayer/ReactExoplayerViewManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b5220154ae..958ef597a6 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -125,7 +125,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; ReadableMap drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? drm.getMap(PROP_SRC_DRM_HEADERS) : null; if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { - Log.d("setDrmLicenseUrl", drmType); + Log.d("setDrmType", drmType); UUID drmUUID = Util.getDrmUuid(drmType); videoView.setDrmType(drmUUID); videoView.setDrmLicenseUrl(drmLicenseServer); @@ -138,9 +138,9 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); } videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); - videoView.setUseTextureView(false); } - + Log.d("Disabling TextureView (needed for DRM)"); + videoView.setUseTextureView(false); } } } From b61e02ac3ef64e61f6d89eb6f0b7b9721780c9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 17:24:27 +0100 Subject: [PATCH 013/101] Update Readme --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 9ee34399ce..b591d44303 100644 --- a/README.md +++ b/README.md @@ -705,6 +705,38 @@ Note: Using this feature adding an entry for NSAppleMusicUsageDescription to you Platforms: iOS +##### Explicit mimetype for the stream + +Provide a member `type` with value (`mpd`/`m3u8`/`ism`) inside the source object. + +Example: +``` +source={{ uri: 'http://host-serving-a-type-different-than-the-extension.ism/manifest(format=mpd-time-csf)', +type: 'mpd' }} +``` + +##### Provide DRM data + +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`. + +Example: +``` +source={{ + uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', + drm: { + type: 'widevine', + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' + }, + } +}} +``` + +Platforms: Android + ###### Other protocols The following other types are supported on some platforms, but aren't fully documented yet: From dcbf469dd598088d5af1ef38ab5b12a9026acd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 17:37:13 +0100 Subject: [PATCH 014/101] avoid props change on textureView when using DRM --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 3 ++- 1 file changed, 2 insertions(+), 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 e8ae17685d..85d692d9bf 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1108,7 +1108,8 @@ public void setFullscreen(boolean fullscreen) { } public void setUseTextureView(boolean useTextureView) { - exoPlayerView.setUseTextureView(useTextureView); + boolean finallyUseTextureView = useTextureView && this.drmUUID == null; + exoPlayerView.setUseTextureView(finallyUseTextureView); } public void setHideShutterView(boolean hideShutterView) { From 29f5ad316c68a4711dc31a9d40dd32728172fc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 14 Jan 2019 17:51:15 +0100 Subject: [PATCH 015/101] remove unneeded tabulations --- .../brentvatne/exoplayer/ReactExoplayerView.java | 2 +- .../exoplayer/ReactExoplayerViewManager.java | 12 ++++++------ .../src/main/res/values/strings.xml | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 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 85d692d9bf..2b83ce7dbb 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -194,7 +194,7 @@ public ReactExoplayerView(ThemedReactContext context) { themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); -// initializePlayer(); + // initializePlayer(); } 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 958ef597a6..c4be6ca261 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -146,15 +146,15 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src } } else { int identifier = context.getResources().getIdentifier( - uriString, - "drawable", - context.getPackageName() + uriString, + "drawable", + context.getPackageName() ); if (identifier == 0) { identifier = context.getResources().getIdentifier( - uriString, - "raw", - context.getPackageName() + uriString, + "raw", + context.getPackageName() ); } if (identifier > 0) { diff --git a/android-exoplayer/src/main/res/values/strings.xml b/android-exoplayer/src/main/res/values/strings.xml index a491271476..1f037779dd 100644 --- a/android-exoplayer/src/main/res/values/strings.xml +++ b/android-exoplayer/src/main/res/values/strings.xml @@ -1,19 +1,19 @@ - This device does not provide a decoder for %1$s + This device does not provide a decoder for %1$s - This device does not provide a secure decoder for %1$s + This device does not provide a secure decoder for %1$s - Unable to query device decoders + Unable to query device decoders - Unable to instantiate decoder %1$s - - Protected content not supported on API levels below 18 + Unable to instantiate decoder %1$s + + Protected content not supported on API levels below 18 Unrecognized media format - This device does not support the required DRM scheme + This device does not support the required DRM scheme - An unknown DRM error occurred + An unknown DRM error occurred 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 016/101] 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 017/101] 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 018/101] 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 019/101] 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 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 020/101] 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 021/101] 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 022/101] 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 023/101] 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 b66d345728481d958c39e43f6370724c83c011c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 21 Jan 2019 09:16:34 +0100 Subject: [PATCH 024/101] DRM type as enum + propType --- DRMType.js | 6 ++++++ Video.js | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 DRMType.js diff --git a/DRMType.js b/DRMType.js new file mode 100644 index 0000000000..473536b249 --- /dev/null +++ b/DRMType.js @@ -0,0 +1,6 @@ +export default { + WIDEVINE: 'widevine', + PLAYREADY: 'playready', + CLEARKEY: 'clearkey', + FAIRPLAY: 'fairplay' +}; diff --git a/Video.js b/Video.js index b05238a939..dfebb06a38 100644 --- a/Video.js +++ b/Video.js @@ -4,6 +4,7 @@ import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; +import DRMType from './DRMType'; import VideoResizeMode from './VideoResizeMode.js'; const styles = StyleSheet.create({ @@ -244,7 +245,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, @@ -334,7 +336,14 @@ Video.propTypes = { /* Wrapper component */ source: PropTypes.oneOfType([ PropTypes.shape({ - uri: PropTypes.string + uri: PropTypes.string, + drm: PropTypes.shape({ + type: PropTypes.oneOf([ + DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY + ]), + licenseServer: PropTypes.string, + headers: PropTypes.shape({}) + }) }), // Opaque type returned by require('./video.mp4') PropTypes.number 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 025/101] 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 026/101] 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 027/101] 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 028/101] 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 029/101] 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 030/101] 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 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 031/101] 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 032/101] 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 033/101] 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 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 034/101] 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 035/101] 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 036/101] 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 037/101] 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 038/101] 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 039/101] 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 040/101] 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 041/101] 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 042/101] 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 043/101] 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 044/101] 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 045/101] 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 046/101] 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 047/101] 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 048/101] 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 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 049/101] 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 050/101] 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 051/101] 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 052/101] 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 403b01e81f862480cb475ff59f5e019a8cf54bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 25 Jan 2019 12:51:24 +0100 Subject: [PATCH 053/101] change to next the version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ad0cb24f..6da88eae81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Changelog -### Version 4.3.1 +### next * Support DRM for iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445) ### Version 4.3.0 From 4e4d8b182014b5012cd1a21629e0e914ac7522a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 25 Jan 2019 13:22:09 +0100 Subject: [PATCH 054/101] error enum --- ios/Video/RCTVideo.h | 12 ++++++++++++ ios/Video/RCTVideo.m | 18 +++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index cdffa0d11d..bcff28bf57 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -40,6 +40,18 @@ @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; @property (nonatomic, copy) RCTBubblingEventBlock onGetLicense; +typedef NS_ENUM(NSInteger, RCTVideoError) { + RCTVideoErrorFromJSPart, + RCTVideoErrorLicenseRequestNotOk, + RCTVideoErrorNoDataFromLicenseRequest, + RCTVideoErrorNoSPC, + RCTVideoErrorNoDataRequest, + RCTVideoErrorNoCertificateData, + RCTVideoErrorNoCertificateURL, + RCTVideoErrorNoFairplayDRM, + RCTVideoErrorNoDRMData +}; + - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0e12f691d7..2ae8705287 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1509,7 +1509,7 @@ - (void)setLicenseResult:(NSString *)license { - (BOOL)setLicenseResultError:(NSString *)error { if (_loadingRequest != nil) { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1336 + code: RCTVideoErrorFromJSPart userInfo: @{ NSLocalizedDescriptionKey: error, NSLocalizedFailureReasonErrorKey: error, @@ -1634,7 +1634,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { if([httpResponse statusCode] != 200){ NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1337 + code: RCTVideoErrorLicenseRequestNotOk userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining license.", NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"License server responded with status code %li", (long)[httpResponse statusCode]], @@ -1647,7 +1647,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { [loadingRequest finishLoading]; } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1338 + code: RCTVideoErrorNoDataFromLicenseRequest userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", @@ -1665,7 +1665,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1339 + code: RCTVideoErrorNoSPC userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining license.", NSLocalizedFailureReasonErrorKey: @"No spc received.", @@ -1677,7 +1677,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1340 + code: RCTVideoErrorNoDataRequest userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", @@ -1688,7 +1688,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1341 + code: RCTVideoErrorNoCertificateData userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", @@ -1699,7 +1699,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1342 + code: RCTVideoErrorNoCertificateURL userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM License.", NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", @@ -1710,7 +1710,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1343 + code: RCTVideoErrorNoFairplayDRM userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", @@ -1722,7 +1722,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: -1344 + code: RCTVideoErrorNoDRMData userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: @"No drm object found.", From d5f18134dcf70ba6bd9b3485708944e12480a9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 25 Jan 2019 13:25:34 +0100 Subject: [PATCH 055/101] typo --- Video.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Video.js b/Video.js index db670b7a42..93cfa1b250 100644 --- a/Video.js +++ b/Video.js @@ -228,7 +228,7 @@ export default class Video extends Component { }); } } else { - NativeModules.VideoManager.setLicenseError("No enough data for license override", findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError("Not enough data for license override", findNodeHandle(this._root)); } } @@ -294,7 +294,7 @@ export default class Video extends Component { onAudioBecomingNoisy: this._onAudioBecomingNoisy, onGetLicense: this._onGetLicense, }); - + const posterStyle = { ...StyleSheet.absoluteFillObject, resizeMode: this.props.posterResizeMode || 'contain', From c0cd82ee58ebdf60a4b27cde071d500e3ffe5330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 25 Jan 2019 13:27:58 +0100 Subject: [PATCH 056/101] no spc error --- Video.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Video.js b/Video.js index 93cfa1b250..dc4a25706c 100644 --- a/Video.js +++ b/Video.js @@ -226,6 +226,8 @@ export default class Video extends Component { })).catch((error) => { NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); }); + } else { + NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); } } else { NativeModules.VideoManager.setLicenseError("Not enough data for license override", findNodeHandle(this._root)); From 9b3e4e71f2c2330ff7701949505c9f60f209ade7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 28 Jan 2019 15:08:09 +0100 Subject: [PATCH 057/101] fix for tvOS when using Pods --- react-native-video.podspec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/react-native-video.podspec b/react-native-video.podspec index c5e09c1137..5833a94e11 100644 --- a/react-native-video.podspec +++ b/react-native-video.podspec @@ -32,4 +32,8 @@ Pod::Spec.new do |s| s.dependency "React" s.default_subspec = "Video" + + s.xcconfig = { + 'OTHER_LDFLAGS': '-ObjC', + } end From 477607b0c566824a276cda37ad2158da79fca77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 29 Jan 2019 14:41:16 +0100 Subject: [PATCH 058/101] fix headers --- ios/Video/RCTVideo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index bcff28bf57..ff1edd8d53 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -58,6 +58,6 @@ typedef NS_ENUM(NSInteger, RCTVideoError) { - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; -- (void)setLicenseResultError:(NSString * )error; +- (BOOL)setLicenseResultError:(NSString * )error; @end From 5f6425f249cdc31c1bd3861b6ad1738c7a4972f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 29 Jan 2019 14:41:32 +0100 Subject: [PATCH 059/101] fix for non playeritem errors --- 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 2ae8705287..9f950ae8b9 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -653,7 +653,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N @"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}, + @"domain": _playerItem != nil && _playerItem.error != nil ? _playerItem.error.domain : @"RTCVideo"}, @"target": self.reactTag}); } } else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) { From b8ccec886408c07668fd3b6113fedaf9503bf20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 1 Feb 2019 18:26:06 +0100 Subject: [PATCH 060/101] bubble DRM error on Android --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 1 + 1 file changed, 1 insertion(+) 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 c242c4296d..ced361f8b4 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -292,6 +292,7 @@ private void initializePlayer() { : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); Log.d("Drm Info", getResources().getString(errorStringId)); + eventEmitter.error(getResources().getString(errorStringId), e); return; } } From 56d2af58184ad05b7c81233a3da96b94861a054c Mon Sep 17 00:00:00 2001 From: Roman Hosek <> Date: Mon, 4 Feb 2019 13:13:11 +0100 Subject: [PATCH 061/101] fix detox tests --- android-exoplayer/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index a532956d85..88c911f159 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -14,6 +14,11 @@ android { versionCode 1 versionName "1.0" } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } } dependencies { From 4dd1c73fde81c3ec60923ef3b858d1a5d7b3a961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 12 Feb 2019 15:11:49 +0100 Subject: [PATCH 062/101] improve props --- Video.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Video.js b/Video.js index dc4a25706c..01afe29cd0 100644 --- a/Video.js +++ b/Video.js @@ -221,16 +221,14 @@ export default class Video extends Component { if (result !== undefined) { NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); } else { - NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); } })).catch((error) => { - NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); }); } else { - NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); } - } else { - NativeModules.VideoManager.setLicenseError("Not enough data for license override", findNodeHandle(this._root)); } } @@ -294,7 +292,7 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, - onGetLicense: this._onGetLicense, + onGetLicense: nativeProps.source && nativeProps.source.drm && nativeProps.source.drm.getLicense && this._onGetLicense, }); const posterStyle = { @@ -368,7 +366,10 @@ Video.propTypes = { DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY ]), licenseServer: PropTypes.string, - headers: PropTypes.shape({}) + headers: PropTypes.shape({}), + base64Certificate: PropTypes.bool, + certificateUrl: PropTypes.string, + getLicense: PropTypes.func, }) }), // Opaque type returned by require('./video.mp4') From e5cd51a8f664638cd64a23ee7f6542dba1be0992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 12 Feb 2019 15:13:34 +0100 Subject: [PATCH 063/101] allow base64 cer response --- ios/Video/RCTVideo.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9f950ae8b9..f76ca82c78 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1591,6 +1591,10 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { if (certificateStringUrl != nil) { NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; + if ([_drm objectForKey:@"base64Certificate"]) { + certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; + } + if (certificateData != nil) { NSData *contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding]; AVAssetResourceLoadingDataRequest *dataRequest = [loadingRequest dataRequest]; From d981f80e2eb4a22aedd7fc4fccec64be52cad244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 12 Feb 2019 15:25:43 +0100 Subject: [PATCH 064/101] Update iOS readme --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cd4e10277..4d10d95ce4 100644 --- a/README.md +++ b/README.md @@ -722,7 +722,47 @@ type: 'mpd' }} 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`. +DRM options are `type`, `licenseServer`, `headers`, and for iOS there are additional ones: `base64Certificate`, `getLicense`, `certificateUrl`. + +###### base64Certificate + +Whether or not the certificate url returns it on base64. + +Platforms: iOS + +###### certificateUrl + +URL to fetch a valid certificatefor FairPlay. + +Platforms: iOS + +###### getLicense + +Overridable method to acquire a license manually. It recieves as argument the `spc` string. + +Example: + +```js +getLicense: (spcString) => { + const base64spc = Base64.encode(spcString); + const formData = new FormData(); + formData.append('spc', base64spc); + return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { + method: 'POST', + headers: { + 'pallycon-customdata-v2': 'eyJkcm1fdHlwZSI6IkZhaXJQbGF5Iiwic2l0ZV9pZCI6IkJMMkciLCJ1c2VyX2lkIjoiMTIxMjc5IiwiY2lkIjoiYXNzZXRfMTQ5OTAiLCJ0b2tlbiI6IjBkQVVLSEQ4bm5pTStJeDJ2Y09HVStzSWRWY2wvSEdxSjdEanNZK1laazZKdlhLczRPM3BVNitVVnV3dkNvLzRyc2lIUi9PSnY4RDJncHBBN0cycnRGdy9pVFMvTWNZaVhML2VLOXdMMXFVM05VbXlFL25RdVV3Tm5mOXI2YlArUjUvRDZxOU5vZmZtTGUybmo4VGphQ3UwUUFQZlVqVzRFREE4eDNUYlI5cXZOa0pKVHdmNTA5NE5UYXY5VzJxbFp0MmczcDNMcUV0RkNMK0N5dFBZSWJEN2ZBUmR1ZzkvVTdiMXB1Y3pndTBqRjg3QnlMU0tac0J3TUpYd2xSZkxTTTZJSzRlWHMvNC9RWU4rVXhnR3ozVTgxODl4aHhWS0RJaDdBcGFkQVllVUZUMWJIVVZBSVVRQms0cjRIQ28yczIydWJvVnVLaVNQazdvYmtJckVNQT09IiwidGltZXN0YW1wIjoiMjAxOS0wMi0xMlQwNjoxODo0MloiLCJoYXNoIjoiMThqcDBDVTdOaUJ3WFdYVC8zR2lFN3R0YXVRWlZ5SjVSMUhSK2J2Um9JWT0ifQ==', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }).then(response => response.text()).then((response) => { + return response; + }).catch((error) => { + console.error('Error', error); + }); +} +``` + +Platforms: iOS ###### headers From 801dce430656adde2677ba95a89622bc563163d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 14 Feb 2019 11:37:51 +0100 Subject: [PATCH 065/101] more readable java version for gradle --- android-exoplayer/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 84bfbda4da..d257f53d33 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -16,8 +16,8 @@ android { } compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } From 59b541c3a970728ee963bc7fdce088754bba9356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Thu, 21 Feb 2019 17:17:47 +0100 Subject: [PATCH 066/101] fix merge --- ios/Video/RCTVideo.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index e195117a4e..3cd4eec68e 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -16,11 +16,7 @@ #if __has_include() @interface RCTVideo : UIView #else -<<<<<<< HEAD -@interface RCTVideo : UIView -======= -@interface RCTVideo : UIView ->>>>>>> rnv-master +@interface RCTVideo : UIView #endif @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; From 5b92ea07cf582fbf95339f637885124e5229d3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 22 Feb 2019 13:32:44 +0100 Subject: [PATCH 067/101] remove extra curly brace --- ios/Video/RCTVideoManager.m | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 9724d340f1..e7d9b4f5a3 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -107,7 +107,6 @@ - (dispatch_queue_t)methodQueue } }]; }; -} RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); From a7780f9a398b840010cbca6dfa6ea55339f91b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 22 Feb 2019 14:06:17 +0100 Subject: [PATCH 068/101] add missing curly after merge --- ios/Video/RCTVideo.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 28606c726d..2bde5f8c26 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1793,6 +1793,7 @@ - (NSData *)base64DataFromBase64String: (NSString *)base64String { return base64Data; } return nil; +} #pragma mark - Picture in Picture - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { From decb56e6899c35975e474f8f160cdae2c43e9502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 5 Mar 2019 18:12:46 +0100 Subject: [PATCH 069/101] support for controls --- .../exoplayer/ReactExoplayerView.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 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 f39f24f5fb..3ed4fd8215 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -159,6 +159,7 @@ class ReactExoplayerView extends FrameLayout implements private UUID drmUUID = null; private String drmLicenseUrl = null; private String[] drmLicenseHeader = null; + private boolean controls; // \ End props // React @@ -197,8 +198,6 @@ public ReactExoplayerView(ThemedReactContext context) { audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); - - // initializePlayer(); } @@ -414,6 +413,7 @@ private void initializePlayer() { // Initializing the playerControlView initializePlayerControl(); + handleControls(); } @@ -1256,10 +1256,19 @@ public void onDrmKeysRemoved() { * @param controls Controls prop, if true enable controls, if false disable them */ public void setControls(boolean controls) { - if (controls && exoPlayerView != null) { - addPlayerControl(); - } else if (getChildAt(1) instanceof PlayerControlView && exoPlayerView != null) { - removeViewAt(1); + this.controls = controls; + if (playerControlView != null) { + this.handleControls(); + } + } + + private void handleControls() { + if (exoPlayerView != null) { + if (this.controls && !(getChildAt(1) instanceof PlayerControlView)) { + addPlayerControl(); + } else if (!this.controls && getChildAt(1) instanceof PlayerControlView) { + removeViewAt(1); + } } } } From 9b5ced246b750aec89f0c66121e32bee4738f27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 15 Apr 2019 12:05:56 +0200 Subject: [PATCH 070/101] split DRM doc --- DRM.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 122 ++---------------------------------------------------- 2 files changed, 123 insertions(+), 118 deletions(-) create mode 100644 DRM.md diff --git a/DRM.md b/DRM.md new file mode 100644 index 0000000000..d78daf2f09 --- /dev/null +++ b/DRM.md @@ -0,0 +1,119 @@ +# DRM + +## Provide DRM data (only tested with http/https assets) + +You can provide some configuration to allow DRM playback. +This feature will disable the use of `TextureView` on Android. +DRM options are `type`, `licenseServer`, `headers`, and for iOS there are additional ones: `base64Certificate`, `getLicense`, `certificateUrl`. + +### base64Certificate + +Whether or not the certificate url returns it on base64. + +Platforms: iOS + +### certificateUrl + +URL to fetch a valid certificatefor FairPlay. + +Platforms: iOS + +### getLicense + +Overridable method to acquire a license manually. It recieves as argument the `spc` string. + +Example: + +```js +getLicense: (spcString) => { + const base64spc = Base64.encode(spcString); + const formData = new FormData(); + formData.append('spc', base64spc); + return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { + method: 'POST', + headers: { + 'pallycon-customdata-v2': 'eyJkcm1fdHlwZSI6IkZhaXJQbGF5Iiwic2l0ZV9pZCI6IkJMMkciLCJ1c2VyX2lkIjoiMTIxMjc5IiwiY2lkIjoiYXNzZXRfMTQ5OTAiLCJ0b2tlbiI6IjBkQVVLSEQ4bm5pTStJeDJ2Y09HVStzSWRWY2wvSEdxSjdEanNZK1laazZKdlhLczRPM3BVNitVVnV3dkNvLzRyc2lIUi9PSnY4RDJncHBBN0cycnRGdy9pVFMvTWNZaVhML2VLOXdMMXFVM05VbXlFL25RdVV3Tm5mOXI2YlArUjUvRDZxOU5vZmZtTGUybmo4VGphQ3UwUUFQZlVqVzRFREE4eDNUYlI5cXZOa0pKVHdmNTA5NE5UYXY5VzJxbFp0MmczcDNMcUV0RkNMK0N5dFBZSWJEN2ZBUmR1ZzkvVTdiMXB1Y3pndTBqRjg3QnlMU0tac0J3TUpYd2xSZkxTTTZJSzRlWHMvNC9RWU4rVXhnR3ozVTgxODl4aHhWS0RJaDdBcGFkQVllVUZUMWJIVVZBSVVRQms0cjRIQ28yczIydWJvVnVLaVNQazdvYmtJckVNQT09IiwidGltZXN0YW1wIjoiMjAxOS0wMi0xMlQwNjoxODo0MloiLCJoYXNoIjoiMThqcDBDVTdOaUJ3WFdYVC8zR2lFN3R0YXVRWlZ5SjVSMUhSK2J2Um9JWT0ifQ==', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }).then(response => response.text()).then((response) => { + return response; + }).catch((error) => { + console.error('Error', error); + }); +} +``` + +Platforms: iOS + +### headers + +You can customize headers send to the licenseServer. + +Example: + +```js +source={{ + uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', + drm: { + type: 'widevine', //or DRMType.WIDEVINE + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' + }, + } +}} +``` + +### licenseServer + +The URL pointing to the licenseServer that will provide the authorization to play the protected stream. + +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` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. + With this prop you can override the license acquisition flow, as an example: + +```js + getLicense: (spcString) => { + const base64spc = 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 + +### 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 \ No newline at end of file diff --git a/README.md b/README.md index cc02a1ae84..e35c6b975f 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,10 @@ For Android MediaPlayer, you will need to build your own controls or use a packa Platforms: Android ExoPlayer, iOS, react-native-dom +### DRM + +To setup DRM please follow [this guide](./DRM.md) + #### filter Add video filter * **FilterType.NONE (default)** - No Filter @@ -742,124 +746,6 @@ source={{ uri: 'http://host-serving-a-type-different-than-the-extension.ism/mani type: 'mpd' }} ``` -##### Provide DRM data (only tested with http/https assets) - -You can provide some configuration to allow DRM playback. -This feature will disable the use of `TextureView` on Android. -DRM options are `type`, `licenseServer`, `headers`, and for iOS there are additional ones: `base64Certificate`, `getLicense`, `certificateUrl`. - -###### base64Certificate - -Whether or not the certificate url returns it on base64. - -Platforms: iOS - -###### certificateUrl - -URL to fetch a valid certificatefor FairPlay. - -Platforms: iOS - -###### getLicense - -Overridable method to acquire a license manually. It recieves as argument the `spc` string. - -Example: - -```js -getLicense: (spcString) => { - const base64spc = Base64.encode(spcString); - const formData = new FormData(); - formData.append('spc', base64spc); - return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { - method: 'POST', - headers: { - 'pallycon-customdata-v2': 'eyJkcm1fdHlwZSI6IkZhaXJQbGF5Iiwic2l0ZV9pZCI6IkJMMkciLCJ1c2VyX2lkIjoiMTIxMjc5IiwiY2lkIjoiYXNzZXRfMTQ5OTAiLCJ0b2tlbiI6IjBkQVVLSEQ4bm5pTStJeDJ2Y09HVStzSWRWY2wvSEdxSjdEanNZK1laazZKdlhLczRPM3BVNitVVnV3dkNvLzRyc2lIUi9PSnY4RDJncHBBN0cycnRGdy9pVFMvTWNZaVhML2VLOXdMMXFVM05VbXlFL25RdVV3Tm5mOXI2YlArUjUvRDZxOU5vZmZtTGUybmo4VGphQ3UwUUFQZlVqVzRFREE4eDNUYlI5cXZOa0pKVHdmNTA5NE5UYXY5VzJxbFp0MmczcDNMcUV0RkNMK0N5dFBZSWJEN2ZBUmR1ZzkvVTdiMXB1Y3pndTBqRjg3QnlMU0tac0J3TUpYd2xSZkxTTTZJSzRlWHMvNC9RWU4rVXhnR3ozVTgxODl4aHhWS0RJaDdBcGFkQVllVUZUMWJIVVZBSVVRQms0cjRIQ28yczIydWJvVnVLaVNQazdvYmtJckVNQT09IiwidGltZXN0YW1wIjoiMjAxOS0wMi0xMlQwNjoxODo0MloiLCJoYXNoIjoiMThqcDBDVTdOaUJ3WFdYVC8zR2lFN3R0YXVRWlZ5SjVSMUhSK2J2Um9JWT0ifQ==', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: formData - }).then(response => response.text()).then((response) => { - return response; - }).catch((error) => { - console.error('Error', error); - }); -} -``` - -Platforms: iOS - -###### headers - -You can customize headers send to the licenseServer. - -Example: - -```js -source={{ - uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', - drm: { - type: 'widevine', //or DRMType.WIDEVINE - licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', - headers: { - 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' - }, - } -}} -``` - -###### licenseServer - -The URL pointing to the licenseServer that will provide the authorization to play the protected stream. - -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` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. - With this prop you can override the license acquisition flow, as an example: - -```js - getLicense: (spcString) => { - const base64spc = 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 - -###### 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 6e3e6537b11d8c5ec36f78091a9aaa604e86a547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 15 Apr 2019 16:34:26 +0200 Subject: [PATCH 071/101] decoupling drm from source prop --- .../exoplayer/ReactExoplayerViewManager.java | 61 ++++++++++--------- ios/Video/RCTVideo.m | 8 ++- 2 files changed, 37 insertions(+), 32 deletions(-) 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 db8b34e206..aae87f556a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -31,10 +31,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager drmKeyRequestPropertiesList = new ArrayList<>(); + ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); + while (itr.hasNextKey()) { + String key = itr.nextKey(); + drmKeyRequestPropertiesList.add(key); + drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + } + videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + } + Log.d("setDrm", "Disabling TextureView (needed for DRM)"); + videoView.setUseTextureView(false); + } + } + } + @ReactProp(name = PROP_SRC) public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { Log.d("SetSrc", "Set Source"); @@ -110,8 +138,6 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; Map headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; - ReadableMap drm = src.hasKey(PROP_SRC_DRM) ? src.getMap(PROP_SRC_DRM) : null; - if (TextUtils.isEmpty(uriString)) { return; @@ -122,29 +148,6 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src if (srcUri != null) { videoView.setSrc(srcUri, extension, headers); - if (drm != null && drm.hasKey(PROP_SRC_DRM_TYPE)) { - String drmType = drm.hasKey(PROP_SRC_DRM_TYPE) ? drm.getString(PROP_SRC_DRM_TYPE) : null; - String drmLicenseServer = drm.hasKey(PROP_SRC_DRM_LICENSESERVER) ? drm.getString(PROP_SRC_DRM_LICENSESERVER) : null; - ReadableMap drmHeaders = drm.hasKey(PROP_SRC_DRM_HEADERS) ? drm.getMap(PROP_SRC_DRM_HEADERS) : null; - if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { - Log.d("setDrmType", drmType); - UUID drmUUID = Util.getDrmUuid(drmType); - videoView.setDrmType(drmUUID); - videoView.setDrmLicenseUrl(drmLicenseServer); - if (drmHeaders != null) { - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); - while (itr.hasNextKey()) { - String key = itr.nextKey(); - drmKeyRequestPropertiesList.add(key); - drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); - } - videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); - } - Log.d("setDrm", "Disabling TextureView (needed for DRM)"); - videoView.setUseTextureView(false); - } - } } } else { int identifier = context.getResources().getIdentifier( diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 35c3beb32a..02776a2790 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -381,8 +381,6 @@ - (void)setSrc:(NSDictionary *)source [self addPlayerTimeObserver]; - _drm = [source objectForKey:@"drm"]; - //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { id uri = [source objectForKey:@"uri"]; @@ -390,8 +388,8 @@ - (void)setSrc:(NSDictionary *)source 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"]]}, + @"drm": _drm ? _drm : [NSNull null], @"target": self.reactTag }); } @@ -400,6 +398,10 @@ - (void)setSrc:(NSDictionary *)source _videoLoadStarted = YES; } +- (void)setDrm:(NSDictionary *)drm { + _drm = drm +} + - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; From 24730ed0c31d2aec8666c50162868bda2693b692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 15 Apr 2019 17:43:33 +0200 Subject: [PATCH 072/101] ios preparePlayback --- Video.js | 14 ++++++++++++++ ios/Video/RCTVideo.h | 1 + ios/Video/RCTVideo.m | 14 ++++++-------- ios/Video/RCTVideoManager.m | 12 ++++++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Video.js b/Video.js index 1f0fde4d5b..4e8d4cf3f5 100644 --- a/Video.js +++ b/Video.js @@ -25,6 +25,20 @@ export default class Video extends Component { }; } + componentDidMount() { + this.preparePlayback(); + } + + componentDidUpdate(prevProps) { + if (prevProps.src !== this.props.src || prevProps.drm !== this.props.drm) { + this.preparePlayback(); + } + } + + preparePlayback() { + NativeModules.VideoManager.preparePlayback(findNodeHandle(this._root)); + } + setNativeProps(nativeProps) { this._root.setNativeProps(nativeProps); } diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index afad5a1ad4..2f7e15799a 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -63,5 +63,6 @@ typedef NS_ENUM(NSInteger, RCTVideoError) { - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; - (BOOL)setLicenseResultError:(NSString * )error; +- (void)preparePlayback; @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 02776a2790..582d467c65 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -347,11 +347,14 @@ - (void)setSrc:(NSDictionary *)source [self removePlayerLayer]; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; +} - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ +- (void)setDrm:(NSDictionary *)drm { + _drm = drm; +} - // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { +- (void)preparePlayback { + [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { _playerItem = playerItem; [self addPlayerItemObservers]; [self setFilter:_filterName]; @@ -394,14 +397,9 @@ - (void)setSrc:(NSDictionary *)source }); } }]; - }); _videoLoadStarted = YES; } -- (void)setDrm:(NSDictionary *)drm { - _drm = drm -} - - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index e7d9b4f5a3..32ac104bb6 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -107,6 +107,18 @@ - (dispatch_queue_t)methodQueue } }]; }; +RCT_REMAP_METHOD(preparePlayback, + 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 preparePlayback]; + } + }]; +}; RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); From 5725c625240a3ba29f2025a181cdd7972af42172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Mon, 15 Apr 2019 18:05:20 +0200 Subject: [PATCH 073/101] fix var name --- ios/Video/RCTVideo.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 582d467c65..a5d6c42858 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -354,7 +354,7 @@ - (void)setDrm:(NSDictionary *)drm { } - (void)preparePlayback { - [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { + [self playerItemForSource:_source withCallback:^(AVPlayerItem * playerItem) { _playerItem = playerItem; [self addPlayerItemObservers]; [self setFilter:_filterName]; @@ -386,12 +386,12 @@ - (void)preparePlayback { //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { - id uri = [source objectForKey:@"uri"]; - id type = [source objectForKey:@"type"]; + id uri = [_source objectForKey:@"uri"]; + id type = [_source objectForKey:@"type"]; self.onVideoLoadStart(@{@"src": @{ @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, + @"isNetwork": [NSNumber numberWithBool:(bool)[_source objectForKey:@"isNetwork"]]}, @"drm": _drm ? _drm : [NSNull null], @"target": self.reactTag }); From 4d12d1d07b18dcdaca8d12ca2e67098335ec1ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 16 Apr 2019 09:23:36 +0200 Subject: [PATCH 074/101] drm prop --- Video.js | 2 +- ios/Video/RCTVideo.m | 86 +++++++++++++++++++------------------ ios/Video/RCTVideoManager.m | 1 + 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Video.js b/Video.js index 4e8d4cf3f5..ec02cbf00e 100644 --- a/Video.js +++ b/Video.js @@ -30,7 +30,7 @@ export default class Video extends Component { } componentDidUpdate(prevProps) { - if (prevProps.src !== this.props.src || prevProps.drm !== this.props.drm) { + if (this.props.source !== null && (prevProps.source !== this.props.source || prevProps.drm !== this.props.drm)) { this.preparePlayback(); } } diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a5d6c42858..efe74f9819 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -354,49 +354,53 @@ - (void)setDrm:(NSDictionary *)drm { } - (void)preparePlayback { - [self playerItemForSource:_source withCallback:^(AVPlayerItem * playerItem) { - _playerItem = playerItem; - [self addPlayerItemObservers]; - [self setFilter:_filterName]; - [self setMaxBitRate:_maxBitRate]; - - [_player pause]; - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; - - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (_isExternalPlaybackActiveObserverRegistered) { - [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - _isExternalPlaybackActiveObserverRegistered = NO; - } - - _player = [AVPlayer playerWithPlayerItem:_playerItem]; - _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ + + // perform on next run loop, otherwise other passed react-props may not be set + [self playerItemForSource:_source withCallback:^(AVPlayerItem * playerItem) { + _playerItem = playerItem; + [self addPlayerItemObservers]; + [self setFilter:_filterName]; + [self setMaxBitRate:_maxBitRate]; - [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; - _playbackRateObserverRegistered = YES; - - [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; - _isExternalPlaybackActiveObserverRegistered = YES; - - [self addPlayerTimeObserver]; + [_player pause]; + [_playerViewController.view removeFromSuperview]; + _playerViewController = nil; + + if (_playbackRateObserverRegistered) { + [_player removeObserver:self forKeyPath:playbackRate context:nil]; + _playbackRateObserverRegistered = NO; + } + if (_isExternalPlaybackActiveObserverRegistered) { + [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; + _isExternalPlaybackActiveObserverRegistered = NO; + } + + _player = [AVPlayer playerWithPlayerItem:_playerItem]; + _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + + [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; + _playbackRateObserverRegistered = YES; - //Perform on next run loop, otherwise onVideoLoadStart is nil - if (self.onVideoLoadStart) { - id uri = [_source objectForKey:@"uri"]; - id type = [_source objectForKey:@"type"]; - self.onVideoLoadStart(@{@"src": @{ - @"uri": uri ? uri : [NSNull null], - @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[_source objectForKey:@"isNetwork"]]}, - @"drm": _drm ? _drm : [NSNull null], - @"target": self.reactTag - }); - } - }]; + [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; + _isExternalPlaybackActiveObserverRegistered = YES; + + [self addPlayerTimeObserver]; + + //Perform on next run loop, otherwise onVideoLoadStart is nil + if (self.onVideoLoadStart) { + id uri = [_source objectForKey:@"uri"]; + id type = [_source objectForKey:@"type"]; + self.onVideoLoadStart(@{@"src": @{ + @"uri": uri ? uri : [NSNull null], + @"type": type ? type : [NSNull null], + @"isNetwork": [NSNumber numberWithBool:(bool)[_source objectForKey:@"isNetwork"]]}, + @"drm": _drm ? _drm : [NSNull null], + @"target": self.reactTag + }); + } + }]; + }); _videoLoadStarted = YES; } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 32ac104bb6..6e0b6e49f5 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -19,6 +19,7 @@ - (dispatch_queue_t)methodQueue } RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(maxBitRate, float); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); From 4210929548f71fa5efdb7bb86ee35d001caca870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 16 Apr 2019 11:30:48 +0200 Subject: [PATCH 075/101] test async --- Video.js | 6 +++++- ios/Video/RCTVideo.m | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Video.js b/Video.js index ec02cbf00e..36254a160e 100644 --- a/Video.js +++ b/Video.js @@ -30,7 +30,11 @@ export default class Video extends Component { } componentDidUpdate(prevProps) { - if (this.props.source !== null && (prevProps.source !== this.props.source || prevProps.drm !== this.props.drm)) { + if (this.props.source !== null && this.props.source.uri) { + if (this.props.source.uri !== prevProps.source.uri) { + this.preparePlayback(); + } + } else { this.preparePlayback(); } } diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index efe74f9819..5b78e6fd7e 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1617,12 +1617,15 @@ - (BOOL)finishLoadingWithError:(NSError *)error { @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], - @"domain": _playerItem.error.domain}, + @"domain": _playerItem.error == nil ? @"RCTVideo" : _playerItem.error.domain}, @"target": self.reactTag}); } } return NO; } +- (void)batchDidComplete { + NSLog(@"batchDidComplete"); +} - (BOOL)ensureDirExistsWithPath:(NSString *)path { BOOL isDir = NO; From 3b952dca194ea08a9df8274db8bf7e57b5140b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 16 Apr 2019 12:47:37 +0200 Subject: [PATCH 076/101] preparing android --- .../com/brentvatne/exoplayer/ReactExoplayerView.java | 10 ++++------ .../exoplayer/ReactExoplayerViewManager.java | 5 +++++ ios/Video/RCTVideo.m | 3 --- 3 files changed, 9 insertions(+), 9 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 0c53cbe840..93d0c9d303 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -224,12 +224,6 @@ private void createViews() { addView(exoPlayerView, 0, layoutParams); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - initializePlayer(); - } - @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -1272,4 +1266,8 @@ private void handleControls() { } } } + + public preparePlayback() { + initializePlayer(); + } } 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 aae87f556a..c4da871e61 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -323,6 +323,11 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab } } + @ReactMethod + public void preparePlayback(final ReactExoplayerView videoView) { + videoView.preparePlayback(); + } + private boolean startsWithValidScheme(String uriString) { return uriString.startsWith("http://") || uriString.startsWith("https://") diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 5b78e6fd7e..49332578b0 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1623,9 +1623,6 @@ - (BOOL)finishLoadingWithError:(NSError *)error { } return NO; } -- (void)batchDidComplete { - NSLog(@"batchDidComplete"); -} - (BOOL)ensureDirExistsWithPath:(NSString *)path { BOOL isDir = NO; From 1b680c2dcab3cbe2e9ed3779c8bca174c8a52149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 16 Apr 2019 14:42:34 +0200 Subject: [PATCH 077/101] fixes android --- Video.js | 14 +++++++++----- .../brentvatne/exoplayer/ReactExoplayerView.java | 2 +- .../exoplayer/ReactExoplayerViewManager.java | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Video.js b/Video.js index 36254a160e..d806e069d9 100644 --- a/Video.js +++ b/Video.js @@ -26,16 +26,20 @@ export default class Video extends Component { } componentDidMount() { - this.preparePlayback(); + if (this._root) { + this.preparePlayback(); + } } componentDidUpdate(prevProps) { - if (this.props.source !== null && this.props.source.uri) { - if (this.props.source.uri !== prevProps.source.uri) { + if (this._root) { + if (this.props.source !== null && this.props.source.uri) { + if (this.props.source.uri !== prevProps.source.uri) { + this.preparePlayback(); + } + } else { this.preparePlayback(); } - } else { - this.preparePlayback(); } } 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 93d0c9d303..ac59704bde 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1267,7 +1267,7 @@ private void handleControls() { } } - public preparePlayback() { + public void preparePlayback() { initializePlayer(); } } 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 c4da871e61..d578f9eeb2 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -13,6 +13,7 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.bridge.ReactMethod; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.upstream.RawResourceDataSource; From a22bf4536a70fa794658c8bf27169cf3b749770f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 16 Apr 2019 16:58:10 +0200 Subject: [PATCH 078/101] protect --- Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Video.js b/Video.js index d806e069d9..5a2d10f0e7 100644 --- a/Video.js +++ b/Video.js @@ -44,7 +44,7 @@ export default class Video extends Component { } preparePlayback() { - NativeModules.VideoManager.preparePlayback(findNodeHandle(this._root)); + NativeModules.VideoManager && NativeModules.VideoManager.preparePlayback(findNodeHandle(this._root)); } setNativeProps(nativeProps) { From ce4dedf2000ad1efac97e8cb074ee73d9e731e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 17 Apr 2019 12:51:28 +0200 Subject: [PATCH 079/101] preparePlayback not needed --- Video.js | 22 ------------------- .../exoplayer/ReactExoplayerView.java | 5 +---- .../exoplayer/ReactExoplayerViewManager.java | 5 ----- ios/Video/RCTVideo.h | 1 - ios/Video/RCTVideo.m | 11 ++++------ 5 files changed, 5 insertions(+), 39 deletions(-) diff --git a/Video.js b/Video.js index 5a2d10f0e7..1f0fde4d5b 100644 --- a/Video.js +++ b/Video.js @@ -25,28 +25,6 @@ export default class Video extends Component { }; } - componentDidMount() { - if (this._root) { - this.preparePlayback(); - } - } - - componentDidUpdate(prevProps) { - if (this._root) { - if (this.props.source !== null && this.props.source.uri) { - if (this.props.source.uri !== prevProps.source.uri) { - this.preparePlayback(); - } - } else { - this.preparePlayback(); - } - } - } - - preparePlayback() { - NativeModules.VideoManager && NativeModules.VideoManager.preparePlayback(findNodeHandle(this._root)); - } - setNativeProps(nativeProps) { this._root.setNativeProps(nativeProps); } 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 ac59704bde..8ed58b8d9f 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1233,6 +1233,7 @@ public void onDrmKeysLoaded() { @Override public void onDrmSessionManagerError(Exception e) { Log.d("DRM Info", "onDrmSessionManagerError"); + eventEmitter.error("onDrmSessionManagerError", e); } @Override @@ -1266,8 +1267,4 @@ private void handleControls() { } } } - - public void preparePlayback() { - initializePlayer(); - } } 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 d578f9eeb2..d096b34799 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -324,11 +324,6 @@ public void setBufferConfig(final ReactExoplayerView videoView, @Nullable Readab } } - @ReactMethod - public void preparePlayback(final ReactExoplayerView videoView) { - videoView.preparePlayback(); - } - private boolean startsWithValidScheme(String uriString) { return uriString.startsWith("http://") || uriString.startsWith("https://") diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 2f7e15799a..afad5a1ad4 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -63,6 +63,5 @@ typedef NS_ENUM(NSInteger, RCTVideoError) { - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)setLicenseResult:(NSString * )license; - (BOOL)setLicenseResultError:(NSString * )error; -- (void)preparePlayback; @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 49332578b0..5f22d81b00 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -347,13 +347,6 @@ - (void)setSrc:(NSDictionary *)source [self removePlayerLayer]; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; -} - -- (void)setDrm:(NSDictionary *)drm { - _drm = drm; -} - -- (void)preparePlayback { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ // perform on next run loop, otherwise other passed react-props may not be set @@ -404,6 +397,10 @@ - (void)preparePlayback { _videoLoadStarted = YES; } +- (void)setDrm:(NSDictionary *)drm { + _drm = drm; +} + - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; From 8818b9a11c8ebb117dcc8118263a544a6dbece84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 17 Apr 2019 15:08:27 +0200 Subject: [PATCH 080/101] revert --- .../com/brentvatne/exoplayer/ReactExoplayerView.java | 6 ++++++ ios/Video/RCTVideoManager.m | 12 ------------ 2 files changed, 6 insertions(+), 12 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 8ed58b8d9f..c4a0e925e5 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -224,6 +224,12 @@ private void createViews() { addView(exoPlayerView, 0, layoutParams); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + initializePlayer(); + } + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 6e0b6e49f5..0c5335f650 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -108,18 +108,6 @@ - (dispatch_queue_t)methodQueue } }]; }; -RCT_REMAP_METHOD(preparePlayback, - 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 preparePlayback]; - } - }]; -}; RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); From bdc599ddf52f9601f533971529d4434907bb6233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 17 Apr 2019 16:03:04 +0200 Subject: [PATCH 081/101] improvements shouldwait --- ios/Video/RCTVideo.m | 203 +++++++++++++++++++++++-------------------- 1 file changed, 111 insertions(+), 92 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 5f22d81b00..c2e7dd6b9a 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -33,6 +33,8 @@ @implementation RCTVideo BOOL _playerLayerObserverSet; RCTVideoPlayerViewController *_playerViewController; NSURL *_videoURL; + BOOL _requestingCertificate; + BOOL _requestingCertificateErrored; /* DRM */ NSDictionary *_drm; @@ -526,6 +528,8 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye } dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); [asset.resourceLoader setDelegate:self queue:queue]; + _requestingCertificate = NO; + _requestingCertificateErrored = NO; [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; } @@ -1385,6 +1389,8 @@ - (void)removePlayerLayer [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; _playerLayerObserverSet = NO; } + _requestingCertificate = NO; + _requestingCertificateErrored = NO; _playerLayer = nil; } @@ -1661,6 +1667,11 @@ - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader } - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { + if (_requestingCertificate) { + return YES; + } else if (_requestingCertificateErrored) { + return NO; + } _loadingRequest = loadingRequest; NSURL *url = loadingRequest.request.URL; NSString *contentId = url.host; @@ -1674,117 +1685,125 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { NSString *certificateStringUrl = (NSString *)[_drm objectForKey:@"certificateUrl"]; if (certificateStringUrl != nil) { NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; - if ([_drm objectForKey:@"base64Certificate"]) { - certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; - } - - 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 (spcError != nil) { - 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 YES; - } else if(licenseServer != nil) { - 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]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; + if ([_drm objectForKey:@"base64Certificate"]) { + certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; + } + + 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 *)[self->_drm objectForKey:@"licenseServer"]; + if (spcError != nil) { + [self finishLoadingWithError:spcError]; + self->_requestingCertificateErrored = YES; + } + if (spcData != nil) { + if(self.onGetLicense) { + NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; + self.onGetLicense(@{@"spc": spcStr, + @"target": self.reactTag}); + } else if(licenseServer != nil) { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:licenseServer]]; + // HEADERS + NSDictionary *headers = (NSDictionary *)[self->_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]; - 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 license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - [self finishLoadingWithError:error]; - } else { - if([httpResponse statusCode] != 200){ + // + + [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 license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorLicenseRequestNotOk - 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?" - } - ]; - [self finishLoadingWithError:licenseError]; - } else if (data != nil) { - [dataRequest respondWithData:data]; - [loadingRequest finishLoading]; + [self finishLoadingWithError:error]; + self->_requestingCertificateErrored = YES; } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataFromLicenseRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." - } - ]; - [self finishLoadingWithError:licenseError]; + if([httpResponse statusCode] != 200){ + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorLicenseRequestNotOk + 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?" + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } else if (data != nil) { + [dataRequest respondWithData:data]; + [loadingRequest finishLoading]; + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDataFromLicenseRequest + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + } - - } - }]; - [postDataTask resume]; - return YES; + }]; + [postDataTask resume]; + } + + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoSPC + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: @"No spc received.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; } } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoSPC + code: RCTVideoErrorNoDataRequest userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: @"No spc received.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." } ]; - return [self finishLoadingWithError:licenseError]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; } - } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataRequest + code: RCTVideoErrorNoCertificateData userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." + NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" } ]; - return [self finishLoadingWithError:licenseError]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; } - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateData - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } + }); + return YES; } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" code: RCTVideoErrorNoCertificateURL From a296fd98d912ef17c6d8d5452508e11a46de04d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Wed, 17 Apr 2019 18:19:01 +0200 Subject: [PATCH 082/101] extend doc --- DRM.md | 73 ++++++++++++++++---------------------------------------- Video.js | 7 +++--- 2 files changed, 24 insertions(+), 56 deletions(-) diff --git a/DRM.md b/DRM.md index d78daf2f09..7d63d7a4b3 100644 --- a/DRM.md +++ b/DRM.md @@ -4,25 +4,37 @@ 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`, and for iOS there are additional ones: `base64Certificate`, `getLicense`, `certificateUrl`. -### base64Certificate +DRM object allows this members: + +| Property | Type | Platform | Description | +| --- | --- | --- | --- | +| [`type`](#type) | DRMType | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) | +| [`licenseServer`](#licenseserver) | string | iOS/Android | Specifies the license server URL | +| [`headers`](#headers) | Object | iOS/Android | Specifies the headers send to the license server URL on license acquisition | +| [`contentId`](#contentid) | string | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) | +| [`certificateUrl`](#certificateurl) | string | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file | +| [`base64Certificate`](#base64certificate) | bool | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 | +| [`getLicense`](#getlicense)| function | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream | + +### `base64Certificate` Whether or not the certificate url returns it on base64. Platforms: iOS -### certificateUrl +### `certificateUrl` URL to fetch a valid certificatefor FairPlay. Platforms: iOS -### getLicense +### `getLicense` -Overridable method to acquire a license manually. It recieves as argument the `spc` string. +`licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` (as ASCII string, you will probably need to convert it to base 64) obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`. + You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. -Example: +With this prop you can override the license acquisition flow, as an example: ```js getLicense: (spcString) => { @@ -46,7 +58,7 @@ getLicense: (spcString) => { Platforms: iOS -### headers +### `headers` You can customize headers send to the licenseServer. @@ -65,54 +77,11 @@ source={{ }} ``` -### licenseServer +### `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. -* `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` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. - With this prop you can override the license acquisition flow, as an example: - -```js - getLicense: (spcString) => { - const base64spc = 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 - -### type +### `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. diff --git a/Video.js b/Video.js index 1f0fde4d5b..c1f0aa6eb7 100644 --- a/Video.js +++ b/Video.js @@ -228,10 +228,10 @@ export default class Video extends Component { }; _onGetLicense = (event) => { - if (this.props.source && this.props.source.drm && this.props.source.drm.getLicense instanceof Function) { + if (this.props.drm && this.props.drm.getLicense instanceof Function) { const data = event.nativeEvent; if (data && data.spc) { - const getLicenseOverride = this.props.source.drm.getLicense(data.spc, this.props); + const getLicenseOverride = this.props.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) { @@ -297,7 +297,6 @@ export default class Video extends Component { mainVer: source.mainVer || 0, patchVer: source.patchVer || 0, requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, - drm: source.drm }, onVideoLoadStart: this._onLoadStart, onVideoLoad: this._onLoad, @@ -320,7 +319,7 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, - onGetLicense: nativeProps.source && nativeProps.source.drm && nativeProps.source.drm.getLicense && this._onGetLicense, + onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense, onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, }); From 2fc3b770a501b33c7ff970aaac33ac32906187a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 26 Apr 2019 11:00:59 +0200 Subject: [PATCH 083/101] finishLoading DRM request when changing source --- ios/Video/RCTVideo.m | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index c2e7dd6b9a..0143f4e9da 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1389,6 +1389,9 @@ - (void)removePlayerLayer [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; _playerLayerObserverSet = NO; } + if (_loadingRequest != nil) { + [_loadingRequest finishLoading]; + } _requestingCertificate = NO; _requestingCertificateErrored = NO; _playerLayer = nil; @@ -1675,19 +1678,19 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { _loadingRequest = loadingRequest; NSURL *url = loadingRequest.request.URL; NSString *contentId = url.host; - if (_drm != nil) { - NSString *contentIdOverride = (NSString *)[_drm objectForKey:@"contentId"]; + if (self->_drm != nil) { + NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; if (contentIdOverride != nil) { contentId = contentIdOverride; } - NSString *drmType = (NSString *)[_drm objectForKey:@"type"]; + NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; if ([drmType isEqualToString:@"fairplay"]) { - NSString *certificateStringUrl = (NSString *)[_drm objectForKey:@"certificateUrl"]; + NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; if (certificateStringUrl != nil) { NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; - if ([_drm objectForKey:@"base64Certificate"]) { + if ([self->_drm objectForKey:@"base64Certificate"]) { certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; } From 2d0e402d49ae173dff257394545e18f8654e710f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 26 Apr 2019 11:26:21 +0200 Subject: [PATCH 084/101] fixes + unify indentation --- ios/Video/RCTVideo.m | 935 +++++++++++++++++++++---------------------- 1 file changed, 467 insertions(+), 468 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 0143f4e9da..23cd2e705a 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -35,7 +35,7 @@ @implementation RCTVideo NSURL *_videoURL; BOOL _requestingCertificate; BOOL _requestingCertificateErrored; - + /* DRM */ NSDictionary *_drm; AVAssetResourceLoadingRequest *_loadingRequest; @@ -59,7 +59,7 @@ @implementation RCTVideo float _volume; float _rate; float _maxBitRate; - + BOOL _muted; BOOL _paused; BOOL _repeat; @@ -145,14 +145,14 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher - (RCTVideoPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; - viewController.showsPlaybackControls = YES; - viewController.rctDelegate = self; - viewController.preferredOrientation = _fullscreenOrientation; - - viewController.view.frame = self.bounds; - viewController.player = player; - return viewController; + RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; + viewController.showsPlaybackControls = YES; + viewController.rctDelegate = self; + viewController.preferredOrientation = _fullscreenOrientation; + + viewController.view.frame = self.bounds; + viewController.player = player; + return viewController; } /* --------------------------------------------------------- @@ -244,11 +244,11 @@ - (void)applicationWillEnterForeground:(NSNotification *)notification - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #pragma mark - Progress @@ -350,51 +350,51 @@ - (void)setSrc:(NSDictionary *)source [self removePlayerTimeObserver]; [self removePlayerItemObservers]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ - + // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:_source withCallback:^(AVPlayerItem * playerItem) { - _playerItem = playerItem; - [self addPlayerItemObservers]; - [self setFilter:_filterName]; - [self setMaxBitRate:_maxBitRate]; - - [_player pause]; - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; - - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (_isExternalPlaybackActiveObserverRegistered) { - [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - _isExternalPlaybackActiveObserverRegistered = NO; - } - - _player = [AVPlayer playerWithPlayerItem:_playerItem]; - _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - - [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; - _playbackRateObserverRegistered = YES; - - [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; - _isExternalPlaybackActiveObserverRegistered = YES; - - [self addPlayerTimeObserver]; - - //Perform on next run loop, otherwise onVideoLoadStart is nil - if (self.onVideoLoadStart) { - id uri = [_source objectForKey:@"uri"]; - id type = [_source objectForKey:@"type"]; - self.onVideoLoadStart(@{@"src": @{ - @"uri": uri ? uri : [NSNull null], - @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[_source objectForKey:@"isNetwork"]]}, - @"drm": _drm ? _drm : [NSNull null], - @"target": self.reactTag - }); - } - }]; + [self playerItemForSource:self->_source withCallback:^(AVPlayerItem * playerItem) { + self->_playerItem = playerItem; + [self addPlayerItemObservers]; + [self setFilter:self->_filterName]; + [self setMaxBitRate:self->_maxBitRate]; + + [self->_player pause]; + [self->_playerViewController.view removeFromSuperview]; + self->_playerViewController = nil; + + if (self->_playbackRateObserverRegistered) { + [self->_player removeObserver:self forKeyPath:playbackRate context:nil]; + self->_playbackRateObserverRegistered = NO; + } + if (self->_isExternalPlaybackActiveObserverRegistered) { + [self->_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; + self->_isExternalPlaybackActiveObserverRegistered = NO; + } + + self->_player = [AVPlayer playerWithPlayerItem:self->_playerItem]; + self->_player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + + [self->_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; + self->_playbackRateObserverRegistered = YES; + + [self->_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; + self->_isExternalPlaybackActiveObserverRegistered = YES; + + [self addPlayerTimeObserver]; + + //Perform on next run loop, otherwise onVideoLoadStart is nil + if (self.onVideoLoadStart) { + id uri = [self->_source objectForKey:@"uri"]; + id type = [self->_source objectForKey:@"type"]; + self.onVideoLoadStart(@{@"src": @{ + @"uri": uri ? uri : [NSNull null], + @"type": type ? type : [NSNull null], + @"isNetwork": [NSNumber numberWithBool:(bool)[self->_source objectForKey:@"isNetwork"]]}, + @"drm": self->_drm ? _drm : [NSNull null], + @"target": self.reactTag + }); + } + }]; }); _videoLoadStarted = YES; } @@ -433,7 +433,7 @@ - (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nu // AVPlayer can't airplay AVMutableCompositions _allowsExternalPlayback = NO; - + // sideload text tracks AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; @@ -474,7 +474,7 @@ - (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nu if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; } - + handler([AVPlayerItem playerItemWithAsset:mixComposition]); } @@ -485,13 +485,12 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye bool shouldCache = [RCTConvert BOOL:[source objectForKey:@"shouldCache"]]; NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; - NSDictionary *drm = [source objectForKey:@"drm"]; AVURLAsset *asset; if (!uri || [uri isEqualToString:@""]) { DebugLog(@"Could not find video URL in source '%@'", source); return; } - + NSURL *url = isNetwork || isAsset ? [NSURL URLWithString:uri] : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; @@ -507,7 +506,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye */ NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; - + #if __has_include() if (shouldCache && (!_textTracks || !_textTracks.count)) { /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying @@ -519,7 +518,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye return; } #endif - + asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; } else if (isAsset) { asset = [AVURLAsset URLAssetWithURL:url options:nil]; @@ -530,51 +529,51 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye [asset.resourceLoader setDelegate:self queue:queue]; _requestingCertificate = NO; _requestingCertificateErrored = NO; - + [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; } #if __has_include() - (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary *)options withCallback:(void(^)(AVPlayerItem *))handler { - NSURL *url = [NSURL URLWithString:uri]; - [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { - switch (videoCacheStatus) { - case RCTVideoCacheStatusMissingFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - case RCTVideoCacheStatusUnsupportedFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - default: - if (cachedAsset) { - DebugLog(@"Playing back uri '%@' from cache", uri); - // See note in playerItemForSource about not being able to support text tracks & caching - handler([AVPlayerItem playerItemWithAsset:cachedAsset]); - return; - } + NSURL *url = [NSURL URLWithString:uri]; + [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { + switch (videoCacheStatus) { + case RCTVideoCacheStatusMissingFileExtension: { + DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; + [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; + return; + } + case RCTVideoCacheStatusUnsupportedFileExtension: { + DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; + [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; + return; + } + default: + if (cachedAsset) { + DebugLog(@"Playing back uri '%@' from cache", uri); + // See note in playerItemForSource about not being able to support text tracks & caching + handler([AVPlayerItem playerItemWithAsset:cachedAsset]); + return; } - - DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; - asset.loaderDelegate = self; - - /* More granular code to have control over the DVURLAsset - DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; - resourceLoaderDelegate.delegate = self; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; - components.scheme = [DVAssetLoaderDelegate scheme]; - AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; - [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; - */ - - handler([AVPlayerItem playerItemWithAsset:asset]); - }]; + } + + DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; + asset.loaderDelegate = self; + + /* More granular code to have control over the DVURLAsset + DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; + resourceLoaderDelegate.delegate = self; + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; + components.scheme = [DVAssetLoaderDelegate scheme]; + AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; + [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; + */ + + handler([AVPlayerItem playerItemWithAsset:asset]); + }]; } #pragma mark - DVAssetLoaderDelegate @@ -582,9 +581,9 @@ - (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary - (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate didLoadData:(NSData *)data forURL:(NSURL *)url { - [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { - DebugLog(@"Cache data stored successfully 🎉"); - }]; + [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { + DebugLog(@"Cache data stored successfully 🎉"); + }]; } #endif @@ -727,10 +726,10 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } } else if([keyPath isEqualToString:externalPlaybackActive]) { - if(self.onVideoExternalPlaybackChange) { - self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], - @"target": self.reactTag}); - } + if(self.onVideoExternalPlaybackChange) { + self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], + @"target": self.reactTag}); + } } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; @@ -755,7 +754,7 @@ - (void)attachListeners selector:@selector(playbackStalled:) name:AVPlayerItemPlaybackStalledNotification object:nil]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemNewAccessLogEntryNotification object:nil]; @@ -770,28 +769,28 @@ - (void)attachListeners selector:@selector(didFailToFinishPlaying:) name: AVPlayerItemFailedToPlayToEndTimeNotification object:nil]; - + } - (void)handleAVPlayerAccess:(NSNotification *)notification { - AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; - AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; - - /* TODO: get this working - if (self.onBandwidthUpdate) { - self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); - } - */ + AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; + AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; + + /* TODO: get this working + if (self.onBandwidthUpdate) { + self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); + } + */ } - (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}); + 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 @@ -839,8 +838,8 @@ - (void)setPlayInBackground:(BOOL)playInBackground - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -854,7 +853,7 @@ - (void)setPictureInPicture:(BOOL)pictureInPicture if (_pictureInPicture == pictureInPicture) { return; } - + _pictureInPicture = pictureInPicture; if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -863,7 +862,7 @@ - (void)setPictureInPicture:(BOOL)pictureInPicture } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController stopPictureInPicture]; - }); + }); } #endif } @@ -1018,52 +1017,52 @@ - (void)setRepeat:(BOOL)repeat { - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; + + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; } + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -1198,25 +1197,25 @@ - (void)setTextTracks:(NSArray*) textTracks; - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; } - return audioTracks; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -1384,16 +1383,16 @@ - (void)setProgressUpdateInterval:(float)progressUpdateInterval - (void)removePlayerLayer { - [_playerLayer removeFromSuperlayer]; - if (_playerLayerObserverSet) { - [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; - _playerLayerObserverSet = NO; - } if (_loadingRequest != nil) { [_loadingRequest finishLoading]; } _requestingCertificate = NO; _requestingCertificateErrored = NO; + [_playerLayer removeFromSuperlayer]; + if (_playerLayerObserverSet) { + [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; + _playerLayerObserverSet = NO; + } _playerLayer = nil; } @@ -1422,29 +1421,29 @@ - (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerView } - (void)setFilter:(NSString *)filterName { - _filterName = filterName; - - if (!_filterEnabled) { - return; - } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { - return; // filters don't work for HLS... return - } else if (!_playerItem.asset) { - return; - } - - CIFilter *filter = [CIFilter filterWithName:filterName]; - _playerItem.videoComposition = [AVVideoComposition - videoCompositionWithAsset:_playerItem.asset - applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { - if (filter == nil) { - [request finishWithImage:request.sourceImage context:nil]; - } else { - CIImage *image = request.sourceImage.imageByClampingToExtent; - [filter setValue:image forKey:kCIInputImageKey]; - CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; - [request finishWithImage:output context:nil]; - } - }]; + _filterName = filterName; + + if (!_filterEnabled) { + return; + } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { + return; // filters don't work for HLS... return + } else if (!_playerItem.asset) { + return; + } + + CIFilter *filter = [CIFilter filterWithName:filterName]; + _playerItem.videoComposition = [AVVideoComposition + videoCompositionWithAsset:_playerItem.asset + applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { + if (filter == nil) { + [request finishWithImage:request.sourceImage context:nil]; + } else { + CIImage *image = request.sourceImage.imageByClampingToExtent; + [filter setValue:image forKey:kCIInputImageKey]; + CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; + [request finishWithImage:output context:nil]; + } + }]; } - (void)setFilterEnabled:(BOOL)filterEnabled { @@ -1541,318 +1540,318 @@ - (void)removeFromSuperview #pragma mark - Export - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - - AVAsset *asset = _playerItem.asset; - - if (asset != nil) { - - AVAssetExportSession *exportSession = [AVAssetExportSession - exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; - - if (exportSession != nil) { - NSString *path = nil; - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] - withExtension:@".mp4"]; - NSURL *url = [NSURL fileURLWithPath:path]; - exportSession.outputFileType = AVFileTypeMPEG4; - exportSession.outputURL = url; - exportSession.videoComposition = _playerItem.videoComposition; - exportSession.shouldOptimizeForNetworkUse = true; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - - switch ([exportSession status]) { - case AVAssetExportSessionStatusFailed: - reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); - break; - case AVAssetExportSessionStatusCancelled: - reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); - break; - default: - resolve(@{@"uri": url.absoluteString}); - break; - } - - }]; - - } else { - - reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); - + + AVAsset *asset = _playerItem.asset; + + if (asset != nil) { + + AVAssetExportSession *exportSession = [AVAssetExportSession + exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; + + if (exportSession != nil) { + NSString *path = nil; + NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] + withExtension:@".mp4"]; + NSURL *url = [NSURL fileURLWithPath:path]; + exportSession.outputFileType = AVFileTypeMPEG4; + exportSession.outputURL = url; + exportSession.videoComposition = _playerItem.videoComposition; + exportSession.shouldOptimizeForNetworkUse = true; + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + + switch ([exportSession status]) { + case AVAssetExportSessionStatusFailed: + reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); + break; + case AVAssetExportSessionStatusCancelled: + reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); + break; + default: + resolve(@{@"uri": url.absoluteString}); + break; } - + + }]; + } else { - - reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); - + + reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); + } + + } else { + + reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); + + } } - (void)setLicenseResult:(NSString *)license { - 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"]; - } + 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"]; + } } - (BOOL)setLicenseResultError:(NSString *)error { - if (_loadingRequest != nil) { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorFromJSPart - userInfo: @{ - NSLocalizedDescriptionKey: error, - NSLocalizedFailureReasonErrorKey: error, - NSLocalizedRecoverySuggestionErrorKey: error - } - ]; - [self finishLoadingWithError:licenseError]; - } - return NO; + if (_loadingRequest != nil) { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorFromJSPart + userInfo: @{ + NSLocalizedDescriptionKey: error, + NSLocalizedFailureReasonErrorKey: error, + NSLocalizedRecoverySuggestionErrorKey: error + } + ]; + [self finishLoadingWithError:licenseError]; + } + return NO; } - (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 == nil ? @"RCTVideo" : _playerItem.error.domain}, - @"target": self.reactTag}); - } + 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 == nil ? @"RCTVideo" : _playerItem.error.domain}, + @"target": self.reactTag}); + } } return NO; } - (BOOL)ensureDirExistsWithPath:(NSString *)path { - BOOL isDir = NO; - NSError *error; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - if (!(exists && isDir)) { - [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - return NO; - } + BOOL isDir = NO; + NSError *error; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + if (!(exists && isDir)) { + [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + return NO; } - return YES; + } + return YES; } - (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension { - NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; - [self ensureDirExistsWithPath:directory]; - return [directory stringByAppendingPathComponent:fileName]; + NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; + [self ensureDirExistsWithPath:directory]; + return [directory stringByAppendingPathComponent:fileName]; } - (NSString *)cacheDirectoryPath { - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return array[0]; + NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + return array[0]; } #pragma mark - AVAssetResourceLoaderDelegate - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { - return [self loadingRequestHandling:renewalRequest]; + return [self loadingRequestHandling:renewalRequest]; } - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { - return [self loadingRequestHandling:loadingRequest]; + return [self loadingRequestHandling:loadingRequest]; } - - - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader - didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { - NSLog(@"didCancelLoadingRequest"); + +- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader +didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { + NSLog(@"didCancelLoadingRequest"); } - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { - if (_requestingCertificate) { - return YES; - } else if (_requestingCertificateErrored) { - return NO; + if (_requestingCertificate) { + return YES; + } else if (_requestingCertificateErrored) { + return NO; + } + _loadingRequest = loadingRequest; + NSURL *url = loadingRequest.request.URL; + NSString *contentId = url.host; + if (self->_drm != nil) { + NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; + if (contentIdOverride != nil) { + contentId = contentIdOverride; } - _loadingRequest = loadingRequest; - NSURL *url = loadingRequest.request.URL; - NSString *contentId = url.host; - if (self->_drm != nil) { - NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; - if (contentIdOverride != nil) { - contentId = contentIdOverride; - } - NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; - if ([drmType isEqualToString:@"fairplay"]) { - NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; - if (certificateStringUrl != nil) { - NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; - if ([self->_drm objectForKey:@"base64Certificate"]) { - certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; + if ([drmType isEqualToString:@"fairplay"]) { + NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; + if (certificateStringUrl != nil) { + NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; + if ([self->_drm objectForKey:@"base64Certificate"]) { + certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; + } + + 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 *)[self->_drm objectForKey:@"licenseServer"]; + if (spcError != nil) { + [self finishLoadingWithError:spcError]; + self->_requestingCertificateErrored = YES; + } + if (spcData != nil) { + if(self.onGetLicense) { + NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; + self.onGetLicense(@{@"spc": spcStr, + @"target": self.reactTag}); + } else if(licenseServer != nil) { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:licenseServer]]; + // HEADERS + NSDictionary *headers = (NSDictionary *)[self->_drm objectForKey:@"headers"]; + if (headers != nil) { + for (NSString *key in headers) { + NSString *value = headers[key]; + [request setValue:value forHTTPHeaderField:key]; } - - 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 *)[self->_drm objectForKey:@"licenseServer"]; - if (spcError != nil) { - [self finishLoadingWithError:spcError]; - self->_requestingCertificateErrored = YES; - } - if (spcData != nil) { - if(self.onGetLicense) { - NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; - self.onGetLicense(@{@"spc": spcStr, - @"target": self.reactTag}); - } else if(licenseServer != nil) { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"POST"]; - [request setURL:[NSURL URLWithString:licenseServer]]; - // HEADERS - NSDictionary *headers = (NSDictionary *)[self->_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]; - 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 license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - [self finishLoadingWithError:error]; - self->_requestingCertificateErrored = YES; - } else { - if([httpResponse statusCode] != 200){ - NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorLicenseRequestNotOk - 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?" - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } else if (data != nil) { - [dataRequest respondWithData:data]; - [loadingRequest finishLoading]; - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataFromLicenseRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } - }]; - [postDataTask resume]; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoSPC - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: @"No spc received.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } + } + // + + [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 license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + [self finishLoadingWithError:error]; + self->_requestingCertificateErrored = YES; } else { + if([httpResponse statusCode] != 200){ + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateData + code: RCTVideoErrorLicenseRequestNotOk + 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?" + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } else if (data != nil) { + [dataRequest respondWithData:data]; + [loadingRequest finishLoading]; + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDataFromLicenseRequest userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" + NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." } ]; [self finishLoadingWithError:licenseError]; self->_requestingCertificateErrored = YES; + } + } - }); - return YES; - } else { + }]; + [postDataTask resume]; + } + + } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateURL + code: RCTVideoErrorNoSPC userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM License.", - NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", - NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: @"No spc received.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." } ]; - return [self finishLoadingWithError:licenseError]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDataRequest + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; } - } else { + } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoFairplayDRM + code: RCTVideoErrorNoCertificateData userInfo: @{ NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" + NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" } ]; - return [self finishLoadingWithError:licenseError]; - } - - } else { + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + }); + return YES; + } else { NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDRMData + code: RCTVideoErrorNoCertificateURL userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No drm object found.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" + NSLocalizedDescriptionKey: @"Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" } ]; return [self finishLoadingWithError:licenseError]; + } + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoFairplayDRM + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" + } + ]; + return [self finishLoadingWithError:licenseError]; } - - return NO; + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDRMData + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No drm object found.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" + } + ]; + return [self finishLoadingWithError:licenseError]; + } + + + return NO; } - (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; + if (base64String != nil) { + // NSData from the Base64 encoded str + NSData *base64Data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSASCIIStringEncoding]; + return base64Data; + } + return nil; } #pragma mark - Picture in Picture @@ -1860,29 +1859,29 @@ - (NSData *)base64DataFromBase64String: (NSString *)base64String { - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { if (self.onPictureInPictureStatusChanged) { self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:false] - }); + @"isActive": [NSNumber numberWithBool:false] + }); } } - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { if (self.onPictureInPictureStatusChanged) { self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:true] - }); + @"isActive": [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 { From 9fa404687d92df2f89c721e933ac8fb7f83b531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Fri, 26 Apr 2019 12:12:56 +0200 Subject: [PATCH 085/101] update readme with more scenarios --- DRM.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/DRM.md b/DRM.md index 7d63d7a4b3..ef3ad79cea 100644 --- a/DRM.md +++ b/DRM.md @@ -7,15 +7,15 @@ This feature will disable the use of `TextureView` on Android. DRM object allows this members: -| Property | Type | Platform | Description | -| --- | --- | --- | --- | -| [`type`](#type) | DRMType | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) | -| [`licenseServer`](#licenseserver) | string | iOS/Android | Specifies the license server URL | -| [`headers`](#headers) | Object | iOS/Android | Specifies the headers send to the license server URL on license acquisition | -| [`contentId`](#contentid) | string | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) | -| [`certificateUrl`](#certificateurl) | string | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file | -| [`base64Certificate`](#base64certificate) | bool | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 | -| [`getLicense`](#getlicense)| function | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream | +| Property | Type | Default | Platform | Description | +| --- | --- | --- | --- | --- | +| [`type`](#type) | DRMType | undefined | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) | +| [`licenseServer`](#licenseserver) | string | undefined | iOS/Android | Specifies the license server URL | +| [`headers`](#headers) | Object | undefined | iOS/Android | Specifies the headers send to the license server URL on license acquisition | +| [`contentId`](#contentid) | string | undefined | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) | +| [`certificateUrl`](#certificateurl) | string | undefined | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file | +| [`base64Certificate`](#base64certificate) | bool | false | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 | +| [`getLicense`](#getlicense)| function | undefined | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream | ### `base64Certificate` @@ -44,7 +44,7 @@ getLicense: (spcString) => { return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { method: 'POST', headers: { - 'pallycon-customdata-v2': 'eyJkcm1fdHlwZSI6IkZhaXJQbGF5Iiwic2l0ZV9pZCI6IkJMMkciLCJ1c2VyX2lkIjoiMTIxMjc5IiwiY2lkIjoiYXNzZXRfMTQ5OTAiLCJ0b2tlbiI6IjBkQVVLSEQ4bm5pTStJeDJ2Y09HVStzSWRWY2wvSEdxSjdEanNZK1laazZKdlhLczRPM3BVNitVVnV3dkNvLzRyc2lIUi9PSnY4RDJncHBBN0cycnRGdy9pVFMvTWNZaVhML2VLOXdMMXFVM05VbXlFL25RdVV3Tm5mOXI2YlArUjUvRDZxOU5vZmZtTGUybmo4VGphQ3UwUUFQZlVqVzRFREE4eDNUYlI5cXZOa0pKVHdmNTA5NE5UYXY5VzJxbFp0MmczcDNMcUV0RkNMK0N5dFBZSWJEN2ZBUmR1ZzkvVTdiMXB1Y3pndTBqRjg3QnlMU0tac0J3TUpYd2xSZkxTTTZJSzRlWHMvNC9RWU4rVXhnR3ozVTgxODl4aHhWS0RJaDdBcGFkQVllVUZUMWJIVVZBSVVRQms0cjRIQ28yczIydWJvVnVLaVNQazdvYmtJckVNQT09IiwidGltZXN0YW1wIjoiMjAxOS0wMi0xMlQwNjoxODo0MloiLCJoYXNoIjoiMThqcDBDVTdOaUJ3WFdYVC8zR2lFN3R0YXVRWlZ5SjVSMUhSK2J2Um9JWT0ifQ==', + 'pallycon-customdata-v2': 'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==', 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData @@ -85,4 +85,55 @@ The URL pointing to the licenseServer that will provide the authorization to pla 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 \ No newline at end of file +for iOS: DRMType.FAIRPLAY + +## Common Usage Scenarios + +### Send cookies to license server + +You can send Cookies to the license server via `headers` prop. Example: + +```js +drm: { + type: 'widevine', //or DRMType.WIDEVINE + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar' + }, +} +``` + +### Custom License Acquisition (only iOS for now) + +```js +drm: { + type: DRMType.FAIRPLAY, + getLicense: (spcString) => { + const base64spc = Base64.encode(spcString); + return fetch('YOUR LICENSE SERVER HERE', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + getFairplayLicense: { + foo: 'bar', + spcMessage: base64spc, + } + }) + }) + .then(response => response.json()) + .then((response) => { + if (response && response.getFairplayLicenseResponse + && response.getFairplayLicenseResponse.ckcResponse) { + return response.getFairplayLicenseResponse.ckcResponse; + } + throw new Error('No correct response'); + }) + .catch((error) => { + console.error('CKC error', error); + }); + } +} +``` From 88c5541a7a1cc8277f421588391289768688d08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 4 Jun 2019 17:39:03 +0200 Subject: [PATCH 086/101] change stream fixes --- ios/Video/RCTVideo.m | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 23cd2e705a..78d7f33697 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -390,7 +390,7 @@ - (void)setSrc:(NSDictionary *)source @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], @"isNetwork": [NSNumber numberWithBool:(bool)[self->_source objectForKey:@"isNetwork"]]}, - @"drm": self->_drm ? _drm : [NSNull null], + @"drm": self->_drm ? self->_drm : [NSNull null], @"target": self.reactTag }); } @@ -525,10 +525,17 @@ - (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("assetQueue", nil); - [asset.resourceLoader setDelegate:self queue:queue]; + // Reset + if (_loadingRequest != nil) { + [_loadingRequest finishLoading]; + } _requestingCertificate = NO; _requestingCertificateErrored = NO; + ///////// + if (self->_drm != nil) { + dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); + [asset.resourceLoader setDelegate:self queue:queue]; + } [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; } @@ -1669,9 +1676,9 @@ - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader } - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { - if (_requestingCertificate) { + if (self->_requestingCertificate) { return YES; - } else if (_requestingCertificateErrored) { + } else if (self->_requestingCertificateErrored) { return NO; } _loadingRequest = loadingRequest; @@ -1708,6 +1715,7 @@ - (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { if (spcData != nil) { if(self.onGetLicense) { NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; + self->_requestingCertificate = YES; self.onGetLicense(@{@"spc": spcStr, @"target": self.reactTag}); } else if(licenseServer != nil) { From 91f200bd59f3fff2653d2054484130a70171bb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Tue, 4 Jun 2019 18:56:45 +0200 Subject: [PATCH 087/101] initializePlayback postDelayed --- .../exoplayer/ReactExoplayerView.java | 125 +++++++++--------- 1 file changed, 66 insertions(+), 59 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 c4a0e925e5..1052fb8c18 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -347,73 +347,80 @@ private void reLayout(View view) { } private void initializePlayer() { - if (player == null) { - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelector.setParameters(trackSelector.buildUponParameters() + ReactExoplayerView self = this; + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (player == null) { + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); - DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - DefaultLoadControl defaultLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, -1, true); - - DrmSessionManager drmSessionManager = null; - if (this.drmUUID != null) { - try { - drmSessionManager = buildDrmSessionManager(this.drmUUID, this.drmLicenseUrl, - this.drmLicenseHeader); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - Log.d("Drm Info", getResources().getString(errorStringId)); - eventEmitter.error(getResources().getString(errorStringId), e); - return; - } - } + DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + DefaultLoadControl defaultLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, -1, true); + + DrmSessionManager drmSessionManager = null; + if (self.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + self.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + Log.d("Drm Info", getResources().getString(errorStringId)); + eventEmitter.error(getResources().getString(errorStringId), e); + return; + } + } - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext(), - drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext(), + drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - player = ExoPlayerFactory.newSimpleInstance(getContext(), renderersFactory, trackSelector, defaultLoadControl); - player.addListener(this); - player.setMetadataOutput(this); - exoPlayerView.setPlayer(player); - audioBecomingNoisyReceiver.setListener(this); - BANDWIDTH_METER.addEventListener(new Handler(), this); - setPlayWhenReady(!isPaused); - playerNeedsSource = true; + player = ExoPlayerFactory.newSimpleInstance(getContext(), renderersFactory, trackSelector, defaultLoadControl); + player.addListener(self); + player.setMetadataOutput(self); + exoPlayerView.setPlayer(player); + audioBecomingNoisyReceiver.setListener(self); + BANDWIDTH_METER.addEventListener(new Handler(), self); + setPlayWhenReady(!isPaused); + playerNeedsSource = true; - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); - } - if (playerNeedsSource && srcUri != null) { - ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension); - MediaSource mediaSource; - if (mediaSourceList.size() == 0) { - mediaSource = videoSource; - } else { - mediaSourceList.add(0, videoSource); - MediaSource[] textSourceArray = mediaSourceList.toArray( - new MediaSource[mediaSourceList.size()] - ); - mediaSource = new MergingMediaSource(textSourceArray); - } + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } + if (playerNeedsSource && srcUri != null) { + ArrayList mediaSourceList = buildTextSources(); + MediaSource videoSource = buildMediaSource(srcUri, extension); + MediaSource mediaSource; + if (mediaSourceList.size() == 0) { + mediaSource = videoSource; + } else { + mediaSourceList.add(0, videoSource); + MediaSource[] textSourceArray = mediaSourceList.toArray( + new MediaSource[mediaSourceList.size()] + ); + mediaSource = new MergingMediaSource(textSourceArray); + } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - playerNeedsSource = false; + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); + playerNeedsSource = false; - eventEmitter.loadStart(); - loadVideoStarted = true; - } + eventEmitter.loadStart(); + loadVideoStarted = true; + } - // Initializing the playerControlView - initializePlayerControl(); - handleControls(); + // Initializing the playerControlView + initializePlayerControl(); + handleControls(); + } + }, 1); } From fb3175f6feda01a1ebd45541a648ab790b9465e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Sat, 15 Jun 2019 22:10:41 +0200 Subject: [PATCH 088/101] remove logs --- .../com/brentvatne/exoplayer/ReactExoplayerViewManager.java | 4 ---- 1 file changed, 4 deletions(-) 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 d096b34799..77cb60bdd9 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -106,13 +106,11 @@ public void onDropViewInstance(ReactExoplayerView view) { @ReactProp(name = PROP_DRM) public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) { - Log.d("SetDRM", "Set DRM config"); if (drm != null && drm.hasKey(PROP_DRM_TYPE)) { String drmType = drm.hasKey(PROP_DRM_TYPE) ? drm.getString(PROP_DRM_TYPE) : null; String drmLicenseServer = drm.hasKey(PROP_DRM_LICENSESERVER) ? drm.getString(PROP_DRM_LICENSESERVER) : null; ReadableMap drmHeaders = drm.hasKey(PROP_DRM_HEADERS) ? drm.getMap(PROP_DRM_HEADERS) : null; if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) { - Log.d("setDrmType", drmType); UUID drmUUID = Util.getDrmUuid(drmType); videoView.setDrmType(drmUUID); videoView.setDrmLicenseUrl(drmLicenseServer); @@ -126,7 +124,6 @@ public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm } videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); } - Log.d("setDrm", "Disabling TextureView (needed for DRM)"); videoView.setUseTextureView(false); } } @@ -134,7 +131,6 @@ public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm @ReactProp(name = PROP_SRC) public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { - Log.d("SetSrc", "Set Source"); Context context = videoView.getContext().getApplicationContext(); String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; From 4c4899b924fe5f88f4db924ecf0dd1c3456dcd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Mon, 1 Jul 2019 21:45:19 +0200 Subject: [PATCH 089/101] Update DRM.md fix typo certificatefor --- DRM.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DRM.md b/DRM.md index ef3ad79cea..a7cef6397d 100644 --- a/DRM.md +++ b/DRM.md @@ -25,7 +25,7 @@ Platforms: iOS ### `certificateUrl` -URL to fetch a valid certificatefor FairPlay. +URL to fetch a valid certificate for FairPlay. Platforms: iOS From a48fb69ba9ab276910a75a86e6097f8736d32fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Mon, 1 Jul 2019 21:47:22 +0200 Subject: [PATCH 090/101] Update DRM.md Use value from enum on the doc --- DRM.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DRM.md b/DRM.md index a7cef6397d..04dff81c38 100644 --- a/DRM.md +++ b/DRM.md @@ -95,7 +95,7 @@ You can send Cookies to the license server via `headers` prop. Example: ```js drm: { - type: 'widevine', //or DRMType.WIDEVINE + type: DRMType.WIDEVINE licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', headers: { 'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar' From c123d86659d45f636cc6d0b5c6efdbca584587c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Mon, 1 Jul 2019 21:52:28 +0200 Subject: [PATCH 091/101] Update README.md Illustrate example for explicit mimetype --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 99cf0cc56f..4d28a24180 100644 --- a/README.md +++ b/README.md @@ -740,6 +740,7 @@ Platforms: iOS ##### Explicit mimetype for the stream Provide a member `type` with value (`mpd`/`m3u8`/`ism`) inside the source object. +Sometimes is needed when URL extension does not match with the mimetype that you are expecting, as seen on the next example. (Extension is .ism -smooth streaming- but file served is on format mpd -mpeg dash-) Example: ``` From e6710f3c1784ee664eb6b379180afd569db78c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Mon, 1 Jul 2019 21:54:36 +0200 Subject: [PATCH 092/101] Update ReactExoplayerView.java remove logs --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 3 --- 1 file changed, 3 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 1052fb8c18..3548606761 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -370,7 +370,6 @@ public void run() { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - Log.d("Drm Info", getResources().getString(errorStringId)); eventEmitter.error(getResources().getString(errorStringId), e); return; } @@ -1228,12 +1227,10 @@ public void setDrmType(UUID drmType) { } public void setDrmLicenseUrl(String licenseUrl){ - Log.d("setDrmLicenseUrl", licenseUrl); this.drmLicenseUrl = licenseUrl; } public void setDrmLicenseHeader(String[] header){ - Log.d("setDrmLicenseHeader", header.toString()); this.drmLicenseHeader = header; } From e9c978c56882acb5a1119e8405050a787fb487ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Tue, 2 Jul 2019 14:33:08 +0200 Subject: [PATCH 093/101] Update DRM.md refresh docs where drm was still nested --- DRM.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DRM.md b/DRM.md index 04dff81c38..2ba5861948 100644 --- a/DRM.md +++ b/DRM.md @@ -67,13 +67,13 @@ Example: ```js source={{ uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', - drm: { - type: 'widevine', //or DRMType.WIDEVINE - licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', - headers: { - 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' - }, - } +}} +drm={{ + type: 'widevine', //or DRMType.WIDEVINE + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' + }, }} ``` From 7525d4a5148aecf8e483b56ecf283a8a83e7f5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o=20Ruiz?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Tue, 2 Jul 2019 15:20:31 +0200 Subject: [PATCH 094/101] Update Video.js unnest drm prop on PropTypes --- Video.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Video.js b/Video.js index c1f0aa6eb7..8b3c159a4e 100644 --- a/Video.js +++ b/Video.js @@ -390,20 +390,20 @@ Video.propTypes = { source: PropTypes.oneOfType([ PropTypes.shape({ uri: PropTypes.string, - drm: PropTypes.shape({ - type: PropTypes.oneOf([ - DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY - ]), - licenseServer: PropTypes.string, - headers: PropTypes.shape({}), - base64Certificate: PropTypes.bool, - certificateUrl: PropTypes.string, - getLicense: PropTypes.func, - }) }), // Opaque type returned by require('./video.mp4') PropTypes.number ]), + drm: PropTypes.shape({ + type: PropTypes.oneOf([ + DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY + ]), + licenseServer: PropTypes.string, + headers: PropTypes.shape({}), + base64Certificate: PropTypes.bool, + certificateUrl: PropTypes.string, + getLicense: PropTypes.func, + }), minLoadRetryCount: PropTypes.number, maxBitRate: PropTypes.number, resizeMode: PropTypes.string, From 395b9b7532353bae6edf4533847c661eec8fbcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marin=CC=83o?= Date: Sat, 28 Sep 2019 10:03:00 +0200 Subject: [PATCH 095/101] Merge master --- .github/ISSUE_TEMPLATE/bug_report.md | 66 + .github/ISSUE_TEMPLATE/feature_request.md | 32 + CHANGELOG.md | 25 + README.md | 120 +- Video.js | 1 + android-exoplayer/build.gradle | 20 +- .../brentvatne/exoplayer/DataSourceUtil.java | 5 - .../DefaultReactExoplayerConfig.java | 26 + .../brentvatne/exoplayer/ExoPlayerView.java | 2 +- .../RawResourceDataSourceFactory.java | 2 +- .../exoplayer/ReactExoplayerConfig.java | 13 + .../exoplayer/ReactExoplayerView.java | 212 +- .../exoplayer/ReactExoplayerViewManager.java | 8 +- .../com/brentvatne/exoplayer/ResizeMode.java | 2 +- .../exoplayer/VideoEventEmitter.java | 2 +- .../brentvatne/react/ReactVideoPackage.java | 24 +- android/build.gradle | 6 +- examples/basic/.babelrc | 3 - examples/basic/android/app/build.gradle | 25 +- .../android/app/src/main/AndroidManifest.xml | 6 +- .../java/com/videoplayer/MainApplication.java | 2 +- examples/basic/android/build.gradle | 19 +- examples/basic/android/gradle.properties | 3 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/basic/android/settings.gradle | 12 +- examples/basic/babel.config.js | 3 + examples/basic/metro.config.js | 10 + examples/basic/package.json | 21 +- examples/basic/rn-cli.config.js | 7 - examples/basic/yarn.lock | 8390 ++++++++++------- ios/Video/RCTVideo.m | 71 +- ios/Video/RCTVideoManager.m | 1 + package.json | 7 +- 33 files changed, 5761 insertions(+), 3387 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 android-exoplayer/src/main/java/com/brentvatne/exoplayer/DefaultReactExoplayerConfig.java create mode 100644 android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerConfig.java delete mode 100644 examples/basic/.babelrc create mode 100644 examples/basic/babel.config.js create mode 100644 examples/basic/metro.config.js delete mode 100644 examples/basic/rn-cli.config.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..ac2d1e3e29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,66 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +# Bug + + + +## Platform + +Which player are you experiencing the problem on: +* iOS +* Android ExoPlayer +* Android MediaPlayer +* Windows UWP +* Windows WPF + +## Environment info + + + +React native info output: + +```bash + // paste it here +``` + +Library version: x.x.x + +## Steps To Reproduce + + + +1. +2. +... + +## Expected behaviour + +1. +2. + +## Reproducible sample code + + + +## Video sample +If possible, include a link to the video that has the problem that can be streamed or downloaded from. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..cc62df4e1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +# Feature Request + + + +## Why it is needed + + + +## Possible implementation + + + +### Code sample + + diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ca55ba5d..fb90809ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ ### next * Basic support for DRM on iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445) +### Version 5.1.0-alpha1 +* Fixed Exoplayer doesn't work with mute=true (Android). [#1696](https://github.com/react-native-community/react-native-video/pull/1696) +* Added support for automaticallyWaitsToMinimizeStalling property (iOS) [#1723](https://github.com/react-native-community/react-native-video/pull/1723) +* Bump Exoplayer to 2.10.4, remove deprecated usages of Exoplayer methods (Android). [#1753](https://github.com/react-native-community/react-native-video/pull/1753) +* Preserve Exoplayer BandwidthMeter instance across video plays, this should noticeably improve streaming bandwidth detection (Android). + +### Version 5.0.2 +* Fix crash when RCTVideo's superclass doesn't observe the keyPath 'frame' (iOS) [#1720](https://github.com/react-native-community/react-native-video/pull/1720) + +### Version 5.0.1 +* Fix AndroidX Support bad merge + +### Version 5.0.0 [Deprecated] +* AndroidX Support + +### Version 4.4.4 +* Handle racing conditions when props are setted on exoplayer + +### Version 4.4.3 +* Fix mute/unmute when controls are present (iOS) [#1654](https://github.com/react-native-community/react-native-video/pull/1654) +* Fix Android videos being able to play with background music/audio from other apps. +* Fixed memory leak on iOS when using `controls` [#1647](https://github.com/react-native-community/react-native-video/pull/1647) +* (Android) Update gradle and target SDK [#1629](https://github.com/react-native-community/react-native-video/pull/1629) +* Fix iOS stressed mount/unmount crash [#1646](https://github.com/react-native-community/react-native-video/pull/1646) + ### Version 4.4.2 * Change compileOnly to implementation on gradle (for newer gradle versions and react-native 0.59 support) [#1592](https://github.com/react-native-community/react-native-video/pull/1592) * Replaced RCTBubblingEventBlock events by RCTDirectEventBlock to avoid event name collisions [#1625](https://github.com/react-native-community/react-native-video/pull/1625) diff --git a/README.md b/README.md index f2eb2afa9a..91a293be7c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,16 @@ A `