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
This commit is contained in:
Krzysztof Moch
2025-03-12 14:13:46 +01:00
committed by GitHub
parent b5103743e8
commit 6e6f91517c
18 changed files with 600 additions and 151 deletions

View File

@@ -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)
}
}
}

View File

@@ -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?
}

View File

@@ -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)
}
}
}

View File

@@ -921,25 +921,32 @@ public class ReactExoplayerView extends FrameLayout implements
return null; return null;
} }
private DrmSessionManager initializePlayerDrm() { private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
DrmSessionManager drmSessionManager = null; if (Util.SDK_INT < 18) {
DRMProps drmProps = source.getDrmProps(); return null;
// 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 { try {
DebugLog.w(TAG, "drm buildDrmSessionManager"); // First check if there's a custom DRM manager registered through the plugin system
drmSessionManager = buildDrmSessionManager(uuid, drmProps); DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager();
} catch (UnsupportedDrmException e) { if (drmManager == null) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported // If no custom manager is registered, use the default implementation
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME drmManager = new DRMManager(buildHttpDataSourceFactory(false));
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
}
} }
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; 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;
}
} }
private void initializePlayerSource(Source runningSource) { private void initializePlayerSource(Source runningSource) {
@@ -998,6 +1005,27 @@ public class ReactExoplayerView extends FrameLayout implements
finishPlayerInitialization(); 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() { private void finishPlayerInitialization() {
// Initializing the playerControlView // Initializing the playerControlView
initializePlayerControl(); 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) { private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
if (uri == null) { if (uri == null) {
throw new IllegalStateException("Invalid video uri"); throw new IllegalStateException("Invalid video uri");

View File

@@ -1,7 +1,8 @@
package com.brentvatne.react 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 { interface RNVPlugin {
/** /**

View File

@@ -1,10 +1,12 @@
package com.brentvatne.react package com.brentvatne.react
import com.brentvatne.common.toolbox.DebugLog 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 * ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
* It handles the list of <Video view instanced and registration of plugins * It handles the list of <Video/> view instanced and registration of plugins
*/ */
class ReactNativeVideoManager : RNVPlugin { class ReactNativeVideoManager : RNVPlugin {
companion object { companion object {
@@ -22,8 +24,9 @@ class ReactNativeVideoManager : RNVPlugin {
} }
} }
private val pluginList = ArrayList<RNVPlugin>()
private var customDRMManager: DRMManagerSpec? = null
private var instanceList: ArrayList<Any> = ArrayList() private var instanceList: ArrayList<Any> = ArrayList()
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
/** /**
* register a new ReactExoplayerViewManager in the managed list * register a new ReactExoplayerViewManager in the managed list
@@ -47,7 +50,8 @@ class ReactNativeVideoManager : RNVPlugin {
*/ */
fun registerPlugin(plugin: RNVPlugin) { fun registerPlugin(plugin: RNVPlugin) {
pluginList.add(plugin) pluginList.add(plugin)
return
maybeRegisterExoplayerPlugin(plugin)
} }
/** /**
@@ -55,9 +59,11 @@ class ReactNativeVideoManager : RNVPlugin {
*/ */
fun unregisterPlugin(plugin: RNVPlugin) { fun unregisterPlugin(plugin: RNVPlugin) {
pluginList.remove(plugin) pluginList.remove(plugin)
return
maybeUnregisterExoplayerPlugin(plugin)
} }
// ----------------------- Generic RNV plugin methods -----------------------
override fun onInstanceCreated(id: String, player: Any) { override fun onInstanceCreated(id: String, player: Any) {
pluginList.forEach { it.onInstanceCreated(id, player) } pluginList.forEach { it.onInstanceCreated(id, player) }
} }
@@ -65,4 +71,34 @@ class ReactNativeVideoManager : RNVPlugin {
override fun onInstanceRemoved(id: String, player: Any) { override fun onInstanceRemoved(id: String, player: Any) {
pluginList.forEach { it.onInstanceRemoved(id, player) } 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
}
}
} }

View File

@@ -34,29 +34,67 @@ npx create-react-native-library@latest react-native-video-custom-analytics
Both Android and iOS implementations expose an `RNVPlugin` interface. 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`. 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 ## Android Implementation
### 1. Create the Plugin ### 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: The `RNVPlugin` interface defines two functions:
```kotlin ```kotlin
/** /**
* Called when a new player instance is created. * Function called when a new player is created
* @param id: A unique identifier for the player instance. * @param id: a random string identifying the player
* @param player: The instantiated player reference. * @param player: the instantiated player reference
*/ */
fun onInstanceCreated(id: String, player: Any) fun onInstanceCreated(id: String, player: Any)
/** /**
* Called when a player instance should be destroyed. * Function called when a player should be destroyed
* The plugin should free resources and release all references to the player object. * when this callback is called, the plugin shall free all
* @param id: A unique identifier for the player instance. * resources and release all reference to Player object
* @param player: The player to release. * @param id: a random string identifying the player
* @param player: the player to release
*/ */
fun onInstanceRemoved(id: String, player: Any) fun onInstanceRemoved(id: String, player: Any)
``` ```
@@ -85,25 +123,54 @@ s.dependency "react-native-video"
### 2. Create the Plugin ### 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 ```swift
/** /**
* Called when a new player instance is created. * Function called when a new player is created
* @param player: The instantiated player reference. * @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. * Function called when a player should be destroyed
* The plugin should free resources and release all references to the player object. * when this callback is called, the plugin shall free all
* @param player: The player to release. * 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 ### 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. 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

View File

@@ -1024,7 +1024,7 @@ PODS:
- React-jsi (= 0.73.2) - React-jsi (= 0.73.2)
- React-logger (= 0.73.2) - React-logger (= 0.73.2)
- React-perflogger (= 0.73.2) - React-perflogger (= 0.73.2)
- ReactNativeHost (0.5.0): - ReactNativeHost (0.5.3):
- glog - glog
- RCT-Folly (= 2022.05.16.00) - RCT-Folly (= 2022.05.16.00)
- React-Core - React-Core
@@ -1213,43 +1213,43 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88 FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5
RCTRequired: 9b1e7e262745fb671e33c51c1078d093bd30e322 RCTRequired: 9b1e7e262745fb671e33c51c1078d093bd30e322
RCTTypeSafety: a759e3b086eccf3e2cbf2493d22f28e082f958e6 RCTTypeSafety: a759e3b086eccf3e2cbf2493d22f28e082f958e6
React: 805f5dd55bbdb92c36b4914c64aaae4c97d358dc React: 805f5dd55bbdb92c36b4914c64aaae4c97d358dc
React-callinvoker: 6a697867607c990c2c2c085296ee32cfb5e47c01 React-callinvoker: 6a697867607c990c2c2c085296ee32cfb5e47c01
React-Codegen: 39377d8c90c3fc0792753c9af53b788abfe5850b React-Codegen: ea685412377744d2e2108adb131fce2f040ddd14
React-Core: 943d6097aaf381b1e7c7e105eecd5a27b51c4e17 React-Core: d9a7e296b2e7b57f27d81b62a7d293d06527268a
React-CoreModules: 710e7c557a1a8180bd1645f5b4bf79f4bd3f5417 React-CoreModules: 399f72f0892befe0fde5d415703c0b9b356762e7
React-cxxreact: 0f0b3933c36dfe4ed10638a33398533f90ab78d3 React-cxxreact: beb7cb8adddd4b25d4262f55927dcd1d3577e99d
React-debug: f1637bce73342b2f6eee4982508fdfb088667a87 React-debug: f1637bce73342b2f6eee4982508fdfb088667a87
React-Fabric: ba7d74992ed878fdbf91f8b49eb725b310786980 React-Fabric: cb2eba1fd764229f9a5737fa7b339695d4949848
React-FabricImage: e7457fb89db50cb1b51d0546b5ff002b91026efe React-FabricImage: b86bf7e1c6560bf82ade361dac6f3281e68453ee
React-graphics: dd5af9d8b1b45171fd6933e19fed522f373bcb10 React-graphics: 87ba141b72379824c7835224c1a87993683151a8
React-ImageManager: c5b7db131eff71443d7f3a8d686fd841d18befd3 React-ImageManager: 1bc92d558d4d5de07c6c1a7244d33ad2a872728e
React-jsc: 94234736a90ea29f017f2ee76e5f358a6ba076a9 React-jsc: 94234736a90ea29f017f2ee76e5f358a6ba076a9
React-jserrorhandler: 97a6a12e2344c3c4fdd7ba1edefb005215c732f8 React-jserrorhandler: 0b1476485be6d79f09a8f0548c355b1cc14e8f21
React-jsi: 0cd661b6ea862c104706311f8265050ee3ecf5e4 React-jsi: dbfd3bab7712367d4c2aced271d794dde76f0d68
React-jsiexecutor: 94f6026bc4054b413f0ac5e210691c2916d99d1b React-jsiexecutor: 368e562638c31174479c00434d37fd67b761c15b
React-jsinspector: 03644c063fc3621c9a4e8bf263a8150909129618 React-jsinspector: 03644c063fc3621c9a4e8bf263a8150909129618
React-logger: 66b168e2b2bee57bd8ce9e69f739d805732a5570 React-logger: 66b168e2b2bee57bd8ce9e69f739d805732a5570
React-Mapbuffer: 9ee041e1d7be96da6d76a251f92e72b711c651d6 React-Mapbuffer: 9ee041e1d7be96da6d76a251f92e72b711c651d6
react-native-video: 6e6a4c453879c646d8a2e5c16ff0903af33daadd react-native-video: 6e6a4c453879c646d8a2e5c16ff0903af33daadd
React-nativeconfig: d753fbbc8cecc8ae413d615599ac378bbf6999bb React-nativeconfig: d753fbbc8cecc8ae413d615599ac378bbf6999bb
React-NativeModulesApple: 22c25a1baa4b0d0d4845dad2578fc017b0805589 React-NativeModulesApple: 0c22e17930a2de06bbd4d49a149351a5151283dc
React-perflogger: 29efe63b7ef5fbaaa50ef6eaa92482f98a24b97e React-perflogger: 29efe63b7ef5fbaaa50ef6eaa92482f98a24b97e
React-RCTActionSheet: 69134c62aefd362027b20da01cd5d14ffd39db3f React-RCTActionSheet: 69134c62aefd362027b20da01cd5d14ffd39db3f
React-RCTAnimation: 3b5a57087c7a5e727855b803d643ac1d445488f5 React-RCTAnimation: ed774e28e707ce47b1e2dc6aa7f8f3267b815061
React-RCTAppDelegate: 842870b97f47de7255908ba1ca8786aef877b0b8 React-RCTAppDelegate: b3312577f20a1c3aaf58ff6d561f3119c74b95b4
React-RCTBlob: 1fa011b5860c9a70802fab986ad334b458387b7a React-RCTBlob: 8c173ce722daff128efb38f29a390984800f9302
React-RCTFabric: c8f86a85501d70c8a77d71f22273e325ffb63fa0 React-RCTFabric: a0a76ccfa863b02382cb74e04450e9f69cd1cf49
React-RCTImage: 27b27f4663df9e776d0549ed2f3536213e793f1b React-RCTImage: 1f75f5d0539b381f70981386b1459fbb2c21eb9b
React-RCTLinking: 962880ce9d0e2ea83fd182953538fc4ed757d4da React-RCTLinking: febd566d57a9cb05a02f39771ccc8c20f8432e89
React-RCTNetwork: 73a756b44d4ad584bae13a5f1484e3ce12accac8 React-RCTNetwork: d427de729372fd50d7cb601db64d0fcd9ea9a514
React-RCTSettings: 6d7f8d807f05de3d01cfb182d14e5f400716faac React-RCTSettings: 73594c6c8c334c7d958cf98ac72335f3e4df9bf5
React-RCTText: 73006e95ca359595c2510c1c0114027c85a6ddd3 React-RCTText: f1079c24f45cec6ddb6363c12ad87f9a940b2ddb
React-RCTVibration: 599f427f9cbdd9c4bf38959ca020e8fef0717211 React-RCTVibration: 62420b57a47482d1b88dde64ba88d333f2625aab
React-rendererdebug: f2946e0a1c3b906e71555a7c4a39aa6a6c0e639b React-rendererdebug: a474ec4cdfed75211dce6c3828de8391cc5c4280
React-rncore: 74030de0ffef7b1a3fb77941168624534cc9ae7f React-rncore: 74030de0ffef7b1a3fb77941168624534cc9ae7f
React-runtimeexecutor: 2d1f64f58193f00a3ad71d3f89c2bfbfe11cf5a5 React-runtimeexecutor: 2d1f64f58193f00a3ad71d3f89c2bfbfe11cf5a5
React-runtimescheduler: 6517c0cdfae3ea29b599759e069ae97746163248 React-runtimescheduler: 6517c0cdfae3ea29b599759e069ae97746163248
@@ -1260,8 +1260,8 @@ SPEC CHECKSUMS:
ReactTestApp-Resources: 857244f3a23f2b3157b364fa06cf3e8866deff9c ReactTestApp-Resources: 857244f3a23f2b3157b364fa06cf3e8866deff9c
RNCPicker: d2ac37457765e0066fe17c93f536eae024b6e53a RNCPicker: d2ac37457765e0066fe17c93f536eae024b6e53a
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 Yoga: 13c8ef87792450193e117976337b8527b49e8c03
PODFILE CHECKSUM: 4f0c2a9ecae3454d8a3aae9fd7fdd7724fa2138a PODFILE CHECKSUM: 4f0c2a9ecae3454d8a3aae9fd7fdd7724fa2138a
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

View File

@@ -97,6 +97,9 @@ const DRMExample = () => {
newSource.drm = { newSource.drm = {
type: DRMType.WIDEVINE, type: DRMType.WIDEVINE,
licenseServer: widevineLicense, licenseServer: widevineLicense,
headers: {
'x-drm-userToken': token,
},
}; };
newSource.uri = dash; newSource.uri = dash;
} else { } else {

View File

@@ -5,14 +5,14 @@ import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger import androidx.media3.exoplayer.util.EventLogger
import com.brentvatne.common.toolbox.DebugLog 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.Promise
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReactMethod
class VideoPluginSampleModule(reactContext: ReactApplicationContext) : class VideoPluginSampleModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext), RNVPlugin, Player.Listener { ReactContextBaseJavaModule(reactContext), RNVExoplayerPlugin, Player.Listener {
private val debugEventLogger = EventLogger("RNVPluginSample") private val debugEventLogger = EventLogger("RNVPluginSample")
@@ -35,16 +35,12 @@ class VideoPluginSampleModule(reactContext: ReactApplicationContext) :
} }
override fun onInstanceCreated(id: String, player: Any) { override fun onInstanceCreated(id: String, player: ExoPlayer) {
if (player is ExoPlayer) {
player.addAnalyticsListener(debugEventLogger) player.addAnalyticsListener(debugEventLogger)
player.addListener(this) player.addListener(this)
} }
}
override fun onInstanceRemoved(id: String, player: Any) { override fun onInstanceRemoved(id: String, player: ExoPlayer) {
if (player is ExoPlayer) {
player.removeAnalyticsListener(debugEventLogger) player.removeAnalyticsListener(debugEventLogger)
} }
}
} }

View File

@@ -3,7 +3,7 @@ import AVFoundation
import AVKit import AVKit
@objc(VideoPluginSample) @objc(VideoPluginSample)
class VideoPluginSample: NSObject, RNVPlugin { class VideoPluginSample: RNVAVPlayerPlugin {
private var _playerRateChangeObserver: NSKeyValueObservation? private var _playerRateChangeObserver: NSKeyValueObservation?
private var _playerCurrentItemChangeObserver: NSKeyValueObservation? private var _playerCurrentItemChangeObserver: NSKeyValueObservation?
private var _playerItemStatusObserver: NSKeyValueObservation? private var _playerItemStatusObserver: NSKeyValueObservation?
@@ -16,6 +16,10 @@ class VideoPluginSample: NSObject, RNVPlugin {
ReactNativeVideoManager.shared.registerPlugin(plugin: self) ReactNativeVideoManager.shared.registerPlugin(plugin: self)
} }
deinit {
ReactNativeVideoManager.shared.unregisterPlugin(plugin: self)
}
@objc(withResolver:withRejecter:) @objc(withResolver:withRejecter:)
func setMetadata(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void { func setMetadata(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
@@ -25,24 +29,17 @@ class VideoPluginSample: NSObject, RNVPlugin {
/* /*
* Handlers called on player creation and destructon * Handlers called on player creation and destructon
*/ */
func onInstanceCreated(id: String, player: Any) { override func onInstanceCreated(id: String, player: AVPlayer) {
if player is AVPlayer {
let avPlayer = player as! AVPlayer
NSLog("plug onInstanceCreated") NSLog("plug onInstanceCreated")
_playerRateChangeObserver = avPlayer.observe(\.rate, options: [.old], changeHandler: handlePlaybackRateChange) _playerRateChangeObserver = player.observe(\.rate, options: [.old], changeHandler: handlePlaybackRateChange)
_playerCurrentItemChangeObserver = avPlayer.observe(\.currentItem, options: [.old], changeHandler: handleCurrentItemChange) _playerCurrentItemChangeObserver = player.observe(\.currentItem, options: [.old], changeHandler: handleCurrentItemChange)
}
} }
func onInstanceRemoved(id: String, player: Any) { override func onInstanceRemoved(id: String, player: AVPlayer) {
if player is AVPlayer {
let avPlayer = player as! AVPlayer
NSLog("plug onInstanceRemoved") NSLog("plug onInstanceRemoved")
_playerRateChangeObserver?.invalidate() _playerRateChangeObserver?.invalidate()
_playerCurrentItemChangeObserver?.invalidate() _playerCurrentItemChangeObserver?.invalidate()
} }
}
/** /**
* custom functions to be able to track AVPlayer state change * custom functions to be able to track AVPlayer state change

View File

@@ -1,4 +1,4 @@
struct DRMParams { public struct DRMParams {
let type: String? let type: String?
let licenseServer: String? let licenseServer: String?
let headers: [String: Any]? let headers: [String: Any]?

View File

@@ -7,7 +7,7 @@
import AVFoundation import AVFoundation
class DRMManager: NSObject { class DRMManager: NSObject, DRMManagerSpec {
static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue") static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue")
let contentKeySession: AVContentKeySession? let contentKeySession: AVContentKeySession?

View File

@@ -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)
}

View File

@@ -97,7 +97,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _didRequestAds = false private var _didRequestAds = false
private var _adPlaying = false private var _adPlaying = false
private lazy var _drmManager: DRMManager? = DRMManager() private lazy var _drmManager: DRMManagerSpec? = ReactNativeVideoManager.shared.getDRMManager()
private var _playerObserver: RCTPlayerObserver = .init() private var _playerObserver: RCTPlayerObserver = .init()
#if USE_VIDEO_CACHING #if USE_VIDEO_CACHING
@@ -512,7 +512,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
if source.drm.json != nil { if source.drm.json != nil {
if _drmManager == nil { if _drmManager == nil {
_drmManager = DRMManager() _drmManager = ReactNativeVideoManager.shared.getDRMManager()
} }
_drmManager?.createContentKeyRequest( _drmManager?.createContentKeyRequest(

View File

@@ -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)
}
}
}

View File

@@ -5,13 +5,20 @@
import Foundation 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 * Function called when a new player is created
* @param id: a random string identifying the player * @param id: a random string identifying the player
* @param player: the instantiated player reference * @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 * Function called when a player should be destroyed
* when this callback is called, the plugin shall free all * 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 id: a random string identifying the player
* @param player: the player to release * @param player: the player to release
*/ */
func onInstanceRemoved(id: String, player: Any) open func onInstanceRemoved(id _: String, player _: Any) { /* no-op */ }
} }

View File

@@ -8,13 +8,12 @@ import Foundation
public class ReactNativeVideoManager: RNVPlugin { public class ReactNativeVideoManager: RNVPlugin {
private let expectedMaxVideoCount = 2 private let expectedMaxVideoCount = 2
// create a private initializer
private init() {}
public static let shared: ReactNativeVideoManager = .init() public static let shared: ReactNativeVideoManager = .init()
public var pluginName: String = "ReactNativeVideoManager"
private var instanceCount = 0 private var instanceCount = 0
private var pluginList: [RNVPlugin] = Array() private var pluginList: Set<RNVPlugin> = Set()
private var customDRMManager: (RNVPlugin, DRMManagerSpec)?
/** /**
* register a new view * register a new view
@@ -37,15 +36,63 @@ public class ReactNativeVideoManager: RNVPlugin {
* register a new plugin in the managed list * register a new plugin in the managed list
*/ */
public func registerPlugin(plugin: RNVPlugin) { public func registerPlugin(plugin: RNVPlugin) {
pluginList.append(plugin) pluginList.insert(plugin)
return
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) } 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) } 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
}
}
} }