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 view instanced and registration of plugins
*/
class ReactNativeVideoManager : RNVPlugin {
companion object {
@@ -22,8 +24,9 @@ class ReactNativeVideoManager : RNVPlugin {
}
}
+ private val pluginList = ArrayList()
+ private var customDRMManager: DRMManagerSpec? = null
private var instanceList: ArrayList = ArrayList()
- private var pluginList: ArrayList = ArrayList()
/**
* register a new ReactExoplayerViewManager in the managed list
@@ -47,7 +50,8 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun registerPlugin(plugin: RNVPlugin) {
pluginList.add(plugin)
- return
+
+ maybeRegisterExoplayerPlugin(plugin)
}
/**
@@ -55,9 +59,11 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun unregisterPlugin(plugin: RNVPlugin) {
pluginList.remove(plugin)
- return
+
+ maybeUnregisterExoplayerPlugin(plugin)
}
+ // ----------------------- Generic RNV plugin methods -----------------------
override fun onInstanceCreated(id: String, player: Any) {
pluginList.forEach { it.onInstanceCreated(id, player) }
}
@@ -65,4 +71,34 @@ class ReactNativeVideoManager : RNVPlugin {
override fun onInstanceRemoved(id: String, player: Any) {
pluginList.forEach { it.onInstanceRemoved(id, player) }
}
+
+ // ----------------------- RNV Exoplayer plugin specific methods -----------------------
+ fun getDRMManager(): DRMManagerSpec? = customDRMManager
+
+ // ----------------------- Custom Plugins Helpers -----------------------
+ private fun maybeRegisterExoplayerPlugin(plugin: RNVPlugin) {
+ if (plugin !is RNVExoplayerPlugin) {
+ return
+ }
+
+ // Check if plugin provides DRM manager
+ plugin.getDRMManager()?.let { drmManager ->
+ if (customDRMManager != null) {
+ DebugLog.w("ReactNativeVideoManager", "Multiple DRM managers registered. This is not supported. Using first registered manager.")
+ return@let
+ }
+ customDRMManager = drmManager
+ }
+ }
+
+ private fun maybeUnregisterExoplayerPlugin(plugin: RNVPlugin) {
+ if (plugin !is RNVExoplayerPlugin) {
+ return
+ }
+
+ // If this plugin provided the DRM manager, remove it
+ if (plugin.getDRMManager() === customDRMManager) {
+ customDRMManager = null
+ }
+ }
}
diff --git a/docs/pages/other/plugin.md b/docs/pages/other/plugin.md
index c93fb244..36f3ec0a 100644
--- a/docs/pages/other/plugin.md
+++ b/docs/pages/other/plugin.md
@@ -34,29 +34,67 @@ npx create-react-native-library@latest react-native-video-custom-analytics
Both Android and iOS implementations expose an `RNVPlugin` interface.
Your `react-native-video-custom-analytics` package should implement this interface and register itself as a plugin for `react-native-video`.
+## Plugin Types
+
+There are two types of plugins you can implement:
+
+1. **Base Plugin (`RNVPlugin`)**: For general-purpose plugins that don't need specific player implementation details.
+2. **Player-Specific Plugins**:
+ - `RNVAVPlayerPlugin` for iOS: Provides type-safe access to AVPlayer instances
+ - `RNVExoplayerPlugin` for Android: Provides type-safe access to ExoPlayer instances
+
+Choose the appropriate plugin type based on your needs. If you need direct access to player-specific APIs, use the player-specific plugin classes.
+
## Android Implementation
### 1. Create the Plugin
-First, instantiate a class that extends `RNVPlugin`.
+You can implement either the base `RNVPlugin` interface or the player-specific `RNVExoplayerPlugin` interface.
-The recommended approach is to implement `RNVPlugin` inside the Module file (`VideoPluginSampleModule`).
+#### Base Plugin
+
+```kotlin
+class MyAnalyticsPlugin : RNVPlugin {
+ override fun onInstanceCreated(id: String, player: Any) {
+ // Handle player creation
+ }
+
+ override fun onInstanceRemoved(id: String, player: Any) {
+ // Handle player removal
+ }
+}
+```
+
+#### ExoPlayer-Specific Plugin
+
+```kotlin
+class MyExoPlayerAnalyticsPlugin : RNVExoplayerPlugin {
+ override fun onInstanceCreated(id: String, player: ExoPlayer) {
+ // Handle ExoPlayer creation with type-safe access
+ }
+
+ override fun onInstanceRemoved(id: String, player: ExoPlayer) {
+ // Handle ExoPlayer removal with type-safe access
+ }
+}
+```
The `RNVPlugin` interface defines two functions:
```kotlin
/**
- * Called when a new player instance is created.
- * @param id: A unique identifier for the player instance.
- * @param player: The instantiated player reference.
+ * Function called when a new player is created
+ * @param id: a random string identifying the player
+ * @param player: the instantiated player reference
*/
fun onInstanceCreated(id: String, player: Any)
/**
- * Called when a player instance should be destroyed.
- * The plugin should free resources and release all references to the player object.
- * @param id: A unique identifier for the player instance.
- * @param player: The player to release.
+ * 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
*/
fun onInstanceRemoved(id: String, player: Any)
```
@@ -85,25 +123,54 @@ s.dependency "react-native-video"
### 2. Create the Plugin
-Instantiate a class that extends `RNVPlugin`.
+You can implement either the base `RNVPlugin` class or the player-specific `RNVAVPlayerPlugin` class.
-The recommended approach is to implement `RNVPlugin` inside the entry point module file (`VideoPluginSample`).
+#### Base Plugin
-The `RNVPlugin` interface defines two functions:
+```swift
+class MyAnalyticsPlugin: RNVPlugin {
+ override func onInstanceCreated(id: String, player: Any) {
+ // Handle player creation
+ }
+
+ override func onInstanceRemoved(id: String, player: Any) {
+ // Handle player removal
+ }
+}
+```
+
+#### AVPlayer-Specific Plugin
+
+```swift
+class MyAVPlayerAnalyticsPlugin: RNVAVPlayerPlugin {
+ override func onInstanceCreated(id: String, player: AVPlayer) {
+ // Handle AVPlayer creation with type-safe access
+ }
+
+ override func onInstanceRemoved(id: String, player: AVPlayer) {
+ // Handle AVPlayer removal with type-safe access
+ }
+}
+```
+
+The `RNVPlugin` class defines two methods:
```swift
/**
- * Called when a new player instance is created.
- * @param player: The instantiated player reference.
+ * Function called when a new player is created
+ * @param id: a random string identifying the player
+ * @param player: the instantiated player reference
*/
-func onInstanceCreated(player: Any)
+open func onInstanceCreated(id: String, player: Any) { /* no-op */ }
/**
- * Called when a player instance should be destroyed.
- * The plugin should free resources and release all references to the player object.
- * @param player: The player to release.
+ * 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
*/
-func onInstanceRemoved(player: Any)
+open func onInstanceRemoved(id: String, player: Any) { /* no-op */ }
```
### 3. Register the Plugin
@@ -128,3 +195,117 @@ override init() {
```
Once registered, your module can track player updates and report analytics data to your backend.
+
+## Custom DRM Manager
+
+You can provide a custom DRM manager through your plugin to handle DRM in a custom way. This is useful when you need to integrate with a specific DRM provider or implement custom DRM logic.
+
+### Android Implementation
+
+#### 1/ Create custom DRM manager
+
+Create a class that implements the `DRMManagerSpec` interface:
+
+```kotlin
+class CustomDRMManager : DRMManagerSpec {
+ @Throws(UnsupportedDrmException::class)
+ override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? {
+ // Your custom implementation for building DRM session manager
+ // Return null if the DRM scheme is not supported
+ // Throw UnsupportedDrmException if the DRM scheme is invalid
+ }
+}
+```
+
+#### 2/ Register DRM manager in your plugin
+
+Implement `getDRMManager()` in your ExoPlayer plugin to provide the custom DRM manager:
+
+```kotlin
+class CustomVideoPlugin : RNVExoplayerPlugin {
+ private val drmManager = CustomDRMManager()
+
+ override fun getDRMManager(): DRMManagerSpec? {
+ return drmManager
+ }
+
+ override fun onInstanceCreated(id: String, player: ExoPlayer) {
+ // Handle player creation
+ }
+
+ override fun onInstanceRemoved(id: String, player: ExoPlayer) {
+ // Handle player removal
+ }
+}
+```
+
+### iOS Implementation
+
+#### 1/ Create custom DRM manager
+
+Create a class that implements the `DRMManagerSpec` protocol:
+
+```swift
+class CustomDRMManager: NSObject, DRMManagerSpec {
+ func createContentKeyRequest(
+ asset: AVContentKeyRecipient,
+ drmProps: DRMParams?,
+ reactTag: NSNumber?,
+ onVideoError: RCTDirectEventBlock?,
+ onGetLicense: RCTDirectEventBlock?
+ ) {
+ // Initialize content key session and handle key request
+ }
+
+ func handleContentKeyRequest(keyRequest: AVContentKeyRequest) {
+ // Process the content key request
+ }
+
+ func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws {
+ // Finish processing the key request with the obtained license
+ }
+
+ func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) {
+ // Handle any errors during the DRM process
+ }
+
+ func setJSLicenseResult(license: String, licenseUrl: String) {
+ // Handle successful license acquisition from JS side
+ }
+
+ func setJSLicenseError(error: String, licenseUrl: String) {
+ // Handle license acquisition errors from JS side
+ }
+}
+```
+
+#### 2/ Register DRM manager in your plugin
+
+Implement `getDRMManager()` in your AVPlayer plugin to provide the custom DRM manager:
+
+```swift
+class CustomVideoPlugin: RNVAVPlayerPlugin {
+ override func getDRMManager() -> DRMManagerSpec? {
+ return CustomDRMManager()
+ }
+
+ override func onInstanceCreated(id: String, player: AVPlayer) {
+ // Handle player creation
+ }
+
+ override func onInstanceRemoved(id: String, player: AVPlayer) {
+ // Handle player removal
+ }
+}
+```
+
+### Important notes about DRM managers:
+
+1. Only one plugin can provide a DRM manager at a time. If multiple plugins try to provide DRM managers, only the first one will be used.
+2. The custom DRM manager will be used for all video instances in the app.
+3. If no custom DRM manager is provided:
+ - On iOS, the default FairPlay-based implementation will be used
+ - On Android, the default ExoPlayer DRM implementation will be used
+4. The DRM manager must handle all DRM-related functionality:
+ - On iOS: key requests, license acquisition, and error handling through AVContentKeySession
+ - On Android: DRM session management and license acquisition through ExoPlayer's DrmSessionManager
\ No newline at end of file
diff --git a/examples/bare/ios/Podfile.lock b/examples/bare/ios/Podfile.lock
index f1a9a9a2..117768e6 100644
--- a/examples/bare/ios/Podfile.lock
+++ b/examples/bare/ios/Podfile.lock
@@ -1024,7 +1024,7 @@ PODS:
- React-jsi (= 0.73.2)
- React-logger (= 0.73.2)
- React-perflogger (= 0.73.2)
- - ReactNativeHost (0.5.0):
+ - ReactNativeHost (0.5.3):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
@@ -1213,43 +1213,43 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
- RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
+ RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
RCTRequired: 9b1e7e262745fb671e33c51c1078d093bd30e322
RCTTypeSafety: a759e3b086eccf3e2cbf2493d22f28e082f958e6
React: 805f5dd55bbdb92c36b4914c64aaae4c97d358dc
React-callinvoker: 6a697867607c990c2c2c085296ee32cfb5e47c01
- React-Codegen: 39377d8c90c3fc0792753c9af53b788abfe5850b
- React-Core: 943d6097aaf381b1e7c7e105eecd5a27b51c4e17
- React-CoreModules: 710e7c557a1a8180bd1645f5b4bf79f4bd3f5417
- React-cxxreact: 0f0b3933c36dfe4ed10638a33398533f90ab78d3
+ React-Codegen: ea685412377744d2e2108adb131fce2f040ddd14
+ React-Core: d9a7e296b2e7b57f27d81b62a7d293d06527268a
+ React-CoreModules: 399f72f0892befe0fde5d415703c0b9b356762e7
+ React-cxxreact: beb7cb8adddd4b25d4262f55927dcd1d3577e99d
React-debug: f1637bce73342b2f6eee4982508fdfb088667a87
- React-Fabric: ba7d74992ed878fdbf91f8b49eb725b310786980
- React-FabricImage: e7457fb89db50cb1b51d0546b5ff002b91026efe
- React-graphics: dd5af9d8b1b45171fd6933e19fed522f373bcb10
- React-ImageManager: c5b7db131eff71443d7f3a8d686fd841d18befd3
+ React-Fabric: cb2eba1fd764229f9a5737fa7b339695d4949848
+ React-FabricImage: b86bf7e1c6560bf82ade361dac6f3281e68453ee
+ React-graphics: 87ba141b72379824c7835224c1a87993683151a8
+ React-ImageManager: 1bc92d558d4d5de07c6c1a7244d33ad2a872728e
React-jsc: 94234736a90ea29f017f2ee76e5f358a6ba076a9
- React-jserrorhandler: 97a6a12e2344c3c4fdd7ba1edefb005215c732f8
- React-jsi: 0cd661b6ea862c104706311f8265050ee3ecf5e4
- React-jsiexecutor: 94f6026bc4054b413f0ac5e210691c2916d99d1b
+ React-jserrorhandler: 0b1476485be6d79f09a8f0548c355b1cc14e8f21
+ React-jsi: dbfd3bab7712367d4c2aced271d794dde76f0d68
+ React-jsiexecutor: 368e562638c31174479c00434d37fd67b761c15b
React-jsinspector: 03644c063fc3621c9a4e8bf263a8150909129618
React-logger: 66b168e2b2bee57bd8ce9e69f739d805732a5570
React-Mapbuffer: 9ee041e1d7be96da6d76a251f92e72b711c651d6
react-native-video: 6e6a4c453879c646d8a2e5c16ff0903af33daadd
React-nativeconfig: d753fbbc8cecc8ae413d615599ac378bbf6999bb
- React-NativeModulesApple: 22c25a1baa4b0d0d4845dad2578fc017b0805589
+ React-NativeModulesApple: 0c22e17930a2de06bbd4d49a149351a5151283dc
React-perflogger: 29efe63b7ef5fbaaa50ef6eaa92482f98a24b97e
React-RCTActionSheet: 69134c62aefd362027b20da01cd5d14ffd39db3f
- React-RCTAnimation: 3b5a57087c7a5e727855b803d643ac1d445488f5
- React-RCTAppDelegate: 842870b97f47de7255908ba1ca8786aef877b0b8
- React-RCTBlob: 1fa011b5860c9a70802fab986ad334b458387b7a
- React-RCTFabric: c8f86a85501d70c8a77d71f22273e325ffb63fa0
- React-RCTImage: 27b27f4663df9e776d0549ed2f3536213e793f1b
- React-RCTLinking: 962880ce9d0e2ea83fd182953538fc4ed757d4da
- React-RCTNetwork: 73a756b44d4ad584bae13a5f1484e3ce12accac8
- React-RCTSettings: 6d7f8d807f05de3d01cfb182d14e5f400716faac
- React-RCTText: 73006e95ca359595c2510c1c0114027c85a6ddd3
- React-RCTVibration: 599f427f9cbdd9c4bf38959ca020e8fef0717211
- React-rendererdebug: f2946e0a1c3b906e71555a7c4a39aa6a6c0e639b
+ React-RCTAnimation: ed774e28e707ce47b1e2dc6aa7f8f3267b815061
+ React-RCTAppDelegate: b3312577f20a1c3aaf58ff6d561f3119c74b95b4
+ React-RCTBlob: 8c173ce722daff128efb38f29a390984800f9302
+ React-RCTFabric: a0a76ccfa863b02382cb74e04450e9f69cd1cf49
+ React-RCTImage: 1f75f5d0539b381f70981386b1459fbb2c21eb9b
+ React-RCTLinking: febd566d57a9cb05a02f39771ccc8c20f8432e89
+ React-RCTNetwork: d427de729372fd50d7cb601db64d0fcd9ea9a514
+ React-RCTSettings: 73594c6c8c334c7d958cf98ac72335f3e4df9bf5
+ React-RCTText: f1079c24f45cec6ddb6363c12ad87f9a940b2ddb
+ React-RCTVibration: 62420b57a47482d1b88dde64ba88d333f2625aab
+ React-rendererdebug: a474ec4cdfed75211dce6c3828de8391cc5c4280
React-rncore: 74030de0ffef7b1a3fb77941168624534cc9ae7f
React-runtimeexecutor: 2d1f64f58193f00a3ad71d3f89c2bfbfe11cf5a5
React-runtimescheduler: 6517c0cdfae3ea29b599759e069ae97746163248
@@ -1260,8 +1260,8 @@ SPEC CHECKSUMS:
ReactTestApp-Resources: 857244f3a23f2b3157b364fa06cf3e8866deff9c
RNCPicker: d2ac37457765e0066fe17c93f536eae024b6e53a
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
- Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047
+ Yoga: 13c8ef87792450193e117976337b8527b49e8c03
PODFILE CHECKSUM: 4f0c2a9ecae3454d8a3aae9fd7fdd7724fa2138a
-COCOAPODS: 1.15.2
+COCOAPODS: 1.16.2
diff --git a/examples/common/DRMExample.tsx b/examples/common/DRMExample.tsx
index b7c5df95..ef8b202d 100644
--- a/examples/common/DRMExample.tsx
+++ b/examples/common/DRMExample.tsx
@@ -97,6 +97,9 @@ const DRMExample = () => {
newSource.drm = {
type: DRMType.WIDEVINE,
licenseServer: widevineLicense,
+ headers: {
+ 'x-drm-userToken': token,
+ },
};
newSource.uri = dash;
} else {
diff --git a/examples/react-native-video-plugin-sample/android/src/main/java/com/videopluginsample/VideoPluginSampleModule.kt b/examples/react-native-video-plugin-sample/android/src/main/java/com/videopluginsample/VideoPluginSampleModule.kt
index 399deb88..bba2449d 100644
--- a/examples/react-native-video-plugin-sample/android/src/main/java/com/videopluginsample/VideoPluginSampleModule.kt
+++ b/examples/react-native-video-plugin-sample/android/src/main/java/com/videopluginsample/VideoPluginSampleModule.kt
@@ -5,14 +5,14 @@ import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger
import com.brentvatne.common.toolbox.DebugLog
-import com.brentvatne.react.RNVPlugin
+import com.brentvatne.exoplayer.RNVExoplayerPlugin
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
class VideoPluginSampleModule(reactContext: ReactApplicationContext) :
- ReactContextBaseJavaModule(reactContext), RNVPlugin, Player.Listener {
+ ReactContextBaseJavaModule(reactContext), RNVExoplayerPlugin, Player.Listener {
private val debugEventLogger = EventLogger("RNVPluginSample")
@@ -35,16 +35,12 @@ class VideoPluginSampleModule(reactContext: ReactApplicationContext) :
}
- override fun onInstanceCreated(id: String, player: Any) {
- if (player is ExoPlayer) {
- player.addAnalyticsListener(debugEventLogger)
- player.addListener(this)
- }
+ override fun onInstanceCreated(id: String, player: ExoPlayer) {
+ player.addAnalyticsListener(debugEventLogger)
+ player.addListener(this)
}
- override fun onInstanceRemoved(id: String, player: Any) {
- if (player is ExoPlayer) {
- player.removeAnalyticsListener(debugEventLogger)
- }
+ override fun onInstanceRemoved(id: String, player: ExoPlayer) {
+ player.removeAnalyticsListener(debugEventLogger)
}
}
diff --git a/examples/react-native-video-plugin-sample/ios/VideoPluginSample.swift b/examples/react-native-video-plugin-sample/ios/VideoPluginSample.swift
index 082f307e..b0bb7c02 100644
--- a/examples/react-native-video-plugin-sample/ios/VideoPluginSample.swift
+++ b/examples/react-native-video-plugin-sample/ios/VideoPluginSample.swift
@@ -3,7 +3,7 @@ import AVFoundation
import AVKit
@objc(VideoPluginSample)
-class VideoPluginSample: NSObject, RNVPlugin {
+class VideoPluginSample: RNVAVPlayerPlugin {
private var _playerRateChangeObserver: NSKeyValueObservation?
private var _playerCurrentItemChangeObserver: NSKeyValueObservation?
private var _playerItemStatusObserver: NSKeyValueObservation?
@@ -16,6 +16,10 @@ class VideoPluginSample: NSObject, RNVPlugin {
ReactNativeVideoManager.shared.registerPlugin(plugin: self)
}
+ deinit {
+ ReactNativeVideoManager.shared.unregisterPlugin(plugin: self)
+ }
+
@objc(withResolver:withRejecter:)
func setMetadata(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
@@ -25,23 +29,16 @@ class VideoPluginSample: NSObject, RNVPlugin {
/*
* Handlers called on player creation and destructon
*/
- func onInstanceCreated(id: String, player: Any) {
- if player is AVPlayer {
- let avPlayer = player as! AVPlayer
- NSLog("plug onInstanceCreated")
- _playerRateChangeObserver = avPlayer.observe(\.rate, options: [.old], changeHandler: handlePlaybackRateChange)
- _playerCurrentItemChangeObserver = avPlayer.observe(\.currentItem, options: [.old], changeHandler: handleCurrentItemChange)
-
- }
+ override func onInstanceCreated(id: String, player: AVPlayer) {
+ NSLog("plug onInstanceCreated")
+ _playerRateChangeObserver = player.observe(\.rate, options: [.old], changeHandler: handlePlaybackRateChange)
+ _playerCurrentItemChangeObserver = player.observe(\.currentItem, options: [.old], changeHandler: handleCurrentItemChange)
}
- func onInstanceRemoved(id: String, player: Any) {
- if player is AVPlayer {
- let avPlayer = player as! AVPlayer
- NSLog("plug onInstanceRemoved")
- _playerRateChangeObserver?.invalidate()
- _playerCurrentItemChangeObserver?.invalidate()
- }
+ override func onInstanceRemoved(id: String, player: AVPlayer) {
+ NSLog("plug onInstanceRemoved")
+ _playerRateChangeObserver?.invalidate()
+ _playerCurrentItemChangeObserver?.invalidate()
}
/**
diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift
index bf8a4d2a..a520de97 100644
--- a/ios/Video/DataStructures/DRMParams.swift
+++ b/ios/Video/DataStructures/DRMParams.swift
@@ -1,4 +1,4 @@
-struct DRMParams {
+public struct DRMParams {
let type: String?
let licenseServer: String?
let headers: [String: Any]?
diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift
index 0b3facd3..93f95a98 100644
--- a/ios/Video/Features/DRMManager.swift
+++ b/ios/Video/Features/DRMManager.swift
@@ -7,7 +7,7 @@
import AVFoundation
-class DRMManager: NSObject {
+class DRMManager: NSObject, DRMManagerSpec {
static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue")
let contentKeySession: AVContentKeySession?
diff --git a/ios/Video/Features/DRMManagerSpec.swift b/ios/Video/Features/DRMManagerSpec.swift
new file mode 100644
index 00000000..3a93cf8a
--- /dev/null
+++ b/ios/Video/Features/DRMManagerSpec.swift
@@ -0,0 +1,17 @@
+import AVFoundation
+
+public protocol DRMManagerSpec: NSObject, AVContentKeySessionDelegate {
+ func createContentKeyRequest(
+ asset: AVContentKeyRecipient,
+ drmParams: DRMParams?,
+ reactTag: NSNumber?,
+ onVideoError: RCTDirectEventBlock?,
+ onGetLicense: RCTDirectEventBlock?
+ )
+
+ func handleContentKeyRequest(keyRequest: AVContentKeyRequest)
+ func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws
+ func handleError(_ error: Error, for keyRequest: AVContentKeyRequest)
+ func setJSLicenseResult(license: String, licenseUrl: String)
+ func setJSLicenseError(error: String, licenseUrl: String)
+}
diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift
index 69699218..7ea1cdf2 100644
--- a/ios/Video/RCTVideo.swift
+++ b/ios/Video/RCTVideo.swift
@@ -97,7 +97,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _didRequestAds = false
private var _adPlaying = false
- private lazy var _drmManager: DRMManager? = DRMManager()
+ private lazy var _drmManager: DRMManagerSpec? = ReactNativeVideoManager.shared.getDRMManager()
private var _playerObserver: RCTPlayerObserver = .init()
#if USE_VIDEO_CACHING
@@ -512,7 +512,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
if source.drm.json != nil {
if _drmManager == nil {
- _drmManager = DRMManager()
+ _drmManager = ReactNativeVideoManager.shared.getDRMManager()
}
_drmManager?.createContentKeyRequest(
diff --git a/ios/Video/RNVAVPlayerPlugin.swift b/ios/Video/RNVAVPlayerPlugin.swift
new file mode 100644
index 00000000..1e62d55d
--- /dev/null
+++ b/ios/Video/RNVAVPlayerPlugin.swift
@@ -0,0 +1,52 @@
+//
+// RNVAVPlayerPlugin.swift
+// react-native-video
+//
+
+import AVFoundation
+import Foundation
+
+/**
+ * class for RNV plugins that have dependencies or logic that is specific to AVPlayer
+ * It extends the RNVPlugin interface
+ */
+open class RNVAVPlayerPlugin: RNVPlugin {
+ // MARK: - Definitions
+
+ /**
+ * Optional function that allows plugin to provide custom DRM manager
+ * Only one plugin can provide DRM manager at a time
+ * @return: DRMManagerSpec type if plugin wants to handle DRM, nil otherwise
+ */
+ open func getDRMManager() -> DRMManagerSpec? { nil }
+
+ /**
+ * Function called when a new AVPlayer instance is created
+ * @param id: a random string identifying the player
+ * @param player: the instantiated AVPlayer
+ * @note: This is helper that ensure that player is non null AVPlayer
+ */
+ open func onInstanceCreated(id _: String, player _: AVPlayer) { /* no-op */ }
+
+ /**
+ * Function called when a AVPlayer instance is being removed
+ * @param id: a random string identifying the player
+ * @param player: the AVPlayer to release
+ * @note: This is helper that ensure that player is non null AVPlayer
+ */
+ open func onInstanceRemoved(id _: String, player _: AVPlayer) { /* no-op */ }
+
+ // MARK: - RNVPlugin methods
+
+ override public func onInstanceCreated(id: String, player: Any) {
+ if let avPlayer = player as? AVPlayer {
+ onInstanceCreated(id: id, player: avPlayer)
+ }
+ }
+
+ override public func onInstanceRemoved(id: String, player: Any) {
+ if let avPlayer = player as? AVPlayer {
+ onInstanceRemoved(id: id, player: avPlayer)
+ }
+ }
+}
diff --git a/ios/Video/RNVPlugin.swift b/ios/Video/RNVPlugin.swift
index 93016cd5..485d981a 100644
--- a/ios/Video/RNVPlugin.swift
+++ b/ios/Video/RNVPlugin.swift
@@ -5,13 +5,20 @@
import Foundation
-public protocol RNVPlugin {
+/**
+ * class for RNV plugins that does not have dependencies or logic specific to any player
+ * It is the base interface for all RNV plugins
+ *
+ * If you need to have dependencies or logic specific to a player, use the RNVAVPlayerPlugin
+ */
+open class RNVPlugin: NSObject {
/**
* Function called when a new player is created
* @param id: a random string identifying the player
* @param player: the instantiated player reference
*/
- func onInstanceCreated(id: String, player: Any)
+ open func onInstanceCreated(id _: String, player _: Any) { /* no-op */ }
+
/**
* Function called when a player should be destroyed
* when this callback is called, the plugin shall free all
@@ -19,5 +26,5 @@ public protocol RNVPlugin {
* @param id: a random string identifying the player
* @param player: the player to release
*/
- func onInstanceRemoved(id: String, player: Any)
+ open func onInstanceRemoved(id _: String, player _: Any) { /* no-op */ }
}
diff --git a/ios/Video/ReactNativeVideoManager.swift b/ios/Video/ReactNativeVideoManager.swift
index 77767f9c..d5efbcad 100644
--- a/ios/Video/ReactNativeVideoManager.swift
+++ b/ios/Video/ReactNativeVideoManager.swift
@@ -8,13 +8,12 @@ import Foundation
public class ReactNativeVideoManager: RNVPlugin {
private let expectedMaxVideoCount = 2
- // create a private initializer
- private init() {}
-
public static let shared: ReactNativeVideoManager = .init()
+ public var pluginName: String = "ReactNativeVideoManager"
private var instanceCount = 0
- private var pluginList: [RNVPlugin] = Array()
+ private var pluginList: Set = Set()
+ private var customDRMManager: (RNVPlugin, DRMManagerSpec)?
/**
* register a new view
@@ -37,15 +36,63 @@ public class ReactNativeVideoManager: RNVPlugin {
* register a new plugin in the managed list
*/
public func registerPlugin(plugin: RNVPlugin) {
- pluginList.append(plugin)
- return
+ pluginList.insert(plugin)
+
+ maybeRegisterAVPlayerPlugin(plugin: plugin)
}
- public func onInstanceCreated(id: String, player: Any) {
+ public func unregisterPlugin(plugin: RNVPlugin) {
+ pluginList.remove(plugin)
+
+ maybeUnregisterAVPlayerPlugin(plugin: plugin)
+ }
+
+ // MARK: - RNVPlugin methods
+
+ override public func onInstanceCreated(id: String, player: Any) {
pluginList.forEach { it in it.onInstanceCreated(id: id, player: player) }
}
- public func onInstanceRemoved(id: String, player: Any) {
+ override public func onInstanceRemoved(id: String, player: Any) {
pluginList.forEach { it in it.onInstanceRemoved(id: id, player: player) }
}
+
+ // MARK: - RNV AVPlayer plugin specific methods
+
+ /**
+ * If a custom DRM manager is registered through a plugin, it will be used
+ * Otherwise, the default DRMManager will be used
+ */
+ public func getDRMManager() -> DRMManagerSpec? {
+ return customDRMManager?.1 ?? DRMManager()
+ }
+
+ // MARK: - Helper methods
+
+ func maybeRegisterAVPlayerPlugin(plugin: RNVPlugin) {
+ guard let avpPlugin = plugin as? RNVAVPlayerPlugin else {
+ return
+ }
+
+ if let drmManager = avpPlugin.getDRMManager() {
+ if customDRMManager != nil {
+ DebugLog(
+ "Multiple DRM managers registered. This is not supported. Using first registered manager."
+ )
+ return
+ }
+
+ customDRMManager = (plugin, drmManager)
+ }
+ }
+
+ func maybeUnregisterAVPlayerPlugin(plugin: RNVPlugin) {
+ guard let avpPlugin = plugin as? RNVAVPlayerPlugin else {
+ return
+ }
+
+ if customDRMManager?.0 == plugin {
+ customDRMManager = nil
+ }
+ }
}