From ca795f298a99a183b81561ef7e09d8d1e8addaf5 Mon Sep 17 00:00:00 2001 From: Subin Yang <49024996+uncoolclub@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:47:51 +0900 Subject: [PATCH] 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 --- .../com/brentvatne/common/api/CMCDProps.kt | 51 +++++++++++++++ .../java/com/brentvatne/common/api/Source.kt | 10 ++- .../com/brentvatne/exoplayer/CMCDConfig.kt | 41 ++++++++++++ .../exoplayer/ReactExoplayerView.java | 21 ++++++ docs/pages/component/props.mdx | 65 +++++++++++++++++++ src/Video.tsx | 32 ++++++++- src/specs/VideoNativeComponent.ts | 10 +++ src/types/video.ts | 22 +++++++ 8 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/CMCDProps.kt create mode 100644 android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt diff --git a/android/src/main/java/com/brentvatne/common/api/CMCDProps.kt b/android/src/main/java/com/brentvatne/common/api/CMCDProps.kt new file mode 100644 index 0000000000..76bcefa1cd --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/CMCDProps.kt @@ -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> = emptyList(), + val cmcdRequest: List> = emptyList(), + val cmcdSession: List> = emptyList(), + val cmcdStatus: List> = 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> { + 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 + } + } + } +} diff --git a/android/src/main/java/com/brentvatne/common/api/Source.kt b/android/src/main/java/com/brentvatne/common/api/Source.kt index ba4f849605..b9ea8bf762 100644 --- a/android/src/main/java/com/brentvatne/common/api/Source.kt +++ b/android/src/main/java/com/brentvatne/common/api/Source.kt @@ -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) diff --git a/android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt b/android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt new file mode 100644 index 0000000000..60d9d59826 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt @@ -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 = buildCustomData() + }, + props.mode + ) + + private fun buildCustomData(): ImmutableListMultimap = + ImmutableListMultimap.builder().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, key: String, dataList: List>) { + 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}") + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index a3cee5e556..bb5279032d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -96,6 +96,7 @@ 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi } } + if (cmcdConfigurationFactory != null) { + mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory( + cmcdConfigurationFactory::createCmcdConfiguration + ); + } + MediaItem mediaItem = mediaItemBuilder.setStreamKeys(streamKeys).build(); MediaSource mediaSource = mediaSourceFactory .setDrmSessionManagerProvider(drmProvider) @@ -1800,6 +1813,14 @@ public void setSrc(Source source) { 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(); } diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index 30cc4b7b50..e32865969e 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -1000,3 +1000,68 @@ Adjust the volume. - **1.0 (default)** - Play at full volume - **0.0** - Mute the audio - **Other values** - Reduce volume + +### `cmcd` + + + +Configure CMCD (Common Media Client Data) parameters. CMCD is a standard for conveying client-side metrics and capabilities to servers, which can help improve streaming quality and performance. + +For detailed information about CMCD, please refer to the [CTA-5004 Final Specification](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf). + +- **false (default)** - Don't use CMCD +- **true** - Use default CMCD configuration +- **object** - Use custom CMCD configuration + +When providing an object, you can configure the following properties: + +| Property | Type | Description | +|----------|-------------------------|----------------------------------------------------| +| `mode` | `CmcdMode` | The mode for sending CMCD data | +| `request` | `CmcdData` | Custom key-value pairs for the request object | +| `session` | `CmcdData` | Custom key-value pairs for the session object | +| `object` | `CmcdData` | Custom key-value pairs for the object metadata | +| `status` | `CmcdData` | Custom key-value pairs for the status information | + +Note: The `mode` property defaults to `CmcdMode.MODE_QUERY_PARAMETER` if not specified. + +#### `CmcdMode` +CmcdMode is an enum that defines how CMCD data should be sent: +- `CmcdMode.MODE_REQUEST_HEADER` (0) - Send CMCD data in the HTTP request headers. +- `CmcdMode.MODE_QUERY_PARAMETER` (1) - Send CMCD data as query parameters in the URL. + +#### `CmcdData` +CmcdData is a type representing custom key-value pairs for CMCD data. It's defined as: + +```typescript +type CmcdData = Record<`${string}-${string}`, string | number>; +``` + +Custom key names MUST include a hyphenated prefix to prevent namespace collisions. It's recommended to use a reverse-DNS syntax for custom prefixes. + +Example: + +```javascript +