feat(android): Support Common Media Client Data (CMCD) (#4034)
* feat(VideoNativeComponent.ts): add support for cmcd configuration in VideoSrc type to enable cmcd feature on android feat(video.ts): introduce CmcdMode enum and CmcdConfiguration type to define cmcd configuration options * feat(Video.tsx): add support for CMCD configuration in Video component to handle Content Management and Delivery (CMCD) headers for Android platform. * feat(CMCDProps.kt): add CMCDProps class to handle CMCD related properties and parsing logic for React Native module * feat(CMCDConfig.kt): add CMCDConfig class to handle CMCD configuration for ExoPlayer with support for custom data and configuration options. * feat(ReactExoplayerViewManager.java): add support for CMCD configuration in ReactExoplayerViewManager to enable Content Management and Control Data (CMCD) for better video playback optimization. * feat(ReactExoplayerView.java): add support for setting CmcdConfiguration.Factory to customize CMCD configuration for media playback * feat(Source.kt): add support for CMCD properties linked to the source to enhance functionality and data handling * docs(props.mdx): add documentation for configuring CMCD parameters in the component, including usage examples and default values * refactor(ReactExoplayerViewManager.java): remove unused PROP_CMCD and prevCmcdConfig variables to clean up code and improve readability * refactor(Video.tsx): simplify cmcd configuration logic for better readability and maintainability * docs(props.mdx): improve props documentation for clarity and consistency feat(props.mdx): add definitions for CmcdMode enum and CmcdData type to enhance understanding of CMCD data structure and usage * refactor(CMCDProps.kt): refactor CMCDProps class to data class for improved readability and immutability - update CMCDProps class to use List instead of Array for properties * refactor(Video.tsx): refactor createCmcdHeader function to improve code readability and reduce duplication * fix(CMCDProps.kt): remove import statement for CmcdConfiguration * feat(ReactExoplayerView.java): add support for CMCD configuration in ReactExoplayerView component feat(ReactExoplayerViewManager.java): remove redundant CMCD configuration logic from ReactExoplayerViewManager to simplify code and improve maintainability * fix(Video.tsx): merge _cmcd memo into src memo for optimization
This commit is contained in:
51
android/src/main/java/com/brentvatne/common/api/CMCDProps.kt
Normal file
51
android/src/main/java/com/brentvatne/common/api/CMCDProps.kt
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.brentvatne.common.api
|
||||
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.ReadableType
|
||||
|
||||
data class CMCDProps(
|
||||
val cmcdObject: List<Pair<String, Any>> = emptyList(),
|
||||
val cmcdRequest: List<Pair<String, Any>> = emptyList(),
|
||||
val cmcdSession: List<Pair<String, Any>> = emptyList(),
|
||||
val cmcdStatus: List<Pair<String, Any>> = emptyList(),
|
||||
val mode: Int = 1
|
||||
) {
|
||||
companion object {
|
||||
private const val PROP_CMCD_OBJECT = "object"
|
||||
private const val PROP_CMCD_REQUEST = "request"
|
||||
private const val PROP_CMCD_SESSION = "session"
|
||||
private const val PROP_CMCD_STATUS = "status"
|
||||
private const val PROP_CMCD_MODE = "mode"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(src: ReadableMap?): CMCDProps? {
|
||||
if (src == null) return null
|
||||
|
||||
return CMCDProps(
|
||||
cmcdObject = parseKeyValuePairs(src.getArray(PROP_CMCD_OBJECT)),
|
||||
cmcdRequest = parseKeyValuePairs(src.getArray(PROP_CMCD_REQUEST)),
|
||||
cmcdSession = parseKeyValuePairs(src.getArray(PROP_CMCD_SESSION)),
|
||||
cmcdStatus = parseKeyValuePairs(src.getArray(PROP_CMCD_STATUS)),
|
||||
mode = safeGetInt(src, PROP_CMCD_MODE, 1)
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseKeyValuePairs(array: ReadableArray?): List<Pair<String, Any>> {
|
||||
if (array == null) return emptyList()
|
||||
|
||||
return (0 until array.size()).mapNotNull { i ->
|
||||
val item = array.getMap(i)
|
||||
val key = item?.getString("key")
|
||||
val value = when (item?.getType("value")) {
|
||||
ReadableType.Number -> item.getDouble("value")
|
||||
ReadableType.String -> item.getString("value")
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (key != null && value != null) Pair(key, value) else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -57,6 +57,11 @@ class Source {
|
||||
*/
|
||||
var textTracksAllowChunklessPreparation: Boolean = false
|
||||
|
||||
/**
|
||||
* CMCD properties linked to the source
|
||||
*/
|
||||
var cmcdProps: CMCDProps? = null
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
|
||||
|
||||
/** return true if this and src are equals */
|
||||
@@ -68,7 +73,8 @@ class Source {
|
||||
cropEndMs == other.cropEndMs &&
|
||||
startPositionMs == other.startPositionMs &&
|
||||
extension == other.extension &&
|
||||
drmProps == other.drmProps
|
||||
drmProps == other.drmProps &&
|
||||
cmcdProps == other.cmcdProps
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,6 +137,7 @@ class Source {
|
||||
private const val PROP_SRC_METADATA = "metadata"
|
||||
private const val PROP_SRC_HEADERS = "requestHeaders"
|
||||
private const val PROP_SRC_DRM = "drm"
|
||||
private const val PROP_SRC_CMCD = "cmcd"
|
||||
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
@@ -189,6 +196,7 @@ class Source {
|
||||
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
|
||||
source.extension = safeGetString(src, PROP_SRC_TYPE, null)
|
||||
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
|
||||
source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD))
|
||||
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
||||
|
||||
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
||||
|
41
android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt
Normal file
41
android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.brentvatne.exoplayer
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration
|
||||
import com.brentvatne.common.api.CMCDProps
|
||||
import com.google.common.collect.ImmutableListMultimap
|
||||
|
||||
class CMCDConfig(private val props: CMCDProps) {
|
||||
fun toCmcdConfigurationFactory(): CmcdConfiguration.Factory = CmcdConfiguration.Factory(::createCmcdConfiguration)
|
||||
|
||||
private fun createCmcdConfiguration(mediaItem: MediaItem): CmcdConfiguration =
|
||||
CmcdConfiguration(
|
||||
java.util.UUID.randomUUID().toString(),
|
||||
mediaItem.mediaId,
|
||||
object : CmcdConfiguration.RequestConfig {
|
||||
override fun getCustomData(): ImmutableListMultimap<String, String> = buildCustomData()
|
||||
},
|
||||
props.mode
|
||||
)
|
||||
|
||||
private fun buildCustomData(): ImmutableListMultimap<String, String> =
|
||||
ImmutableListMultimap.builder<String, String>().apply {
|
||||
addFormattedData(this, CmcdConfiguration.KEY_CMCD_OBJECT, props.cmcdObject)
|
||||
addFormattedData(this, CmcdConfiguration.KEY_CMCD_REQUEST, props.cmcdRequest)
|
||||
addFormattedData(this, CmcdConfiguration.KEY_CMCD_SESSION, props.cmcdSession)
|
||||
addFormattedData(this, CmcdConfiguration.KEY_CMCD_STATUS, props.cmcdStatus)
|
||||
}.build()
|
||||
|
||||
private fun addFormattedData(builder: ImmutableListMultimap.Builder<String, String>, key: String, dataList: List<Pair<String, Any>>) {
|
||||
dataList.forEach { (dataKey, dataValue) ->
|
||||
builder.put(key, formatKeyValue(dataKey, dataValue))
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatKeyValue(key: String, value: Any): String =
|
||||
when (value) {
|
||||
is String -> "$key=\"$value\""
|
||||
is Number -> "$key=$value"
|
||||
else -> throw IllegalArgumentException("Unsupported value type: ${value::class.java}")
|
||||
}
|
||||
}
|
@@ -96,6 +96,7 @@ import androidx.media3.exoplayer.trackselection.MappingTrackSelector;
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelection;
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelectionArray;
|
||||
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
@@ -269,6 +270,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
private String instanceId = String.valueOf(UUID.randomUUID());
|
||||
|
||||
private CmcdConfiguration.Factory cmcdConfigurationFactory;
|
||||
|
||||
public void setCmcdConfigurationFactory(CmcdConfiguration.Factory factory) {
|
||||
this.cmcdConfigurationFactory = factory;
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
if (player != null) {
|
||||
if (playerControlView != null && isPlayingAd() && controls) {
|
||||
@@ -1103,6 +1110,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
if (cmcdConfigurationFactory != null) {
|
||||
mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
|
||||
cmcdConfigurationFactory::createCmcdConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
MediaItem mediaItem = mediaItemBuilder.setStreamKeys(streamKeys).build();
|
||||
MediaSource mediaSource = mediaSourceFactory
|
||||
.setDrmSessionManagerProvider(drmProvider)
|
||||
@@ -1800,6 +1813,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
|
||||
source.getHeaders());
|
||||
|
||||
if (source.getCmcdProps() != null) {
|
||||
CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps());
|
||||
CmcdConfiguration.Factory factory = cmcdConfig.toCmcdConfigurationFactory();
|
||||
this.setCmcdConfigurationFactory(factory);
|
||||
} else {
|
||||
this.setCmcdConfigurationFactory(null);
|
||||
}
|
||||
|
||||
if (!isSourceEqual) {
|
||||
reloadSource();
|
||||
}
|
||||
|
Reference in New Issue
Block a user