Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow plugins to implement custom drm provider #4366

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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)
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)
}
}
}
18 changes: 18 additions & 0 deletions android/src/main/java/com/brentvatne/exoplayer/DRMManagerSpec.kt
Original file line number Diff line number Diff line change
@@ -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?
}
102 changes: 45 additions & 57 deletions android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
Original file line number Diff line number Diff line change
Expand Up @@ -920,25 +920,32 @@ private AdsMediaSource initializeAds(MediaSource videoSource, Source runningSour
return null;
}

private DrmSessionManager initializePlayerDrm() {
DrmSessionManager drmSessionManager = null;
DRMProps drmProps = source.getDrmProps();
// need to realign UUID in DRM Props from source
if (drmProps != null && drmProps.getDrmType() != null) {
UUID uuid = Util.getDrmUuid(drmProps.getDrmType());
if (uuid != null) {
try {
DebugLog.w(TAG, "drm buildDrmSessionManager");
drmSessionManager = buildDrmSessionManager(uuid, drmProps);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}

try {
// First check if there's a custom DRM manager registered through the plugin system
DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager();
if (drmManager == null) {
// If no custom manager is registered, use the default implementation
drmManager = new DRMManager(buildHttpDataSourceFactory(false));
}

DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps);
if (drmSessionManager == null) {
eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007");
}
return drmSessionManager;
} catch (UnsupportedDrmException ex) {
// Unsupported DRM exceptions are handled by the calling method
throw ex;
} catch (Exception ex) {
// Handle any other exception and emit to JS
eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
return null;
}
return drmSessionManager;
}

private void initializePlayerSource(Source runningSource) {
Expand Down Expand Up @@ -997,6 +1004,27 @@ private void initializePlayerSource(Source runningSource) {
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.w(TAG, "drm buildDrmSessionManager");
drmSessionManager = buildDrmSessionManager(uuid, drmProps);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
}
}
}
return drmSessionManager;
}

private void finishPlayerInitialization() {
// Initializing the playerControlView
initializePlayerControl();
Expand Down Expand Up @@ -1080,46 +1108,6 @@ private void cleanupPlaybackService() {
}
}

private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, drmProps, 0);
}

private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps, int retryCount) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
try {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(),
buildHttpDataSourceFactory(false));

String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader();
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
if (hasDrmFailed) {
// When DRM fails using L1 we want to switch to L3
mediaDrm.setPropertyString("securityLevel", "L3");
}
return new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm)
.setKeyRequestParameters(null)
.setMultiSession(drmProps.getMultiDrm())
.build(drmCallback);
} catch (UnsupportedDrmException ex) {
// Unsupported DRM exceptions are handled by the calling method
throw ex;
} catch (Exception ex) {
if (retryCount < 3) {
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
return buildDrmSessionManager(uuid, drmProps, ++retryCount);
}
// Handle the unknow exception and emit to JS
eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
return null;
}
}

private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
if (uri == null) {
throw new IllegalStateException("Invalid video uri");
Expand Down
9 changes: 9 additions & 0 deletions android/src/main/java/com/brentvatne/react/RNVPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.brentvatne.react

import com.brentvatne.exoplayer.DRMManagerSpec

/**
* Plugin interface definition
*/
Expand All @@ -19,4 +21,11 @@ interface RNVPlugin {
* @param player: the player to release
*/
fun onInstanceRemoved(id: String, player: Any)

/**
* 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
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.brentvatne.react

import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.exoplayer.DRMManagerSpec

/**
* 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 {
companion object {
Expand All @@ -22,8 +23,9 @@ class ReactNativeVideoManager : RNVPlugin {
}
}

private val pluginList = ArrayList<RNVPlugin>()
private var customDRMManager: DRMManagerSpec? = null
private var instanceList: ArrayList<Any> = ArrayList()
private var pluginList: ArrayList<RNVPlugin> = ArrayList()

/**
* register a new ReactExoplayerViewManager in the managed list
Expand All @@ -47,15 +49,27 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun registerPlugin(plugin: RNVPlugin) {
pluginList.add(plugin)
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
}
}

/**
* unregister a plugin from the managed list
*/
fun unregisterPlugin(plugin: RNVPlugin) {
pluginList.remove(plugin)
return

// If this plugin provided the DRM manager, remove it
if (plugin.getDRMManager() === customDRMManager) {
customDRMManager = null
}
}

override fun onInstanceCreated(id: String, player: Any) {
Expand All @@ -65,4 +79,6 @@ class ReactNativeVideoManager : RNVPlugin {
override fun onInstanceRemoved(id: String, player: Any) {
pluginList.forEach { it.onInstanceRemoved(id, player) }
}

override fun getDRMManager(): DRMManagerSpec? = customDRMManager
}
Loading
Loading