From 11ba5ff721dae5e1ab2b2affdfdd887473582dfe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:07:26 +0000 Subject: [PATCH 01/16] feat: add CMCD player and source configuration TypeScript API Add CmcdEndpointConfiguration, CmcdPlayerConfiguration, and CmcdSourceConfiguration interfaces for CMCDv2 event mode reporting. - CmcdPlayerConfiguration: player-level config with externalSessionId, userId, and eventEndpoints - CmcdSourceConfiguration: extends player-level config with sessionId - CmcdEndpointConfiguration: endpoint with url - CmcdConfiguration: extends CmcdSourceConfiguration for backward compat - Add cmcd property to PlayerConfiguration The existing CmcdConfiguration interface for request-mode CMCD on SourceDescription is preserved unchanged. Co-Authored-By: Ruben Baetens --- src/api/config/PlayerConfiguration.ts | 11 ++++ src/api/source/cmcd/CmcdConfiguration.ts | 69 +++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/api/config/PlayerConfiguration.ts b/src/api/config/PlayerConfiguration.ts index 90e0f5706..176b51eb4 100644 --- a/src/api/config/PlayerConfiguration.ts +++ b/src/api/config/PlayerConfiguration.ts @@ -4,6 +4,7 @@ import type { MediaControlConfiguration } from '../media/MediaControlConfigurati import type { RetryConfiguration } from '../utils/RetryConfiguration'; import type { UIConfiguration } from '../ui/UIConfiguration'; import { TheoLiveConfiguration } from '../theolive/TheoLiveConfiguration'; +import type { CmcdPlayerConfiguration } from '../source/cmcd/CmcdConfiguration'; /** * Describes a player's configuration. @@ -142,6 +143,16 @@ export interface PlayerConfiguration { *
- Any user-defined overrides are still respected. */ useSystemCaptionStyle?: boolean; + + /** + * The CMCD configuration for the player. + * + * @remarks + *
- Available since v11.4.0. + *
- Configures event mode reporting for Common Media Client Data (CTA-5004). + *
- Source-level configuration can override player-level values. See {@link CmcdSourceConfiguration}. + */ + cmcd?: CmcdPlayerConfiguration; } /** diff --git a/src/api/source/cmcd/CmcdConfiguration.ts b/src/api/source/cmcd/CmcdConfiguration.ts index 6b0fa296e..9c1885b46 100644 --- a/src/api/source/cmcd/CmcdConfiguration.ts +++ b/src/api/source/cmcd/CmcdConfiguration.ts @@ -1,3 +1,70 @@ +/** + * Configuration for a CMCD endpoint. + * + * @remarks + *
- Available since v11.4.0. + * + * @category CMCD + * @public + */ +export interface CmcdEndpointConfiguration { + /** + * The URL of the CMCD endpoint. + */ + url: string; +} + +/** + * Describes the CMCD (Common Media Client Data) configuration for event mode reporting at the player level. + * + * @remarks + *
- Available since v11.4.0. + *
- This configuration is set at the player level. For source-level configuration, see {@link CmcdSourceConfiguration}. + * + * @category CMCD + * @public + */ +export interface CmcdPlayerConfiguration { + /** + * An external session ID that can be used to identify the current playback session. + */ + externalSessionId?: string; + + /** + * A user ID that can be used to identify the user. + */ + userId?: string; + + /** + * A list of CMCD endpoints to which events should be sent. + */ + eventEndpoints?: CmcdEndpointConfiguration[]; +} + +/** + * Describes the CMCD (Common Media Client Data) configuration for event mode reporting at the source level. + * + * @remarks + *
- Available since v11.4.0. + *
- This extends the player-level {@link CmcdPlayerConfiguration} by additionally allowing a session ID to be specified + * per source. Source-level values take precedence over player-level values for overlapping fields, + * except for `eventEndpoints` which are merged (both player and source endpoints receive events). + * + * @category CMCD + * @public + */ +export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { + /** + * A GUID identifying the current playback session. + * + * @remarks + * A playback session typically consists of the playback of a single media + * asset along with accompanying content such as advertisements. The maximum length is 64 characters. + * It is RECOMMENDED to conform to the UUID specification (https://tools.ietf.org/html/rfc4122). + */ + sessionId?: string; +} + /** * The configuration for transmitting information to Content Delivery Networks (CDNs) * through Common Media Client Data (CMCD) (CTA-5004) @@ -5,7 +72,7 @@ * @category Source * @public */ -export interface CmcdConfiguration { +export interface CmcdConfiguration extends CmcdSourceConfiguration { /** * The content ID parameter which should be passed as a CMCD value. If left empty, no content ID will be sent. * From 20503fba9e734c2b81ec772739d6d4762f475a3a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:08:51 +0000 Subject: [PATCH 02/16] feat(android): bridge CMCD player and source configuration - PlayerConfigAdapter: parse cmcd from player config props and pass CMCDConfiguration (externalSessionId, userId, eventEndpoints) to THEOplayerConfig.Builder.cmcd() - SourceAdapter: parse CMCDSourceConfiguration from source cmcd props (sessionId, externalSessionId, userId, eventEndpoints) and pass to SourceDescription.Builder.cmcd() - Existing request-mode CMCD (transmissionMode on TypedSource) is preserved unchanged. Co-Authored-By: Ruben Baetens --- .../com/theoplayer/PlayerConfigAdapter.kt | 26 ++++++++++++++ .../com/theoplayer/source/SourceAdapter.kt | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt b/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt index 5a1cb9dbd..94b0599c7 100644 --- a/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt +++ b/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt @@ -8,6 +8,8 @@ import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerGlobal import com.theoplayer.android.api.cast.CastStrategy import com.theoplayer.android.api.cast.CastConfiguration +import com.theoplayer.android.api.cmcd.CMCDConfiguration +import com.theoplayer.android.api.cmcd.CMCDEndpointConfiguration import com.theoplayer.android.api.pip.PipConfiguration import com.theoplayer.android.api.player.NetworkConfiguration import com.theoplayer.android.api.theolive.THEOLiveConfig @@ -45,6 +47,11 @@ private const val PROP_THEOLIVE_DISCOVERY_URL = "discoveryUrl" private const val PROP_MULTIMEDIA_TUNNELING_ENABLED = "tunnelingEnabled" private const val PROP_DEBUG_LOGS_ENABLED = "debugLogsEnabled" private const val PROP_SYSTEM_CAPTION_STYLE = "useSystemCaptionStyle" +private const val PROP_CMCD = "cmcd" +private const val PROP_CMCD_EXTERNAL_SESSION_ID = "externalSessionId" +private const val PROP_CMCD_USER_ID = "userId" +private const val PROP_CMCD_EVENT_ENDPOINTS = "eventEndpoints" +private const val PROP_CMCD_ENDPOINT_URL = "url" class PlayerConfigAdapter(private val configProps: ReadableMap?) { @@ -88,6 +95,9 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) { if (hasKey(PROP_SYSTEM_CAPTION_STYLE)) { useSystemCaptionStyle(getBoolean(PROP_SYSTEM_CAPTION_STYLE)) } + if (hasKey(PROP_CMCD)) { + cmcd(cmcdConfig()) + } } }.build() } @@ -241,6 +251,22 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) { return MediaSessionConfigAdapter.fromProps(configProps?.getMap(PROP_MEDIA_CONTROL)) } + private fun cmcdConfig(): CMCDConfiguration { + val config = configProps?.getMap(PROP_CMCD) + val endpoints = config?.getArray(PROP_CMCD_EVENT_ENDPOINTS)?.let { arr -> + (0 until arr.size()).mapNotNull { i -> + arr.getMap(i)?.getString(PROP_CMCD_ENDPOINT_URL)?.let { url -> + CMCDEndpointConfiguration(url = url) + } + } + } + return CMCDConfiguration( + externalSessionId = config?.getString(PROP_CMCD_EXTERNAL_SESSION_ID), + userId = config?.getString(PROP_CMCD_USER_ID), + eventEndpoints = endpoints + ) + } + private fun theoLiveConfig (): THEOLiveConfig { val config = configProps?.getMap(PROP_THEOLIVE_CONFIG) return THEOLiveConfig.Builder( diff --git a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt index 264a1dd3f..51fa23169 100644 --- a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +++ b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt @@ -20,6 +20,8 @@ import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.source.metadata.ChromecastMetadataImage import com.theoplayer.BuildConfig import com.theoplayer.android.api.ads.theoads.TheoAdsLayoutOverride +import com.theoplayer.android.api.cmcd.CMCDEndpointConfiguration +import com.theoplayer.android.api.cmcd.CMCDSourceConfiguration import com.theoplayer.android.api.cmcd.CMCDTransmissionMode import com.theoplayer.android.api.error.ErrorCode import com.theoplayer.android.api.source.AdIntegration @@ -94,6 +96,11 @@ private const val TYPE_MILLICAST = "millicast" private const val PROP_CMCD = "cmcd" private const val CMCD_TRANSMISSION_MODE = "transmissionMode" +private const val CMCD_SESSION_ID = "sessionId" +private const val CMCD_EXTERNAL_SESSION_ID = "externalSessionId" +private const val CMCD_USER_ID = "userId" +private const val CMCD_EVENT_ENDPOINTS = "eventEndpoints" +private const val CMCD_ENDPOINT_URL = "url" class SourceAdapter { private val gson = Gson() @@ -175,6 +182,11 @@ class SourceAdapter { if (metadataDescription != null) { builder.metadata(metadataDescription) } + if (jsonSourceObject.has(PROP_CMCD)) { + parseCmcdSourceConfiguration(jsonSourceObject.getJSONObject(PROP_CMCD))?.let { + builder.cmcd(it) + } + } return builder.build() } catch (e: JSONException) { e.printStackTrace() @@ -476,4 +488,26 @@ class SourceAdapter { return CMCDTransmissionMode.HTTP_HEADER } } + + private fun parseCmcdSourceConfiguration(cmcdJson: JSONObject): CMCDSourceConfiguration? { + val sessionId = cmcdJson.optString(CMCD_SESSION_ID, null) + val externalSessionId = cmcdJson.optString(CMCD_EXTERNAL_SESSION_ID, null) + val userId = cmcdJson.optString(CMCD_USER_ID, null) + val endpoints = cmcdJson.optJSONArray(CMCD_EVENT_ENDPOINTS)?.let { arr -> + (0 until arr.length()).mapNotNull { i -> + arr.optJSONObject(i)?.optString(CMCD_ENDPOINT_URL)?.let { url -> + CMCDEndpointConfiguration(url = url) + } + } + } + if (sessionId == null && externalSessionId == null && userId == null && endpoints == null) { + return null + } + return CMCDSourceConfiguration( + sessionId = sessionId, + externalSessionId = externalSessionId, + userId = userId, + eventEndpoints = endpoints + ) + } } From 05786ffb5b06b8fec687f36f57ea9add72e340d0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:10:32 +0000 Subject: [PATCH 03/16] feat(ios): bridge CMCD player and source configuration - Add THEOplayerRCTView+CmcdConfig.swift: parse cmcd from player config and build CMCDConfiguration (externalSessionId, userId, eventEndpoints) for THEOplayerConfigurationBuilder.cmcd - THEOplayerRCTSourceDescriptionBuilder: parse CMCDSourceConfiguration from source cmcd data (sessionId, externalSessionId, userId, eventEndpoints) and pass to SourceDescription init - Existing request-mode CMCD (typedSource.cmcd = true) is preserved. Co-Authored-By: Ruben Baetens # Conflicts: # ios/THEOplayerRCTSourceDescriptionBuilder.swift --- ...HEOplayerRCTSourceDescriptionBuilder.swift | 26 ++++++++++++- ios/THEOplayerRCTView.swift | 3 ++ ios/cmcd/THEOplayerRCTView+CmcdConfig.swift | 37 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 ios/cmcd/THEOplayerRCTView+CmcdConfig.swift diff --git a/ios/THEOplayerRCTSourceDescriptionBuilder.swift b/ios/THEOplayerRCTSourceDescriptionBuilder.swift index edd6e50f9..ea89a6c17 100644 --- a/ios/THEOplayerRCTSourceDescriptionBuilder.swift +++ b/ios/THEOplayerRCTSourceDescriptionBuilder.swift @@ -167,11 +167,13 @@ class THEOplayerRCTSourceDescriptionBuilder { } // 6. configure CMCD + var cmcdSourceConfig: CMCDSourceConfiguration? = nil let cmcd = sourceData[SD_PROP_CMCD] as? [String:Any] if cmcd != nil { typedSources.forEach { typedSource in typedSource.cmcd = true; } + cmcdSourceConfig = THEOplayerRCTSourceDescriptionBuilder.buildCmcdSourceConfiguration(cmcd!) } // 7. construct the SourceDescription @@ -179,13 +181,33 @@ class THEOplayerRCTSourceDescriptionBuilder { textTracks: textTrackDescriptions, ads: adsDescriptions, poster: poster, - metadata: metadataDescription) - + metadata: metadataDescription, + cmcdConfiguration: cmcdSourceConfig) + return (sourceDescription, metadataAndChapterTrackDescriptions) } // MARK: Private build methods + private static func buildCmcdSourceConfiguration(_ cmcdData: [String:Any]) -> CMCDSourceConfiguration? { + let sessionId = cmcdData["sessionId"] as? String + let externalSessionId = cmcdData["externalSessionId"] as? String + let userId = cmcdData["userId"] as? String + let endpoints: [CMCDEndpointConfiguration]? = (cmcdData["eventEndpoints"] as? [[String: Any]])?.compactMap { dict in + guard let url = dict["url"] as? String else { return nil } + return CMCDEndpointConfiguration(url: url) + } + if sessionId == nil && externalSessionId == nil && userId == nil && endpoints == nil { + return nil + } + return CMCDSourceConfiguration( + sessionId: sessionId, + externalSessionId: externalSessionId, + userId: userId, + eventEndpoints: endpoints + ) + } + /** Creates a THEOplayer TypedSource. This requires a source property for non SSAI strreams (either as a string or as an object contiaining a src property). For SSAI streams the TypeSource can be created from the ssai property. - returns: a THEOplayer TypedSource. In case of SSAI we support GoogleDAITypedSource with GoogleDAIVodConfiguration or GoogleDAILiveConfiguration diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index e58c275e2..d679581f8 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -48,6 +48,7 @@ public class THEOplayerRCTView: UIView { var castConfig = CastConfig() var uiConfig = UIConfig() var theoliveConfig = THEOliveConfig() + var cmcdConfig = CmcdConfig() public var bypassDropInstanceOnReactLifecycle = false // controls superView detaching behaviour @@ -232,6 +233,7 @@ public class THEOplayerRCTView: UIView { config.hlsDateRange = self.hlsDateRange config.license = self.license config.licenseUrl = self.licenseUrl + config.cmcd = self.playerCmcdConfiguration() self.player = THEOplayer(configuration: config.build()) self.initAdsIntegration() @@ -322,6 +324,7 @@ public class THEOplayerRCTView: UIView { self.parseUIConfig(configDict: configDict) self.parseMediaControlConfig(configDict: configDict) self.parseTHEOliveConfig(configDict: configDict) + self.parseCmcdConfig(configDict: configDict) if DEBUG_VIEW { PrintUtils.printLog(logText: "[NATIVE] config prop updated.") } // Given the bridged config, create the initial THEOplayer instance diff --git a/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift b/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift new file mode 100644 index 000000000..70eac5ded --- /dev/null +++ b/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift @@ -0,0 +1,37 @@ +// THEOplayerRCTView+CmcdConfig.swift + +import Foundation +import THEOplayerSDK + +struct CmcdConfig { + var externalSessionId: String? + var userId: String? + var eventEndpoints: [[String: Any]]? +} + +extension THEOplayerRCTView { + + func parseCmcdConfig(configDict: NSDictionary) { + if let cmcdDict = configDict["cmcd"] as? NSDictionary { + self.cmcdConfig.externalSessionId = cmcdDict["externalSessionId"] as? String + self.cmcdConfig.userId = cmcdDict["userId"] as? String + self.cmcdConfig.eventEndpoints = cmcdDict["eventEndpoints"] as? [[String: Any]] + } + } + + func playerCmcdConfiguration() -> CMCDConfiguration? { + let config = self.cmcdConfig + if config.externalSessionId == nil && config.userId == nil && config.eventEndpoints == nil { + return nil + } + let endpoints = config.eventEndpoints?.compactMap { dict -> CMCDEndpointConfiguration? in + guard let url = dict["url"] as? String else { return nil } + return CMCDEndpointConfiguration(url: url) + } + return CMCDConfiguration( + externalSessionId: config.externalSessionId, + userId: config.userId, + eventEndpoints: endpoints + ) + } +} From 91fe791f309737513a91386475b63a96323196b6 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 11:48:47 +0200 Subject: [PATCH 04/16] Move CMCD API files --- src/api/barrel.ts | 1 + src/api/{source => }/cmcd/CmcdConfiguration.ts | 0 src/api/{source => }/cmcd/barrel.ts | 0 src/api/config/PlayerConfiguration.ts | 2 +- src/api/source/SourceDescription.ts | 2 +- src/api/source/barrel.ts | 1 - 6 files changed, 3 insertions(+), 3 deletions(-) rename src/api/{source => }/cmcd/CmcdConfiguration.ts (100%) rename src/api/{source => }/cmcd/barrel.ts (100%) diff --git a/src/api/barrel.ts b/src/api/barrel.ts index 672b359a3..e031b4eeb 100644 --- a/src/api/barrel.ts +++ b/src/api/barrel.ts @@ -4,6 +4,7 @@ export * from './backgroundAudio/barrel'; export * from './broadcast/barrel'; export * from './cache/barrel'; export * from './cast/barrel'; +export * from './cmcd/barrel'; export * from './pip/barrel'; export * from './config/barrel'; export * from './error/barrel'; diff --git a/src/api/source/cmcd/CmcdConfiguration.ts b/src/api/cmcd/CmcdConfiguration.ts similarity index 100% rename from src/api/source/cmcd/CmcdConfiguration.ts rename to src/api/cmcd/CmcdConfiguration.ts diff --git a/src/api/source/cmcd/barrel.ts b/src/api/cmcd/barrel.ts similarity index 100% rename from src/api/source/cmcd/barrel.ts rename to src/api/cmcd/barrel.ts diff --git a/src/api/config/PlayerConfiguration.ts b/src/api/config/PlayerConfiguration.ts index 176b51eb4..1e3f3d7b1 100644 --- a/src/api/config/PlayerConfiguration.ts +++ b/src/api/config/PlayerConfiguration.ts @@ -4,7 +4,7 @@ import type { MediaControlConfiguration } from '../media/MediaControlConfigurati import type { RetryConfiguration } from '../utils/RetryConfiguration'; import type { UIConfiguration } from '../ui/UIConfiguration'; import { TheoLiveConfiguration } from '../theolive/TheoLiveConfiguration'; -import type { CmcdPlayerConfiguration } from '../source/cmcd/CmcdConfiguration'; +import type { CmcdPlayerConfiguration } from '../cmcd/CmcdConfiguration'; /** * Describes a player's configuration. diff --git a/src/api/source/SourceDescription.ts b/src/api/source/SourceDescription.ts index e303c545a..482f301b5 100644 --- a/src/api/source/SourceDescription.ts +++ b/src/api/source/SourceDescription.ts @@ -14,7 +14,7 @@ import type { AdDescription } from './ads/Ads'; import type { MetadataDescription } from './metadata/MetadataDescription'; import type { ServerSideAdInsertionConfiguration } from './ads/ssai/ServerSideAdInsertionConfiguration'; import type { AnalyticsDescription } from './analytics/AnalyticsDescription'; -import { CmcdConfiguration } from './cmcd/CmcdConfiguration'; +import { CmcdConfiguration } from '../cmcd/CmcdConfiguration'; import { SourceLatencyConfiguration } from './latency/SourceLatencyConfiguration'; /** diff --git a/src/api/source/barrel.ts b/src/api/source/barrel.ts index 1fe72454d..bcabdd48e 100644 --- a/src/api/source/barrel.ts +++ b/src/api/source/barrel.ts @@ -1,6 +1,5 @@ export * from './ads/barrel'; export * from './analytics/barrel'; -export * from './cmcd/barrel'; export * from './drm/barrel'; export * from './dash/barrel'; export * from './hls/barrel'; From 1043c1bc8221487f6d50fd5abef0a86e3525f51c Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 13:21:04 +0200 Subject: [PATCH 05/16] Combine existing and new source configuration --- src/api/cmcd/CmcdConfiguration.ts | 57 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/api/cmcd/CmcdConfiguration.ts b/src/api/cmcd/CmcdConfiguration.ts index 9c1885b46..bc26c0fda 100644 --- a/src/api/cmcd/CmcdConfiguration.ts +++ b/src/api/cmcd/CmcdConfiguration.ts @@ -22,6 +22,7 @@ export interface CmcdEndpointConfiguration { *
- This configuration is set at the player level. For source-level configuration, see {@link CmcdSourceConfiguration}. * * @category CMCD + * @category Player * @public */ export interface CmcdPlayerConfiguration { @@ -42,48 +43,42 @@ export interface CmcdPlayerConfiguration { } /** - * Describes the CMCD (Common Media Client Data) configuration for event mode reporting at the source level. + * Describes the CMCD (Common Media Client Data) configuration at the source level. * * @remarks - *
- Available since v11.4.0. - *
- This extends the player-level {@link CmcdPlayerConfiguration} by additionally allowing a session ID to be specified + *
    + *
  • Event mode reporting available since v11.4.0.
  • + *
  • + * This extends the player-level {@link CmcdPlayerConfiguration} by additionally allowing a session ID to be specified * per source. Source-level values take precedence over player-level values for overlapping fields, * except for `eventEndpoints` which are merged (both player and source endpoints receive events). + *
  • + *
* * @category CMCD - * @public - */ -export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { - /** - * A GUID identifying the current playback session. - * - * @remarks - * A playback session typically consists of the playback of a single media - * asset along with accompanying content such as advertisements. The maximum length is 64 characters. - * It is RECOMMENDED to conform to the UUID specification (https://tools.ietf.org/html/rfc4122). - */ - sessionId?: string; -} - -/** - * The configuration for transmitting information to Content Delivery Networks (CDNs) - * through Common Media Client Data (CMCD) (CTA-5004) - * * @category Source * @public */ -export interface CmcdConfiguration extends CmcdSourceConfiguration { +export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { /** * The content ID parameter which should be passed as a CMCD value. If left empty, no content ID will be sent. * * @platform web + * + * @remarks Request mode only */ contentID?: string; - /** - * The session ID parameter which should be passed as a CMCD value. If left empty, a UUIDv4 will be generated when applying the configuration. + * A GUID identifying the current playback session. If left empty, a UUIDv4 will be generated when applying the configuration. * - * @platform web + * @remarks + *
    + *
  • A playback session typically consists of the playback of a single media + * asset along with accompanying content such as advertisements. The maximum length is 64 characters. + * It is RECOMMENDED to conform to the UUID specification (https://tools.ietf.org/html/rfc4122). + *
  • + *
  • Event mode only for iOS and Android
  • + *
*/ sessionID?: string; @@ -120,6 +115,18 @@ export interface CmcdConfiguration extends CmcdSourceConfiguration { transmissionMode: CmcdTransmissionMode } +/** + * The configuration for transmitting information to Content Delivery Networks (CDNs) + * through Common Media Client Data (CMCD) (CTA-5004) + * + * @category CMCD + * @category Source + * @public + * + * @deprecated Use {@link CmcdSourceConfiguration} instead. + */ +export type CmcdConfiguration = CmcdSourceConfiguration; + /** * The CMCD transmission mode. * From 070dbc16ab9cd2af85cbc7aa29a1c50a6f7faef1 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 13:28:34 +0200 Subject: [PATCH 06/16] Limit existing properties to request mode --- src/api/cmcd/CmcdConfiguration.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/api/cmcd/CmcdConfiguration.ts b/src/api/cmcd/CmcdConfiguration.ts index bc26c0fda..7533b448e 100644 --- a/src/api/cmcd/CmcdConfiguration.ts +++ b/src/api/cmcd/CmcdConfiguration.ts @@ -87,6 +87,8 @@ export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { * When set to a truthy value, a UUIDv4 will be sent as a request id (`rid`) with every request to allow for request tracing. * * @platform web + * + * @remarks Request mode only */ sendRequestID?: boolean; @@ -95,6 +97,8 @@ export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { * to {@link CmcdTransmissionMode.JSON_OBJECT}. * * @platform web + * + * @remarks Request mode only */ jsonObjectTargetURI?: string; @@ -104,6 +108,8 @@ export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { * revisions to the specification. Clients SHOULD use a reverse-DNS syntax when defining their own prefix. * * @platform web + * + * @remarks Request mode only */ customKeys?: { [key: string]: string | number | boolean; @@ -111,6 +117,8 @@ export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { /** * The data transmission mode as defined in section 2 of the specification. + * + * @remarks Request mode only */ transmissionMode: CmcdTransmissionMode } From 366b0b7669f99df59360886263365b0b5fe79918 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 13:56:03 +0200 Subject: [PATCH 07/16] Update docs --- doc/cmcd.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/cmcd.md b/doc/cmcd.md index 00cbcd4d0..349e95bd0 100644 --- a/doc/cmcd.md +++ b/doc/cmcd.md @@ -1,8 +1,13 @@ # Getting started with CMCD on React Native Media player clients can transmit useful information to Content Delivery Networks (CDNs) with each object request. -This implementation supports Common Media Client Data (CMCD) as defined in -[CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf), published in September 2020. +This implementation is planned to fully support Common Media Client Data (CMCD) as defined in +[CTA-5004-B](https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html), published in April 2026. + +CMCD supports two modes of transmission: + +- **Request mode**: CMCD data is sent as HTTP headers or query parameters on manifest and media segment requests. +- **Event mode** (available since v11.4.0): CMCD events are POSTed to configured HTTP endpoints. ## Usage @@ -44,8 +49,7 @@ On Web, there are additional configuration options. For more details, visit the ## Remarks -- Note that CMCD is only supported on iOS 18.0+. -- Note that CMCD is only supported with the [Media3 integration](./media3.md) on Android. +- Note that CMCD request mode is only supported on iOS 18.0+. - Note that using a custom HTTP header from a web browser user-agent will trigger a preflight OPTIONS request before each unique media object request. This will lead to an increased request rate against the server. As a result, for CMCD transmissions from web browser user-agents that require CORS-preflighting per URL, the preferred mode of use is From 4224e1ecc378734e16e4aadd74d78f3465582ffc Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 14:44:19 +0200 Subject: [PATCH 08/16] Make transmission mode optional to make request mode optional --- .../java/com/theoplayer/source/SourceAdapter.kt | 5 ++++- ios/THEOplayerRCTSourceDescriptionBuilder.swift | 13 ++++++++----- src/api/cmcd/CmcdConfiguration.ts | 9 +++++++-- src/internal/adapter/THEOplayerWebAdapter.ts | 7 ++++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt index 51fa23169..36ba65aec 100644 --- a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +++ b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt @@ -476,7 +476,10 @@ class SourceAdapter { return BridgeUtils.fromJSONObjectToBridge(JSONObject(gson.toJson(typedSource))) } - private fun parseCmcdTransmissionMode(cmcdConfiguration : JSONObject) : CMCDTransmissionMode { + private fun parseCmcdTransmissionMode(cmcdConfiguration : JSONObject) : CMCDTransmissionMode? { + if (!cmcdConfiguration.has(CMCD_TRANSMISSION_MODE)) { + return null + } try { val transmissionMode = cmcdConfiguration.optInt(CMCD_TRANSMISSION_MODE) if (transmissionMode == CmcdTransmissionMode.QUERY_ARGUMENT.ordinal) { diff --git a/ios/THEOplayerRCTSourceDescriptionBuilder.swift b/ios/THEOplayerRCTSourceDescriptionBuilder.swift index ea89a6c17..169d80fc8 100644 --- a/ios/THEOplayerRCTSourceDescriptionBuilder.swift +++ b/ios/THEOplayerRCTSourceDescriptionBuilder.swift @@ -64,6 +64,7 @@ let SD_PROP_INITIALIZATION_DELAY: String = "initializationDelay" let SD_PROP_HLS_DATE_RANGE: String = "hlsDateRange" let SD_PROP_BREAK_MANIFEST_URL: String = "breakManifestUrl" let SD_PROP_CMCD: String = "cmcd" +let SD_PROP_TRANSMISSION_MODE: String = "transmissionMode" let SD_PROP_QUERY_PARAMETERS: String = "queryParameters" let EXTENSION_HLS: String = ".m3u8" @@ -168,12 +169,14 @@ class THEOplayerRCTSourceDescriptionBuilder { // 6. configure CMCD var cmcdSourceConfig: CMCDSourceConfiguration? = nil - let cmcd = sourceData[SD_PROP_CMCD] as? [String:Any] - if cmcd != nil { - typedSources.forEach { typedSource in - typedSource.cmcd = true; + if let cmcd = sourceData[SD_PROP_CMCD] as? [String:Any] { + let requestModeEnabled = cmcd[SD_PROP_TRANSMISSION_MODE] != nil + if requestModeEnabled { + typedSources.forEach { typedSource in + typedSource.cmcd = true + } } - cmcdSourceConfig = THEOplayerRCTSourceDescriptionBuilder.buildCmcdSourceConfiguration(cmcd!) + cmcdSourceConfig = THEOplayerRCTSourceDescriptionBuilder.buildCmcdSourceConfiguration(cmcd) } // 7. construct the SourceDescription diff --git a/src/api/cmcd/CmcdConfiguration.ts b/src/api/cmcd/CmcdConfiguration.ts index 7533b448e..1959b8bfc 100644 --- a/src/api/cmcd/CmcdConfiguration.ts +++ b/src/api/cmcd/CmcdConfiguration.ts @@ -118,9 +118,14 @@ export interface CmcdSourceConfiguration extends CmcdPlayerConfiguration { /** * The data transmission mode as defined in section 2 of the specification. * - * @remarks Request mode only + * @remarks + *
    + *
  • Starting from version 11.4.0, this property is now optional. By providing a value, request mode will be + * enabled as it was before. If left undefined, the SDK will not use request mode.
  • + *
  • Request mode only
  • + *
*/ - transmissionMode: CmcdTransmissionMode + transmissionMode?: CmcdTransmissionMode } /** diff --git a/src/internal/adapter/THEOplayerWebAdapter.ts b/src/internal/adapter/THEOplayerWebAdapter.ts index 1f4a1642e..484da08d6 100644 --- a/src/internal/adapter/THEOplayerWebAdapter.ts +++ b/src/internal/adapter/THEOplayerWebAdapter.ts @@ -96,10 +96,11 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher this._targetVideoQuality = undefined; if (this._player) { this._player.source = source as NativeSourceDescription; - if (source?.cmcd && this._cmcdConnector === undefined) { - this._cmcdConnector = createCMCDConnector(this._player); + const requestModeEnabled = source?.cmcd?.transmissionMode !== undefined; + if (requestModeEnabled) { + this._cmcdConnector ??= createCMCDConnector(this._player); + this._cmcdConnector?.reconfigure(this.toWebCmcdConfiguration(source?.cmcd)); } - this._cmcdConnector?.reconfigure(this.toWebCmcdConfiguration(source?.cmcd)); } } From 48341105f4acd28155df8022ef2f582a93b75ad6 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 16:18:24 +0200 Subject: [PATCH 09/16] Update documentation --- doc/cmcd.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/doc/cmcd.md b/doc/cmcd.md index 349e95bd0..38c6a77cc 100644 --- a/doc/cmcd.md +++ b/doc/cmcd.md @@ -9,9 +9,9 @@ CMCD supports two modes of transmission: - **Request mode**: CMCD data is sent as HTTP headers or query parameters on manifest and media segment requests. - **Event mode** (available since v11.4.0): CMCD events are POSTed to configured HTTP endpoints. -## Usage +## Request mode -To enable CMCD, developers can set the `cmcd` parameter inside a SourceDescription. +To enable CMCD request mode, developers need to explicitly set the `transmissionMode` parameter inside a SourceDescription. ```tsx const source = { @@ -47,10 +47,55 @@ player.source = source; On Web, there are additional configuration options. For more details, visit the API references. -## Remarks +### Remarks - Note that CMCD request mode is only supported on iOS 18.0+. - Note that using a custom HTTP header from a web browser user-agent will trigger a preflight OPTIONS request before each unique media object request. This will lead to an increased request rate against the server. As a result, for CMCD transmissions from web browser user-agents that require CORS-preflighting per URL, the preferred mode of use is query arguments. + +## Event mode + +Event mode allows posting CMCD events to configured HTTP endpoints. + +:::info +Event mode is supported starting from version 11.4.0. +::: + +### Player-level configuration + +```javascript +const player = new THEOplayer.Player(element, { + cmcd: { + externalSessionId: 'YOUR-EXTERNAL-SESSION-ID', // optional + userId: 'YOUR-USER-ID', // optional + eventEndpoints: [{ url: 'https://example.com/cmcd-event-endpoint' }], + }, +}); +``` + +### Source-level configuration + +```javascript +player.source = { + sources: [ /* ... */ ], + /* ... */ + cmcd: { + externalSessionId: 'YOUR-EXTERNAL-SESSION-ID', // optional + sessionId: 'YOUR-SESSION-ID', // optional + userId: 'YOUR-USER-ID', // optional + eventEndpoints: [{ url: 'https://example.com/cmcd-event-other-endpoint' }], + }, +}; +``` + +### Merging behavior + +- Source-level values take precedence for `externalSessionId` and `userId`. +- `eventEndpoints` from both levels are **merged** (both player and source endpoints receive events). + +:::warning +Event mode reporting currently only supports DRM and ad events. +::: + From dbea00bcd4723e1960e84737f23ba4cd96acb224 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Wed, 3 Jun 2026 16:24:15 +0200 Subject: [PATCH 10/16] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5d38e44..a80467a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added basic support for CMCD event mode reporting of DRM and ad events. + ### Fixed - Fixed an issue on Android where `GoogleImaConfiguration.sessionId` was not properly passed. From 83672c78d56ae29c4838379b398bd3ce15c79089 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 13:30:54 +0200 Subject: [PATCH 11/16] Include cmcd specifc iOS files --- react-native-theoplayer.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-native-theoplayer.podspec b/react-native-theoplayer.podspec index 4ae211870..0b6d781f4 100644 --- a/react-native-theoplayer.podspec +++ b/react-native-theoplayer.podspec @@ -35,7 +35,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "15.0", :tvos => "15.0" } s.source = { :git => "https://www.theoplayer.com/.git", :tag => "#{s.version}" } - s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift', 'ios/sideloadedMetadata/*.swift', 'ios/eventBroadcasting/*.swift' , 'ios/ui/*.swift', 'ios/presentationMode/*.swift', 'ios/viewController/*.swift', 'ios/THEOlive/*.swift', 'ios/THEOads/*.swift', 'ios/millicast/*.swift' + s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift', 'ios/sideloadedMetadata/*.swift', 'ios/eventBroadcasting/*.swift' , 'ios/ui/*.swift', 'ios/presentationMode/*.swift', 'ios/viewController/*.swift', 'ios/THEOlive/*.swift', 'ios/THEOads/*.swift', 'ios/millicast/*.swift', 'ios/cmcd/*.swift' s.resources = ['ios/*.css'] # ReactNative Dependency From 8230707e14adbd97fd3a25d1f5bc406413034c5d Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 13:34:26 +0200 Subject: [PATCH 12/16] Use CmcdSourceConfiguration --- src/api/source/SourceDescription.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/source/SourceDescription.ts b/src/api/source/SourceDescription.ts index 482f301b5..475d33aaf 100644 --- a/src/api/source/SourceDescription.ts +++ b/src/api/source/SourceDescription.ts @@ -14,7 +14,7 @@ import type { AdDescription } from './ads/Ads'; import type { MetadataDescription } from './metadata/MetadataDescription'; import type { ServerSideAdInsertionConfiguration } from './ads/ssai/ServerSideAdInsertionConfiguration'; import type { AnalyticsDescription } from './analytics/AnalyticsDescription'; -import { CmcdConfiguration } from '../cmcd/CmcdConfiguration'; +import { CmcdSourceConfiguration } from '../cmcd/CmcdConfiguration'; import { SourceLatencyConfiguration } from './latency/SourceLatencyConfiguration'; /** @@ -109,7 +109,7 @@ export interface SourceConfiguration { * The configuration for transmitting information to Content Delivery Networks (CDNs) * through Common Media Client Data (CMCD) (CTA-5004) */ - cmcd?: CmcdConfiguration; + cmcd?: CmcdSourceConfiguration; } /** From 9adfa8a638cc2b9cb2eb0f30875733ae5b89de4e Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 15:22:26 +0200 Subject: [PATCH 13/16] Map existing sessionID to native sessionId --- android/src/main/java/com/theoplayer/source/SourceAdapter.kt | 2 +- ios/THEOplayerRCTSourceDescriptionBuilder.swift | 2 +- src/internal/adapter/THEOplayerWebAdapter.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt index 36ba65aec..303c02564 100644 --- a/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +++ b/android/src/main/java/com/theoplayer/source/SourceAdapter.kt @@ -96,7 +96,7 @@ private const val TYPE_MILLICAST = "millicast" private const val PROP_CMCD = "cmcd" private const val CMCD_TRANSMISSION_MODE = "transmissionMode" -private const val CMCD_SESSION_ID = "sessionId" +private const val CMCD_SESSION_ID = "sessionID" private const val CMCD_EXTERNAL_SESSION_ID = "externalSessionId" private const val CMCD_USER_ID = "userId" private const val CMCD_EVENT_ENDPOINTS = "eventEndpoints" diff --git a/ios/THEOplayerRCTSourceDescriptionBuilder.swift b/ios/THEOplayerRCTSourceDescriptionBuilder.swift index 169d80fc8..c2ff21853 100644 --- a/ios/THEOplayerRCTSourceDescriptionBuilder.swift +++ b/ios/THEOplayerRCTSourceDescriptionBuilder.swift @@ -193,7 +193,7 @@ class THEOplayerRCTSourceDescriptionBuilder { // MARK: Private build methods private static func buildCmcdSourceConfiguration(_ cmcdData: [String:Any]) -> CMCDSourceConfiguration? { - let sessionId = cmcdData["sessionId"] as? String + let sessionId = cmcdData["sessionID"] as? String let externalSessionId = cmcdData["externalSessionId"] as? String let userId = cmcdData["userId"] as? String let endpoints: [CMCDEndpointConfiguration]? = (cmcdData["eventEndpoints"] as? [[String: Any]])?.compactMap { dict in diff --git a/src/internal/adapter/THEOplayerWebAdapter.ts b/src/internal/adapter/THEOplayerWebAdapter.ts index 484da08d6..cdcade965 100644 --- a/src/internal/adapter/THEOplayerWebAdapter.ts +++ b/src/internal/adapter/THEOplayerWebAdapter.ts @@ -95,7 +95,9 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher set source(source: SourceDescription | undefined) { this._targetVideoQuality = undefined; if (this._player) { - this._player.source = source as NativeSourceDescription; + this._player.source = ( + source?.cmcd ? { ...source, cmcd: { ...source.cmcd, sessionId: source.cmcd.sessionID } } : source + ) as NativeSourceDescription; const requestModeEnabled = source?.cmcd?.transmissionMode !== undefined; if (requestModeEnabled) { this._cmcdConnector ??= createCMCDConnector(this._player); From 6466fbfde6b08698ded25cc7503b3e08daf47bf8 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 15:24:27 +0200 Subject: [PATCH 14/16] Fix doc example --- doc/cmcd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/cmcd.md b/doc/cmcd.md index 38c6a77cc..af9de2484 100644 --- a/doc/cmcd.md +++ b/doc/cmcd.md @@ -83,7 +83,7 @@ player.source = { /* ... */ cmcd: { externalSessionId: 'YOUR-EXTERNAL-SESSION-ID', // optional - sessionId: 'YOUR-SESSION-ID', // optional + sessionID: 'YOUR-SESSION-ID', // optional userId: 'YOUR-USER-ID', // optional eventEndpoints: [{ url: 'https://example.com/cmcd-event-other-endpoint' }], }, From bddc729136a3b31b8cee1eb128348ddbb3027068 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 15:33:59 +0200 Subject: [PATCH 15/16] Explicitly disable the web cmcd connector if request mode is disabled --- src/internal/adapter/THEOplayerWebAdapter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/internal/adapter/THEOplayerWebAdapter.ts b/src/internal/adapter/THEOplayerWebAdapter.ts index cdcade965..40bde64aa 100644 --- a/src/internal/adapter/THEOplayerWebAdapter.ts +++ b/src/internal/adapter/THEOplayerWebAdapter.ts @@ -102,6 +102,8 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher if (requestModeEnabled) { this._cmcdConnector ??= createCMCDConnector(this._player); this._cmcdConnector?.reconfigure(this.toWebCmcdConfiguration(source?.cmcd)); + } else { + this._cmcdConnector?.reconfigure(undefined); } } } From a68edbd67919c2a8d8f2e021c5193638af0f2653 Mon Sep 17 00:00:00 2001 From: rbnbtns Date: Fri, 12 Jun 2026 15:40:13 +0200 Subject: [PATCH 16/16] Better struct typing --- ios/cmcd/THEOplayerRCTView+CmcdConfig.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift b/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift index 70eac5ded..22891fe72 100644 --- a/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift +++ b/ios/cmcd/THEOplayerRCTView+CmcdConfig.swift @@ -6,7 +6,7 @@ import THEOplayerSDK struct CmcdConfig { var externalSessionId: String? var userId: String? - var eventEndpoints: [[String: Any]]? + var eventEndpoints: [CMCDEndpointConfiguration]? } extension THEOplayerRCTView { @@ -15,7 +15,10 @@ extension THEOplayerRCTView { if let cmcdDict = configDict["cmcd"] as? NSDictionary { self.cmcdConfig.externalSessionId = cmcdDict["externalSessionId"] as? String self.cmcdConfig.userId = cmcdDict["userId"] as? String - self.cmcdConfig.eventEndpoints = cmcdDict["eventEndpoints"] as? [[String: Any]] + self.cmcdConfig.eventEndpoints = (cmcdDict["eventEndpoints"] as? [[String: Any]])?.compactMap { dict -> CMCDEndpointConfiguration? in + guard let url = dict["url"] as? String else { return nil } + return CMCDEndpointConfiguration(url: url) + } } } @@ -24,14 +27,10 @@ extension THEOplayerRCTView { if config.externalSessionId == nil && config.userId == nil && config.eventEndpoints == nil { return nil } - let endpoints = config.eventEndpoints?.compactMap { dict -> CMCDEndpointConfiguration? in - guard let url = dict["url"] as? String else { return nil } - return CMCDEndpointConfiguration(url: url) - } return CMCDConfiguration( externalSessionId: config.externalSessionId, userId: config.userId, - eventEndpoints: endpoints + eventEndpoints: config.eventEndpoints ) } }