From 6e6f91517c492cdc7d2140ae8564712e9a98450a Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 12 Mar 2025 14:13:46 +0100 Subject: [PATCH] feat: enhance react-native-video plugins [Plugins API Breaking] (#4366) * feat: allow plugins for providing custom DRM manager This will allow plugins to provide DRM manger to create custom implementations, eg. using SKD from DRM providers * chore(example): fix drm example on android * chore: lint code * fix: remove platform player logic & dependency from `RNVPlugin` * chore: change warning to debug msg * chore: lint code * chore(example/bare): update Podfile.lock * refactor: reorganize ReactNativeVideoManager plugin registration methods * refactor: add helpers & clean code * docs: update documentation * lint code * add comment * docs: update plugins section --- .../com/brentvatne/exoplayer/DRMManager.kt | 59 +++++ .../brentvatne/exoplayer/DRMManagerSpec.kt | 18 ++ .../exoplayer/RNVExoplayerPlugin.kt | 47 ++++ .../exoplayer/ReactExoplayerView.java | 104 ++++----- .../java/com/brentvatne/react/RNVPlugin.kt | 3 +- .../react/ReactNativeVideoManager.kt | 44 +++- docs/pages/other/plugin.md | 219 ++++++++++++++++-- examples/bare/ios/Podfile.lock | 54 ++--- examples/common/DRMExample.tsx | 3 + .../VideoPluginSampleModule.kt | 18 +- .../ios/VideoPluginSample.swift | 29 ++- ios/Video/DataStructures/DRMParams.swift | 2 +- ios/Video/Features/DRMManager.swift | 2 +- ios/Video/Features/DRMManagerSpec.swift | 17 ++ ios/Video/RCTVideo.swift | 4 +- ios/Video/RNVAVPlayerPlugin.swift | 52 +++++ ios/Video/RNVPlugin.swift | 13 +- ios/Video/ReactNativeVideoManager.swift | 63 ++++- 18 files changed, 600 insertions(+), 151 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/DRMManagerSpec.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/RNVExoplayerPlugin.kt create mode 100644 ios/Video/Features/DRMManagerSpec.swift create mode 100644 ios/Video/RNVAVPlayerPlugin.swift diff --git a/android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt b/android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt new file mode 100644 index 00000000..462f47a2 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt @@ -0,0 +1,59 @@ +package com.brentvatne.exoplayer + +import androidx.media3.common.util.Util +import androidx.media3.datasource.HttpDataSource +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager +import androidx.media3.exoplayer.drm.DrmSessionManager +import androidx.media3.exoplayer.drm.FrameworkMediaDrm +import androidx.media3.exoplayer.drm.HttpMediaDrmCallback +import androidx.media3.exoplayer.drm.UnsupportedDrmException +import com.brentvatne.common.api.DRMProps +import java.util.UUID + +class DRMManager(private val dataSourceFactory: HttpDataSource.Factory) : DRMManagerSpec { + private var hasDrmFailed = false + + @Throws(UnsupportedDrmException::class) + override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? = buildDrmSessionManager(uuid, drmProps, 0) + + @Throws(UnsupportedDrmException::class) + private fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps, retryCount: Int = 0): DrmSessionManager? { + if (Util.SDK_INT < 18) { + return null + } + + try { + val drmCallback = HttpMediaDrmCallback(drmProps.drmLicenseServer, dataSourceFactory) + + // Set DRM headers + val keyRequestPropertiesArray = drmProps.drmLicenseHeader + for (i in keyRequestPropertiesArray.indices step 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]) + } + + val mediaDrm = FrameworkMediaDrm.newInstance(uuid) + + // TODO: This isn't very secure, should be fixed + if (hasDrmFailed) { + // When DRM fails using L1 we want to switch to L3 + mediaDrm.setPropertyString("securityLevel", "L3") + } + + return DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(uuid) { mediaDrm } + .setKeyRequestParameters(null) + .setMultiSession(drmProps.multiDrm) + .build(drmCallback) + } catch (ex: UnsupportedDrmException) { + hasDrmFailed = true + throw ex + } catch (ex: Exception) { + if (retryCount < 3) { + // Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason + hasDrmFailed = true + return buildDrmSessionManager(uuid, drmProps, retryCount + 1) + } + throw UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, ex) + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/DRMManagerSpec.kt b/android/src/main/java/com/brentvatne/exoplayer/DRMManagerSpec.kt new file mode 100644 index 00000000..7adc1eda --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/DRMManagerSpec.kt @@ -0,0 +1,18 @@ +package com.brentvatne.exoplayer + +import androidx.media3.exoplayer.drm.DrmSessionManager +import androidx.media3.exoplayer.drm.UnsupportedDrmException +import com.brentvatne.common.api.DRMProps +import java.util.UUID + +interface DRMManagerSpec { + /** + * Build a DRM session manager for the given UUID and DRM properties + * @param uuid The DRM system UUID + * @param drmProps The DRM properties from the source + * @return DrmSessionManager instance or null if not supported + * @throws UnsupportedDrmException if the DRM scheme is not supported + */ + @Throws(UnsupportedDrmException::class) + fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/RNVExoplayerPlugin.kt b/android/src/main/java/com/brentvatne/exoplayer/RNVExoplayerPlugin.kt new file mode 100644 index 00000000..49315beb --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/RNVExoplayerPlugin.kt @@ -0,0 +1,47 @@ +package com.brentvatne.exoplayer + +import androidx.media3.exoplayer.ExoPlayer +import com.brentvatne.react.RNVPlugin + +/** + * Interface for RNV plugins that have dependencies or logic that is specific to Exoplayer + * It extends the RNVPlugin interface + */ +interface RNVExoplayerPlugin : RNVPlugin { + /** + * Optional function that allows plugin to provide custom DRM manager + * Only one plugin can provide DRM manager at a time + * @return DRMManagerSpec implementation if plugin wants to handle DRM, null otherwise + */ + fun getDRMManager(): DRMManagerSpec? = null + + /** + * Function called when a new player is created + * @param id: a random string identifying the player + * @param player: the instantiated player reference + * @note: This is helper that ensure that player is non null ExoPlayer + */ + fun onInstanceCreated(id: String, player: ExoPlayer) + + /** + * Function called when a player should be destroyed + * when this callback is called, the plugin shall free all + * resources and release all reference to Player object + * @param id: a random string identifying the player + * @param player: the player to release + * @note: This is helper that ensure that player is non null ExoPlayer + */ + fun onInstanceRemoved(id: String, player: ExoPlayer) + + override fun onInstanceCreated(id: String, player: Any) { + if (player is ExoPlayer) { + onInstanceCreated(id, player) + } + } + + override fun onInstanceRemoved(id: String, player: Any) { + if (player is ExoPlayer) { + onInstanceRemoved(id, player) + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 2a4dabac..7f46d059 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -921,25 +921,32 @@ public class ReactExoplayerView extends FrameLayout implements return null; } - private DrmSessionManager initializePlayerDrm() { - DrmSessionManager drmSessionManager = null; - DRMProps drmProps = source.getDrmProps(); - // need to realign UUID in DRM Props from source - if (drmProps != null && drmProps.getDrmType() != null) { - UUID uuid = Util.getDrmUuid(drmProps.getDrmType()); - if (uuid != null) { - try { - DebugLog.w(TAG, "drm buildDrmSessionManager"); - drmSessionManager = buildDrmSessionManager(uuid, drmProps); - } 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); - eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); - } - } + private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException { + if (Util.SDK_INT < 18) { + return null; + } + + try { + // First check if there's a custom DRM manager registered through the plugin system + DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager(); + if (drmManager == null) { + // If no custom manager is registered, use the default implementation + drmManager = new DRMManager(buildHttpDataSourceFactory(false)); + } + + DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps); + if (drmSessionManager == null) { + eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007"); + } + return drmSessionManager; + } catch (UnsupportedDrmException ex) { + // Unsupported DRM exceptions are handled by the calling method + throw ex; + } catch (Exception ex) { + // Handle any other exception and emit to JS + eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006"); + return null; } - return drmSessionManager; } private void initializePlayerSource(Source runningSource) { @@ -998,6 +1005,27 @@ public class ReactExoplayerView extends FrameLayout implements finishPlayerInitialization(); } + private DrmSessionManager initializePlayerDrm() { + DrmSessionManager drmSessionManager = null; + DRMProps drmProps = source.getDrmProps(); + // need to realign UUID in DRM Props from source + if (drmProps != null && drmProps.getDrmType() != null) { + UUID uuid = Util.getDrmUuid(drmProps.getDrmType()); + if (uuid != null) { + try { + DebugLog.d(TAG, "drm buildDrmSessionManager"); + drmSessionManager = buildDrmSessionManager(uuid, drmProps); + } 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); + eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); + } + } + } + return drmSessionManager; + } + private void finishPlayerInitialization() { // Initializing the playerControlView initializePlayerControl(); @@ -1081,46 +1109,6 @@ public class ReactExoplayerView extends FrameLayout implements } } - private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException { - return buildDrmSessionManager(uuid, drmProps, 0); - } - - private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps, int retryCount) throws UnsupportedDrmException { - if (Util.SDK_INT < 18) { - return null; - } - try { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(), - buildHttpDataSourceFactory(false)); - - String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader(); - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); - } - FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); - if (hasDrmFailed) { - // When DRM fails using L1 we want to switch to L3 - mediaDrm.setPropertyString("securityLevel", "L3"); - } - return new DefaultDrmSessionManager.Builder() - .setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm) - .setKeyRequestParameters(null) - .setMultiSession(drmProps.getMultiDrm()) - .build(drmCallback); - } catch (UnsupportedDrmException ex) { - // Unsupported DRM exceptions are handled by the calling method - throw ex; - } catch (Exception ex) { - if (retryCount < 3) { - // Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason - return buildDrmSessionManager(uuid, drmProps, ++retryCount); - } - // Handle the unknow exception and emit to JS - eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006"); - return null; - } - } - private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) { if (uri == null) { throw new IllegalStateException("Invalid video uri"); diff --git a/android/src/main/java/com/brentvatne/react/RNVPlugin.kt b/android/src/main/java/com/brentvatne/react/RNVPlugin.kt index 2cdf6766..8c7bcb2c 100644 --- a/android/src/main/java/com/brentvatne/react/RNVPlugin.kt +++ b/android/src/main/java/com/brentvatne/react/RNVPlugin.kt @@ -1,7 +1,8 @@ package com.brentvatne.react /** - * Plugin interface definition + * Plugin interface definition for RNV plugins that does not have dependencies nor logic specific to any player + * It is the base interface for all RNV plugins */ interface RNVPlugin { /** diff --git a/android/src/main/java/com/brentvatne/react/ReactNativeVideoManager.kt b/android/src/main/java/com/brentvatne/react/ReactNativeVideoManager.kt index 0104c2b4..975ef4ee 100644 --- a/android/src/main/java/com/brentvatne/react/ReactNativeVideoManager.kt +++ b/android/src/main/java/com/brentvatne/react/ReactNativeVideoManager.kt @@ -1,10 +1,12 @@ package com.brentvatne.react import com.brentvatne.common.toolbox.DebugLog +import com.brentvatne.exoplayer.DRMManagerSpec +import com.brentvatne.exoplayer.RNVExoplayerPlugin /** * ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app - * It handles the list of