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:
59
android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt
Normal file
59
android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]?
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|
||||||
|
|||||||
17
ios/Video/Features/DRMManagerSpec.swift
Normal file
17
ios/Video/Features/DRMManagerSpec.swift
Normal 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)
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
52
ios/Video/RNVAVPlayerPlugin.swift
Normal file
52
ios/Video/RNVAVPlayerPlugin.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user