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(android): Support Common Media Client Data (CMCD) #4034

Merged
merged 17 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2723bc7
feat(VideoNativeComponent.ts): add support for cmcd configuration in …
uncoolclub Jul 25, 2024
ebd6081
feat(Video.tsx): add support for CMCD configuration in Video componen…
uncoolclub Jul 25, 2024
7d3cd84
feat(CMCDProps.kt): add CMCDProps class to handle CMCD related proper…
uncoolclub Jul 25, 2024
3d49cce
feat(CMCDConfig.kt): add CMCDConfig class to handle CMCD configuratio…
uncoolclub Jul 25, 2024
2548b5a
feat(ReactExoplayerViewManager.java): add support for CMCD configurat…
uncoolclub Jul 25, 2024
e0e105c
feat(ReactExoplayerView.java): add support for setting CmcdConfigurat…
uncoolclub Jul 25, 2024
747c7aa
feat(Source.kt): add support for CMCD properties linked to the source…
uncoolclub Jul 25, 2024
741b6e5
docs(props.mdx): add documentation for configuring CMCD parameters in…
uncoolclub Jul 25, 2024
876b3aa
refactor(ReactExoplayerViewManager.java): remove unused PROP_CMCD and…
uncoolclub Jul 25, 2024
7f36c17
refactor(Video.tsx): simplify cmcd configuration logic for better rea…
uncoolclub Jul 25, 2024
1c36726
docs(props.mdx): improve props documentation for clarity and consistency
uncoolclub Jul 25, 2024
488f498
refactor(CMCDProps.kt): refactor CMCDProps class to data class for im…
uncoolclub Jul 26, 2024
df678c4
refactor(Video.tsx): refactor createCmcdHeader function to improve co…
uncoolclub Jul 29, 2024
d7f787c
fix(CMCDProps.kt): remove import statement for CmcdConfiguration
uncoolclub Jul 29, 2024
76aaa6c
feat(ReactExoplayerView.java): add support for CMCD configuration in …
uncoolclub Jul 29, 2024
9ded21b
fix(Video.tsx): merge _cmcd memo into src memo for optimization
uncoolclub Aug 5, 2024
1e9c258
Merge branch 'master' into feat/android-cmcd
uncoolclub Aug 5, 2024
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
51 changes: 51 additions & 0 deletions android/src/main/java/com/brentvatne/common/api/CMCDProps.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
10 changes: 9 additions & 1 deletion android/src/main/java/com/brentvatne/common/api/Source.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -68,7 +73,8 @@ class Source {
cropEndMs == other.cropEndMs &&
startPositionMs == other.startPositionMs &&
extension == other.extension &&
drmProps == other.drmProps
drmProps == other.drmProps &&
cmcdProps == other.cmcdProps
)
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt
Original file line number Diff line number Diff line change
@@ -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}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1097,6 +1104,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)
Expand Down Expand Up @@ -1794,6 +1807,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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;

import com.brentvatne.common.api.BufferConfig;
import com.brentvatne.common.api.BufferingStrategy;
import com.brentvatne.common.api.ControlsConfig;
import com.brentvatne.common.api.DRMProps;
import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrackList;
import com.brentvatne.common.api.Source;
Expand Down Expand Up @@ -116,6 +116,7 @@ public void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext();
Source source = Source.parse(src, context);

if (source.getUri() == null) {
videoView.clearSrc();
} else {
Expand Down
65 changes: 65 additions & 0 deletions docs/pages/component/props.mdx
uncoolclub marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -999,3 +999,68 @@ Adjust the volume.
- **1.0 (default)** - Play at full volume
- **0.0** - Mute the audio
- **Other values** - Reduce volume

### `cmcd`

<PlatformsList types={['Android']} />

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
<Video
source={{
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
cmcd: {
mode: CmcdMode.MODE_QUERY_PARAMETER,
request: {
'com-custom-key': 'custom-value'
},
session: {
sid: 'session-id'
},
object: {
br: '3000',
d: '4000'
},
status: {
rtp: '1200'
}
}
}}
// or other video props
/>
```
32 changes: 30 additions & 2 deletions src/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import type {
ImageResizeMode,
} from 'react-native';

import NativeVideoComponent from './specs/VideoNativeComponent';
import NativeVideoComponent, {
NativeCmcdConfiguration,
} from './specs/VideoNativeComponent';
import type {
OnAudioFocusChangedData,
OnAudioTracksData,
Expand Down Expand Up @@ -44,12 +46,13 @@ import {
} from './utils';
import NativeVideoManager from './specs/NativeVideoManager';
import type {VideoSaveData} from './specs/NativeVideoManager';
import {ViewType} from './types';
import {CmcdMode, ViewType} from './types';
import type {
OnLoadData,
OnTextTracksData,
OnReceiveAdEventData,
ReactVideoProps,
CmcdData,
} from './types';

export interface VideoRef {
Expand Down Expand Up @@ -176,6 +179,30 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
multiDrm: selectedDrm.multiDrm,
};

let _cmcd: NativeCmcdConfiguration | undefined;
if (Platform.OS === 'android' && source?.cmcd) {
const cmcd = source.cmcd;

if (typeof cmcd === 'boolean') {
_cmcd = cmcd ? {mode: CmcdMode.MODE_QUERY_PARAMETER} : undefined;
} else if (typeof cmcd === 'object' && !Array.isArray(cmcd)) {
const createCmcdHeader = (property?: CmcdData) =>
property ? generateHeaderForNative(property) : undefined;

_cmcd = {
mode: cmcd.mode ?? CmcdMode.MODE_QUERY_PARAMETER,
request: createCmcdHeader(cmcd.request),
session: createCmcdHeader(cmcd.session),
object: createCmcdHeader(cmcd.object),
status: createCmcdHeader(cmcd.status),
};
} else {
throw new Error(
'Invalid CMCD configuration: Expected a boolean or an object.',
);
}
}

return {
uri,
isNetwork,
Expand All @@ -190,6 +217,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
cropEnd: resolvedSource.cropEnd,
metadata: resolvedSource.metadata,
drm: _drm,
cmcd: _cmcd,
textTracksAllowChunklessPreparation:
resolvedSource.textTracksAllowChunklessPreparation,
};
Expand Down
10 changes: 10 additions & 0 deletions src/specs/VideoNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type VideoSrc = Readonly<{
cropEnd?: Float;
metadata?: VideoMetadata;
drm?: Drm;
cmcd?: NativeCmcdConfiguration; // android
textTracksAllowChunklessPreparation?: boolean; // android
}>;

Expand All @@ -61,6 +62,15 @@ type Drm = Readonly<{
multiDrm?: WithDefault<boolean, false>; // android
}>;

type CmcdMode = WithDefault<Int32, 1>;
export type NativeCmcdConfiguration = Readonly<{
mode?: CmcdMode; // default: MODE_QUERY_PARAMETER
request?: Headers;
session?: Headers;
object?: Headers;
status?: Headers;
}>;

type TextTracks = ReadonlyArray<
Readonly<{
title: string;
Expand Down
22 changes: 22 additions & 0 deletions src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type ReactVideoSourceProperties = {
cropEnd?: number;
metadata?: VideoMetadata;
drm?: Drm;
cmcd?: Cmcd; // android
textTracksAllowChunklessPreparation?: boolean;
};

Expand Down Expand Up @@ -87,6 +88,27 @@ export type Drm = Readonly<{
/* eslint-enable @typescript-eslint/no-unused-vars */
}>;

export enum CmcdMode {
MODE_REQUEST_HEADER = 0,
MODE_QUERY_PARAMETER = 1,
}
/**
* Custom key names MUST carry a hyphenated prefix to ensure that there will not be a
* namespace collision with future revisions to this specification. Clients SHOULD
* use a reverse-DNS syntax when defining their own prefix.
*
* @see https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf CTA-5004 Specification (Page 6, Section 3.1)
*/
export type CmcdData = Record<`${string}-${string}`, string | number>;
export type CmcdConfiguration = Readonly<{
mode?: CmcdMode; // default: MODE_QUERY_PARAMETER
request?: CmcdData;
session?: CmcdData;
object?: CmcdData;
status?: CmcdData;
}>;
export type Cmcd = boolean | CmcdConfiguration;

export enum BufferingStrategyType {
DEFAULT = 'Default',
DISABLE_BUFFERING = 'DisableBuffering',
Expand Down