diff --git a/.changeset/gold-fans-visit.md b/.changeset/gold-fans-visit.md new file mode 100644 index 00000000..c3524e9b --- /dev/null +++ b/.changeset/gold-fans-visit.md @@ -0,0 +1,5 @@ +--- +'@theoplayer/react-native-analytics-adobe-edge': major +--- + +Updated the connector to use the latest Adobe Experience Platform Mobile and Web SDKs. diff --git a/adobe-edge/README.md b/adobe-edge/README.md index f3ddd848..7fd2752b 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -15,13 +15,8 @@ To set up terminology, in chronological order, media tracking solutions were: ## Installation -The `@theoplayer/react-native` package has a peer dependency on `react-native-device-info`, which has to be installed as -well: - ```sh -npm install \ - react-native-device-info \ - @theoplayer/react-native-analytics-adobe-edge +npm install @theoplayer/react-native-analytics-adobe-edge ``` [//]: # (npm install @theoplayer/react-native-analytics-adobe) @@ -30,20 +25,41 @@ npm install \ ### Configuring the connector -Create the connector by providing the `THEOplayer` instance, the Media Collection API's end point, -Visitor Experience Cloud Org ID, Analytics Report Suite ID and the Analytics Tracking Server URL. +Create the connector by providing the `THEOplayer` instance and a configuration object with separate parts for +Web and mobile platforms. ```tsx -import { useAdobe } from '@theoplayer/react-native-analytics-adobe-edge'; +import {useAdobe} from '@theoplayer/react-native-analytics-adobe-edge'; + +const config = { + web: { + datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', + orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', + edgeBasePath: 'ee', + edgeDomain: 'my.domain.com', + debugEnabled: true, + }, + mobile: { + environmentId: 'abcdef123456/abcdef123456/launch-1234567890abcdef1234567890abcdef12', + debugEnabled: true, + }, +} -const baseUrl = "https://edge.adobedc.net/ee-pre-prd/va/v1"; -const dataStreamId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; -const userAgent = ""; // Optionally provide a custom user-agent header value. -const debugSessionID = ""; // Optionally provide a query parameter to be added to outgoing requests. -const useNative = true; // Use a native connector on iOS & Android; `false` by default. +/** + * An optional custom identity map to associate the media session with user identities. + */ +const customIdentityMap = { + EMAIL: [ + { + id: 'user@example.com', + authenticatedState: 'authenticated', + primary: false, + }, + ], +} const App = () => { - const [adobe, initAdobe] = useAdobe(baseUrl, dataStreamId, userAgent, true, debugSessionID, useNative); + const [adobe, initAdobe] = useAdobe(config, /* optional */ customIdentityMap); const onPlayerReady = (player: THEOplayer) => { // Initialize Adobe connector @@ -62,7 +78,7 @@ such as duration or whether it is a live or vod. The connector allows passing or updating the current asset's metadata at any time: ```typescript -import { AdobeCustomMetadataDetails } from "@theoplayer/react-native-analytics-adobe-edge"; +import {AdobeCustomMetadataDetails} from "@theoplayer/react-native-analytics-adobe-edge"; const onUpdateMetadata = () => { const metadata: AdobeCustomMetadataDetails[] = [ @@ -72,3 +88,39 @@ const onUpdateMetadata = () => { adobe.current?.updateMetadata(metadata); }; ``` + +### Setting an custom identity map + +Besides passing a custom identity map during initialization, you can also set or update the identity map at any time: + +```typescript +import {AdobeIdentityMap} from "@theoplayer/react-native-analytics-adobe-edge"; + +const onUpdateIdentityMap = () => { + const identityMap: AdobeIdentityMap = { + CUSTOMER_ID: [ + { + id: 'customer-12345', + authenticatedState: 'authenticated', + primary: true, + }, + ], + }; + adobe.current?.setIdentityMap(identityMap); +}; +``` + +### Starting a new session during a live stream + +By default, the connector will start a new session when a new asset is loaded. However, during live streams, you might +want to start a new session +periodically when a new program starts. You can do this by calling `stopAndStartNewSession` with the new program's +metadata: + +```typescript +const onNewProgram = () => { + adobe.current?.stopAndStartNewSession({ + 'friendlyName': 'Evening News', + }); +}; +``` diff --git a/adobe-edge/android/build.gradle b/adobe-edge/android/build.gradle index 37d0e3d3..9247a85c 100644 --- a/adobe-edge/android/build.gradle +++ b/adobe-edge/android/build.gradle @@ -80,6 +80,18 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.5.1' implementation("com.google.code.gson:gson:$gsonVersion") + implementation(platform("com.adobe.marketing.mobile:sdk-bom:3.+")) + implementation("com.adobe.marketing.mobile:core") + implementation("com.adobe.marketing.mobile:edge") + + /** + * The Identity framework lets your app use Experience Cloud ID (ECID). Using ECIDs improves + * synchronization with Adobe and other customer identifiers. + * https://developer.adobe.com/client-sdks/home/base/mobile-core/identity/ + */ + implementation("com.adobe.marketing.mobile:edgeidentity") + implementation("com.adobe.marketing.mobile:edgemedia") + // THEOplayer dependencies compileOnly "com.theoplayer.theoplayer-sdk-android:core:$theoplayer_sdk_version" compileOnly "com.theoplayer.theoplayer-sdk-android:integration-ads-ima:$theoplayer_sdk_version" diff --git a/adobe-edge/android/src/main/AndroidManifest.xml b/adobe-edge/android/src/main/AndroidManifest.xml index cc947c56..99e3702d 100644 --- a/adobe-edge/android/src/main/AndroidManifest.xml +++ b/adobe-edge/android/src/main/AndroidManifest.xml @@ -1 +1,4 @@ - + + + + diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt deleted file mode 100644 index 0be08a3c..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge - -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails -import com.theoplayer.reactnative.adobe.edge.api.ErrorSource - -fun ReadableMap.toAdobeErrorDetails(): AdobeErrorDetails { - return AdobeErrorDetails( - name = this.getString("name") ?: "NA", - source = when (this.getString("source")) { - "player" -> ErrorSource.PLAYER - else -> ErrorSource.EXTERNAL - } - ) -} - -fun ReadableMap.toAdobeCustomMetadataDetails() : AdobeCustomMetadataDetails { - return AdobeCustomMetadataDetails( - name = getString("name"), - value = getString("value") - ) -} - -fun ReadableArray.toAdobeCustomMetadataDetails() : List { - return mutableListOf().apply { - toArrayList() - .map { e -> (e as? ReadableMap)?.toAdobeCustomMetadataDetails() } - .filter { e -> e != null } - } -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index 20ad99bc..1e439bb2 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -1,429 +1,38 @@ -@file:Suppress("unused") - package com.theoplayer.reactnative.adobe.edge -import com.theoplayer.android.api.event.EventListener -import com.theoplayer.android.api.event.ads.AdBeginEvent -import com.theoplayer.android.api.event.ads.AdBreakBeginEvent -import com.theoplayer.android.api.event.ads.AdBreakEndEvent -import com.theoplayer.android.api.event.ads.AdEndEvent -import com.theoplayer.android.api.event.ads.AdSkipEvent -import com.theoplayer.android.api.event.ads.AdsEventTypes -import com.theoplayer.android.api.event.player.EndedEvent -import com.theoplayer.android.api.event.player.ErrorEvent -import com.theoplayer.android.api.event.player.PauseEvent -import com.theoplayer.android.api.event.player.PlayerEventTypes -import com.theoplayer.android.api.event.player.PlayingEvent -import com.theoplayer.android.api.event.player.SourceChangeEvent -import com.theoplayer.android.api.event.player.WaitingEvent -import com.theoplayer.android.api.event.track.mediatrack.video.ActiveQualityChangedEvent -import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes -import com.theoplayer.android.api.event.track.mediatrack.video.list.VideoTrackListEventTypes -import com.theoplayer.android.api.event.track.texttrack.EnterCueEvent -import com.theoplayer.android.api.event.track.texttrack.ExitCueEvent -import com.theoplayer.android.api.event.track.texttrack.TextTrackEventTypes -import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventTypes +import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.edge.identity.IdentityMap import com.theoplayer.android.api.player.Player -import com.theoplayer.android.api.player.track.texttrack.TextTrackKind -import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeQoeDataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeSessionDetails -import com.theoplayer.reactnative.adobe.edge.api.ContentType -import com.theoplayer.reactnative.adobe.edge.api.ErrorSource -import com.theoplayer.reactnative.adobe.edge.api.MediaEdgeAPI -import com.theoplayer.reactnative.adobe.edge.api.buildUserAgent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient - -typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent -typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent -typealias AddVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.AddTrackEvent -typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent - -private const val TAG = "AdobeEdgeConnector" -private const val CONTENT_PING_INTERVAL = 10000L -private const val AD_PING_INTERVAL = 1000L -private val JSON_MEDIA_TYPE = "application/json".toMediaType() class AdobeEdgeConnector( - private val player: Player, - baseUrl: String, - configId: String, - userAgent: String?, - debug: Boolean? = false, - debugSessionId: String? = null + player: Player, + trackerConfig: Map, + customIdentityMap: IdentityMap? = null, ) { - private var pingJob: Job? = null - - private var sessionInProgress = false - - private var adBreakPodIndex = 0 - - private var adPodPosition = 1 - - private var isPlayingAd = false - - private var customMetadata: MutableList = mutableListOf() - - private var currentChapter: TextTrackCue? = null - - private var customUserAgent: String? = null - - private val scope = CoroutineScope(Dispatchers.Main) - - private val client = OkHttpClient() - - private val onPlaying: EventListener = EventListener { handlePlaying() } - private val onPause: EventListener = EventListener { handlePause() } - private val onEnded: EventListener = EventListener { handleEnded() } - private val onWaiting: EventListener = EventListener { handleWaiting() } - private val onSourceChange: EventListener = - EventListener { handleSourceChange() } - private val onAddTextTrack: EventListener = - EventListener { handleAddTextTrack(it) } - private val onRemoveTextTrack: EventListener = - EventListener { handleRemoveTextTrack(it) } - private val onAddVideoTrack: EventListener = - EventListener { handleAddVideoTrack(it) } - private val onRemoveVideoTrack: EventListener = - EventListener { handleRemoveVideoTrack(it) } - private val onActiveVideoQualityChanged: EventListener = - EventListener { handleQualityChanged(it) } - private val onEnterCue: EventListener = EventListener { handleEnterCue(it) } - private val onExitCue: EventListener = EventListener { handleExitCue(it) } - private val onError: EventListener = EventListener { handleError(it) } - private val onAdBreakBegin: EventListener = - EventListener { event -> handleAdBreakBegin(event) } - private val onAdBreakEnd: EventListener = - EventListener { event -> handleAdBreakEnd() } - private val onAdBegin: EventListener = - EventListener { event -> handleAdBegin(event) } - private val onAdEnd: EventListener = EventListener { handleAdEnd(it) } - private val onAdSkip: EventListener = EventListener { event -> handleAdSkip() } - - private val mediaApi: MediaEdgeAPI = - MediaEdgeAPI(baseUrl, configId, userAgent ?: buildUserAgent(), debugSessionId) - - init { - setDebug(debug ?: false) - addEventListeners() - Logger.debug("Initialized connector") - } - - fun setDebug(debug: Boolean) { - Logger.debug = debug - } - - fun setDebugSessionId(id: String?) { - mediaApi.setDebugSessionId(id) - } - - fun updateMetadata(metadata: List) { - customMetadata.addAll(metadata) - } - - fun setError(errorDetails: AdobeErrorDetails) { - mediaApi.error(player.currentTime, errorDetails) - } - - fun stopAndStartNewSession(metadata: List?) { - scope.launch { - maybeEndSession() - metadata?.let { - updateMetadata(it) - } - maybeStartSession() - if (player.isPaused) { - handlePause() - } else { - handlePlaying() - } - } - } - - private fun addEventListeners() { - player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.addEventListener(PlayerEventTypes.PAUSE, onPause) - player.addEventListener(PlayerEventTypes.ENDED, onEnded) - player.addEventListener(PlayerEventTypes.WAITING, onWaiting) - player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) - player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) - player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack) - player.videoTracks.addEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) - player.addEventListener(PlayerEventTypes.ERROR, onError) - player.ads.apply { - addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) - addEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) - addEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) - addEventListener(AdsEventTypes.AD_END, onAdEnd) - addEventListener(AdsEventTypes.AD_SKIP, onAdSkip) - } - } - - private fun removeEventListeners() { - player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.removeEventListener(PlayerEventTypes.PAUSE, onPause) - player.removeEventListener(PlayerEventTypes.ENDED, onEnded) - player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) - player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) - player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) - player.textTracks.removeEventListener( - TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack - ) - player.videoTracks.removeEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) - player.removeEventListener(PlayerEventTypes.ERROR, onError) - player.ads.apply { - removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) - removeEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) - removeEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) - removeEventListener(AdsEventTypes.AD_END, onAdEnd) - removeEventListener(AdsEventTypes.AD_SKIP, onAdSkip) - } - } - - private fun handlePlaying() { - // NOTE: In case of a pre-roll ad, the `playing` event will be sent twice: once starting the re-roll, and once - // starting content. During the pre-roll, all events will be queued. The session will be started after the pre-roll, - // making sure we can start the session with the correct content duration (not the ad duration). - Logger.debug("onPlaying") - scope.launch { - maybeStartSession(player.duration) - mediaApi.play(player.currentTime) - } - } - - private fun handlePause() { - Logger.debug("onPause") - mediaApi.pause(player.currentTime) - } - - private fun handleWaiting() { - Logger.debug("onWaiting") - mediaApi.bufferStart(player.currentTime) - } - - private fun handleEnded() { - Logger.debug("onEnded") - mediaApi.sessionComplete(player.currentTime) - reset() - } - - private fun handleSourceChange() { - Logger.debug("onSourceChange") - maybeEndSession() - } - - private fun handleQualityChanged(event: ActiveQualityChangedEvent) { - mediaApi.bitrateChange( - player.currentTime, AdobeQoeDataDetails( - bitrate = event.quality?.bandwidth?.toInt() ?: 0, - ) - ) - } - - private fun handleAddTextTrack(event: AddTextTrackEvent) { - event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> - Logger.debug("onAddTextTrack - add chapter track ${track.uid}") - track.addEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) - track.addEventListener(TextTrackEventTypes.EXITCUE, onExitCue) - } - } - - private fun handleRemoveTextTrack(event: RemoveTextTrackEvent) { - event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> - Logger.debug("onRemoveTextTrack - remove chapter track ${track.uid}") - track.removeEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) - track.removeEventListener(TextTrackEventTypes.EXITCUE, onExitCue) - } - } - - private fun handleAddVideoTrack(event: AddVideoTrackEvent) { - Logger.debug("onAddVideoTrack") - event.track.addEventListener( - VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged - ) - } - - private fun handleRemoveVideoTrack(event: RemoveVideoTrackEvent) { - Logger.debug("onRemoveVideoTrack") - event.track.removeEventListener( - VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged - ) - } - - private fun handleEnterCue(event: EnterCueEvent) { - Logger.debug("onEnterCue") - val chapterCue = event.cue - if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { - mediaApi.chapterSkip(this.player.currentTime) - } - val chapterDetails = calculateChapterDetails(chapterCue) - mediaApi.chapterStart(this.player.currentTime, chapterDetails, customMetadata) - currentChapter = chapterCue - } - - private fun handleExitCue(event: ExitCueEvent) { - Logger.debug("onExitCue") - mediaApi.chapterComplete(player.currentTime) - } - - private fun handleError(event: ErrorEvent) { - Logger.debug("onError") - mediaApi.error( - player.currentTime, AdobeErrorDetails( - name = event.errorObject.code.toString(), source = ErrorSource.PLAYER - ) - ) - } - - private fun handleAdBreakBegin(event: AdBreakBeginEvent) { - Logger.debug("onAdBreakBegin") - isPlayingAd = true - startPinger(AD_PING_INTERVAL) - val podDetails = calculateAdvertisingPodDetails(event.adBreak, adBreakPodIndex) - mediaApi.adBreakStart(player.currentTime, podDetails) - if (podDetails.index > adBreakPodIndex) { - adBreakPodIndex++ - } - } - - private fun handleAdBreakEnd() { - Logger.debug("onAdBreakEnd") - isPlayingAd = false - adPodPosition = 1 - startPinger(CONTENT_PING_INTERVAL) - mediaApi.adBreakComplete(player.currentTime) - } - - private fun handleAdBegin(event: AdBeginEvent) { - Logger.debug("onAdBegin") - mediaApi.adStart( - player.currentTime, calculateAdvertisingDetails(event.ad, adPodPosition), customMetadata - ) - adPodPosition++ - } - - private fun handleAdEnd(event: AdEndEvent) { - Logger.debug("onAdEnd") - mediaApi.adComplete(player.currentTime) - } - - private fun handleAdSkip() { - Logger.debug("onAdSkip") - mediaApi.adSkip(player.currentTime) - } - - private fun maybeEndSession() { - Logger.debug("maybeEndSession") - if (mediaApi.hasSessionStarted()) { - mediaApi.sessionEnd(player.currentTime) - } - reset() - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLengthSec - * @private - */ - private suspend fun maybeStartSession(mediaLengthSec: Double? = null) { - val mediaLength = getContentLength(mediaLengthSec) - val hasValidSource = player.source !== null - val hasValidDuration = isValidDuration(mediaLengthSec) - - Logger.debug( - "maybeStartSession - " + "mediaLength: $mediaLength, " + "hasValidSource: $hasValidSource, " + "hasValidDuration: $hasValidDuration, " + "isPlayingAd: ${player.ads.isPlaying}" - ) - - if (sessionInProgress) { - Logger.debug("maybeStartSession - NOT started: already in progress") - return - } - - if (isPlayingAd) { - Logger.debug("maybeStartSession - NOT started: playing ad") - return - } - - if (!hasValidSource || !hasValidDuration) { - Logger.debug("maybeStartSession - NOT started: invalid ${if (hasValidSource) "duration" else "source"}") - return - } - - mediaApi.startSession( - AdobeSessionDetails( - ID = "N/A", - name = player.source?.metadata?.get("title") ?: "N/A", - channel = "N/A", - contentType = getContentType(), - playerName = "THEOplayer", - length = mediaLength - ), this.customMetadata - ) - - if (!mediaApi.hasSessionStarted()) { - Logger.debug("maybeStartSession - session was not started") - return - } - - sessionInProgress = true - Logger.debug("maybeStartSession - STARTED sessionId: ${mediaApi.sessionId}") + private val handler = AdobeEdgeHandler(player, trackerConfig, customIdentityMap) - if (!isPlayingAd) { - startPinger(CONTENT_PING_INTERVAL) - } else { - startPinger(AD_PING_INTERVAL) - } + fun updateMetadata(metadata: HashMap) { + handler.updateMetadata(metadata) } - private fun startPinger(intervalMs: Long) { - pingJob?.cancel() - pingJob = scope.launch { - while (isActive) { - mediaApi.ping(player.currentTime) - delay(intervalMs) - } - } + fun setCustomIdentityMap(customIdentityMap: IdentityMap) { + handler.setCustomIdentityMap(customIdentityMap) } - private fun getContentLength(mediaLengthSec: Double?): Int { - return sanitiseContentLength(mediaLengthSec) + fun stopAndStartNewSession(metadata: Map?) { + handler.stopAndStartNewSession(metadata) } - private fun getContentType(): ContentType { - return if (player.duration == Double.POSITIVE_INFINITY) ContentType.LIVE else ContentType.VOD + fun setLoggingMode(loggingMode: LoggingMode) { + handler.setLoggingMode(loggingMode) } - fun reset() { - Logger.debug("reset") - mediaApi.reset() - adBreakPodIndex = 0 - adPodPosition = 1 - isPlayingAd = false - sessionInProgress = false - pingJob?.cancel() - currentChapter = null + fun setError(errorId: String) { + handler.setError(errorId) } fun destroy() { - Logger.debug("destroy") - scope.launch { - maybeEndSession() - removeEventListeners() - } + handler.destroy() } } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt new file mode 100644 index 00000000..dc4b72dd --- /dev/null +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -0,0 +1,545 @@ +package com.theoplayer.reactnative.adobe.edge + +import android.util.Log +import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.MobileCore +import com.adobe.marketing.mobile.edge.identity.Identity +import com.adobe.marketing.mobile.edge.identity.IdentityMap +import com.adobe.marketing.mobile.edge.media.Media +import com.adobe.marketing.mobile.edge.media.Media.Event +import com.adobe.marketing.mobile.edge.media.MediaConstants +import com.theoplayer.android.api.ads.LinearAd +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.ads.AdBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakEndEvent +import com.theoplayer.android.api.event.ads.AdEndEvent +import com.theoplayer.android.api.event.ads.AdSkipEvent +import com.theoplayer.android.api.event.ads.AdsEventTypes +import com.theoplayer.android.api.event.player.EndedEvent +import com.theoplayer.android.api.event.player.ErrorEvent +import com.theoplayer.android.api.event.player.PauseEvent +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.event.player.PlayingEvent +import com.theoplayer.android.api.event.player.SeekedEvent +import com.theoplayer.android.api.event.player.SeekingEvent +import com.theoplayer.android.api.event.player.SourceChangeEvent +import com.theoplayer.android.api.event.player.TimeUpdateEvent +import com.theoplayer.android.api.event.player.WaitingEvent +import com.theoplayer.android.api.event.track.mediatrack.video.ActiveQualityChangedEvent +import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes +import com.theoplayer.android.api.event.track.mediatrack.video.list.VideoTrackListEventTypes +import com.theoplayer.android.api.event.track.texttrack.EnterCueEvent +import com.theoplayer.android.api.event.track.texttrack.ExitCueEvent +import com.theoplayer.android.api.event.track.texttrack.TextTrackEventTypes +import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventTypes +import com.theoplayer.android.api.player.Player +import com.theoplayer.android.api.player.track.texttrack.TextTrackKind +import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent +typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent +typealias AddVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.AddTrackEvent +typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent + +private const val TAG = "AdobeEdgeConnector" + +enum class EventType { + PLAY, + PAUSE, + AD_BREAK_START, + AD_BREAK_COMPLETE, + AD_START, + AD_COMPLETE, + AD_SKIP, + CHAPTER_START, + CHAPTER_COMPLETE, + CHAPTER_SKIP, + SEEK_START, + SEEK_COMPLETE, + BUFFER_START, + BUFFER_COMPLETE, + BITRATE_CHANGE, + STATE_START, + STATE_END, + PLAYHEAD_UPDATE, + ERROR, + COMPLETE, + QOE_UPDATE, + SESSION_END, +} + +data class QueuedEvent( + val type: EventType, + val info: Map?, + val metadata: Map? +) + +const val PROP_CURRENT_TIME = "currentTime" +const val PROP_ERROR_ID = "errorId" +const val PROP_NA = "NA" + +class AdobeEdgeHandler( + private val player: Player, + trackerConfig: Map = emptyMap(), + customIdentityMap: IdentityMap? = null +) { + private var sessionInProgress = false + + private var adBreakPodIndex = 0 + + private var adPodPosition = 1 + + private var isPlayingAd = false + private var customMetadata: MutableMap = mutableMapOf() + private var currentChapter: TextTrackCue? = null + private var loggingMode: LoggingMode = LoggingMode.ERROR + private val onPlaying = EventListener { handlePlaying() } + private val onPause = EventListener { handlePause() } + private val onEnded = EventListener { handleEnded() } + private val onTimeUpdate = EventListener { handleTimeUpdate(it) } + private val onWaiting = EventListener { handleWaiting() } + private val onSeeking = EventListener { handleSeeking() } + private val onSeeked = EventListener { handleSeeked() } + private val onSourceChange = EventListener { handleSourceChange() } + private val onAddTextTrack = EventListener { handleAddTextTrack(it) } + private val onRemoveTextTrack = EventListener { handleRemoveTextTrack(it) } + private val onAddVideoTrack = EventListener { handleAddVideoTrack(it) } + private val onRemoveVideoTrack = + EventListener { handleRemoveVideoTrack(it) } + private val onActiveVideoQualityChanged = + EventListener { handleQualityChanged(it) } + private val onEnterCue = EventListener { handleEnterCue(it) } + private val onExitCue = EventListener { handleExitCue() } + private val onError = EventListener { handleError(it) } + private val onAdBreakBegin = + EventListener { event -> handleAdBreakBegin(event) } + private val onAdBreakEnd = EventListener { handleAdBreakEnd() } + private val onAdBegin = EventListener { event -> handleAdBegin(event) } + private val onAdEnd = EventListener { handleAdEnd() } + private val onAdSkip = EventListener { handleAdSkip() } + + private val scope = CoroutineScope(Dispatchers.Main) + private val tracker = Media.createTracker(trackerConfig) + private val eventQueue = mutableListOf() + private fun logDebug(message: String) { + if (loggingMode >= LoggingMode.DEBUG) { + Log.d(TAG, message) + } + } + + init { + addEventListeners() + customIdentityMap?.let { setCustomIdentityMap(it) } + logDebug("Initialized connector") + } + + fun setLoggingMode(loggingMode: LoggingMode) { + this.loggingMode = loggingMode + MobileCore.setLogLevel(loggingMode) + } + + fun updateMetadata(metadata: Map) { + customMetadata += metadata + } + + fun setCustomIdentityMap(customIdentityMap: IdentityMap) { + /** + * https://developer.adobe.com/client-sdks/edge/identity-for-edge-network/api-reference/#updateidentities + */ + Identity.updateIdentities(customIdentityMap) + } + + fun setError(errorId: String) { + queueOrSendEvent(EventType.ERROR, mapOf(PROP_ERROR_ID to errorId), null) + } + + fun stopAndStartNewSession(metadata: Map?) { + scope.launch { + maybeEndSession() + metadata?.let { + updateMetadata(it) + } + maybeStartSession() + if (player.isPaused) { + handlePause() + } else { + handlePlaying() + } + } + } + + private fun addEventListeners() { + player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.addEventListener(PlayerEventTypes.PAUSE, onPause) + player.addEventListener(PlayerEventTypes.ENDED, onEnded) + player.addEventListener(PlayerEventTypes.WAITING, onWaiting) + player.addEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) + player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack) + player.videoTracks.addEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + player.videoTracks.addEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + player.addEventListener(PlayerEventTypes.ERROR, onError) + player.ads.apply { + addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + addEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) + addEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + addEventListener(AdsEventTypes.AD_END, onAdEnd) + addEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + } + } + + private fun removeEventListeners() { + player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.removeEventListener(PlayerEventTypes.PAUSE, onPause) + player.removeEventListener(PlayerEventTypes.ENDED, onEnded) + player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) + player.removeEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) + player.textTracks.removeEventListener( + TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack + ) + player.videoTracks.removeEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + player.videoTracks.removeEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + player.removeEventListener(PlayerEventTypes.ERROR, onError) + player.ads.apply { + removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + removeEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) + removeEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + removeEventListener(AdsEventTypes.AD_END, onAdEnd) + removeEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + } + } + + private fun sendEvent( + event: EventType, + info: Map? = null, + metadata: Map? = null + ) { + when (event) { + EventType.AD_BREAK_START -> tracker.trackEvent(Event.AdBreakStart, info, metadata) + EventType.AD_BREAK_COMPLETE -> tracker.trackEvent(Event.AdBreakComplete, info, metadata) + EventType.AD_START -> tracker.trackEvent(Event.AdStart, info, metadata) + EventType.AD_COMPLETE -> tracker.trackEvent(Event.AdComplete, info, metadata) + EventType.AD_SKIP -> tracker.trackEvent(Event.AdSkip, info, metadata) + EventType.CHAPTER_START -> tracker.trackEvent(Event.ChapterStart, info, metadata) + EventType.CHAPTER_COMPLETE -> tracker.trackEvent(Event.ChapterComplete, info, metadata) + EventType.CHAPTER_SKIP -> tracker.trackEvent(Event.ChapterSkip, info, metadata) + EventType.SEEK_START -> tracker.trackEvent(Event.SeekStart, info, metadata) + EventType.SEEK_COMPLETE -> tracker.trackEvent(Event.SeekComplete, info, metadata) + EventType.BUFFER_START -> tracker.trackEvent(Event.BufferStart, info, metadata) + EventType.BUFFER_COMPLETE -> tracker.trackEvent(Event.BufferComplete, info, metadata) + EventType.BITRATE_CHANGE -> tracker.trackEvent(Event.BitrateChange, info, metadata) + EventType.STATE_START -> tracker.trackEvent(Event.StateStart, info, metadata) + EventType.STATE_END -> tracker.trackEvent(Event.StateEnd, info, metadata) + EventType.PLAYHEAD_UPDATE -> tracker.updateCurrentPlayhead( + (info?.get(PROP_CURRENT_TIME) as Int?) ?: 0 + ) + + EventType.ERROR -> tracker.trackError(info?.get(PROP_ERROR_ID) as String? ?: PROP_NA) + EventType.COMPLETE -> tracker.trackComplete() + EventType.QOE_UPDATE -> tracker.updateQoEObject(info ?: emptyMap()) + EventType.SESSION_END -> tracker.trackSessionEnd() + EventType.PLAY -> tracker.trackPlay() + EventType.PAUSE -> tracker.trackPause() + } + } + + private fun queueOrSendEvent( + type: EventType, + info: Map? = null, + metadata: Map? = null + ) { + if (sessionInProgress) { + sendEvent(type, info, metadata) + } else { + eventQueue.add(QueuedEvent(type, info, metadata)) + } + } + + private fun handlePlaying() { + // NOTE: In case of a pre-roll ad, the `playing` event will be sent twice: once starting the re-roll, and once + // starting content. During the pre-roll, all events will be queued. The session will be started after the pre-roll, + // making sure we can start the session with the correct content duration (not the ad duration). + logDebug("onPlaying") + maybeStartSession(player.duration) + queueOrSendEvent(EventType.PLAY) + } + + private fun handlePause() { + logDebug("onPause") + queueOrSendEvent(EventType.PAUSE) + } + + private fun handleTimeUpdate(event: TimeUpdateEvent) { + logDebug("onTimeUpdate") + queueOrSendEvent( + EventType.PLAYHEAD_UPDATE, + mapOf(PROP_CURRENT_TIME to sanitisePlayhead(event.currentTime, player.duration)) + ) + } + + private fun handleWaiting() { + logDebug("onWaiting") + queueOrSendEvent(EventType.BUFFER_START) + } + + private fun handleSeeking() { + logDebug("handleSeeking") + queueOrSendEvent(EventType.SEEK_START) + } + + private fun handleSeeked() { + logDebug("handleSeeked") + queueOrSendEvent(EventType.SEEK_COMPLETE) + } + + private fun handleEnded() { + logDebug("onEnded") + /** + * Tracks the completion of the media playback session. Call this method only when the media has + * been completely viewed. If the viewing session is ended before the media is completely viewed, + * use trackSessionEnd instead. + */ + queueOrSendEvent(EventType.COMPLETE) + reset() + } + + private fun handleSourceChange() { + logDebug("onSourceChange") + maybeEndSession() + } + + private fun handleQualityChanged(event: ActiveQualityChangedEvent) { + queueOrSendEvent( + EventType.QOE_UPDATE, Media.createQoEObject( + event.quality?.bandwidth?.toInt() ?: 0, + 0, + 0, + 0 + ) + ) + } + + private fun handleAddTextTrack(event: AddTextTrackEvent) { + event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> + logDebug("onAddTextTrack - add chapter track ${track.uid}") + track.addEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) + track.addEventListener(TextTrackEventTypes.EXITCUE, onExitCue) + } + } + + private fun handleRemoveTextTrack(event: RemoveTextTrackEvent) { + event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> + logDebug("onRemoveTextTrack - remove chapter track ${track.uid}") + track.removeEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) + track.removeEventListener(TextTrackEventTypes.EXITCUE, onExitCue) + } + } + + private fun handleAddVideoTrack(event: AddVideoTrackEvent) { + logDebug("onAddVideoTrack") + event.track.addEventListener( + VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged + ) + } + + private fun handleRemoveVideoTrack(event: RemoveVideoTrackEvent) { + logDebug("onRemoveVideoTrack") + event.track.removeEventListener( + VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged + ) + } + + private fun handleEnterCue(event: EnterCueEvent) { + logDebug("onEnterCue") + val chapterCue = event.cue + if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { + queueOrSendEvent(EventType.CHAPTER_SKIP) + } + queueOrSendEvent( + EventType.CHAPTER_START, + Media.createChapterObject( + chapterCue.id.ifEmpty { PROP_NA }, + chapterCue.id.toIntOrNull() ?: 1, + chapterCue.endTime.toInt(), + (chapterCue.endTime - chapterCue.startTime).toInt() + ), + customMetadata + ) + currentChapter = chapterCue + } + + private fun handleExitCue() { + logDebug("onExitCue") + queueOrSendEvent(EventType.CHAPTER_COMPLETE) + } + + private fun handleError(event: ErrorEvent) { + logDebug("onError") + queueOrSendEvent(EventType.ERROR, mapOf("errorId" to event.errorObject.code.toString())) + } + + private fun handleAdBreakBegin(event: AdBreakBeginEvent) { + logDebug("onAdBreakBegin") + isPlayingAd = true + val currentAdBreakTimeOffset = event.adBreak.timeOffset + // The pod position should start at 1. + val position = when { + currentAdBreakTimeOffset <= 0 -> 1 + else -> adBreakPodIndex + 1 + } + queueOrSendEvent( + EventType.AD_BREAK_START, Media.createAdBreakObject( + PROP_NA, + position, + currentAdBreakTimeOffset + ) + ) + + if (position > adBreakPodIndex) { + adBreakPodIndex++ + } + } + + private fun handleAdBreakEnd() { + logDebug("onAdBreakEnd") + isPlayingAd = false + adPodPosition = 1 + queueOrSendEvent(EventType.AD_BREAK_COMPLETE) + } + + private fun handleAdBegin(event: AdBeginEvent) { + logDebug("onAdBegin") + queueOrSendEvent( + EventType.AD_START, + Media.createAdObject( + PROP_NA, + PROP_NA, + adPodPosition, + (event.ad as? LinearAd)?.duration ?: 0 + ), + customMetadata + ) + adPodPosition++ + } + + private fun handleAdEnd() { + logDebug("onAdEnd") + queueOrSendEvent(EventType.AD_COMPLETE) + } + + private fun handleAdSkip() { + logDebug("onAdSkip") + queueOrSendEvent(EventType.AD_SKIP) + } + + private fun maybeEndSession() { + logDebug("maybeEndSession") + if (sessionInProgress) { + /** + * Tracks the end of a media playback session. Call this method when the viewing session ends, + * even if the user has not viewed the media to completion. If the media is viewed to completion, + * use trackComplete instead. + */ + queueOrSendEvent(EventType.SESSION_END) + } + reset() + } + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLengthSec + * @private + */ + private fun maybeStartSession(mediaLengthSec: Double? = null) { + val mediaLength = getContentLength(mediaLengthSec) + val hasValidSource = player.source !== null + val hasValidDuration = isValidDuration(mediaLengthSec) + + logDebug( + "maybeStartSession - " + "mediaLength: $mediaLength, " + "hasValidSource: $hasValidSource, " + "hasValidDuration: $hasValidDuration, " + "isPlayingAd: ${player.ads.isPlaying}" + ) + + if (sessionInProgress) { + logDebug("maybeStartSession - NOT started: already in progress") + return + } + + if (isPlayingAd) { + logDebug("maybeStartSession - NOT started: playing ad") + return + } + + if (!hasValidSource || !hasValidDuration) { + logDebug("maybeStartSession - NOT started: invalid ${if (hasValidSource) "duration" else "source"}") + return + } + + // Allow overriding metadata with custom metadata set via updateMetadata(). + val mergedMetadata = (player.source?.metadata?.data?.mapValues { it.value.toString() } + ?: emptyMap()) + customMetadata + tracker.trackSessionStart( + Media.createMediaObject( + mergedMetadata["friendlyName"] ?: mergedMetadata["title"] ?: PROP_NA, + mergedMetadata["name"] ?: mergedMetadata["id"] ?: PROP_NA, + mediaLength, + calculateStreamType(), + Media.MediaType.Video, + ), + customMetadata + ) + + sessionInProgress = true + + if (eventQueue.isNotEmpty()) { + val queuedEvents = eventQueue.toList() + eventQueue.clear() + queuedEvents.forEach { event -> sendEvent(event.type, event.info, event.metadata) } + } + + logDebug("maybeStartSession - STARTED") + } + + private fun getContentLength(mediaLengthSec: Double?): Int { + return sanitiseContentLength(mediaLengthSec) + } + + private fun calculateStreamType(): String { + return if (player.duration == Double.POSITIVE_INFINITY) + MediaConstants.StreamType.LIVE + else + MediaConstants.StreamType.VOD + } + + fun reset() { + logDebug("reset") + eventQueue.clear() + adBreakPodIndex = 0 + adPodPosition = 1 + isPlayingAd = false + sessionInProgress = false + currentChapter = null + customMetadata = mutableMapOf() + } + + fun destroy() { + logDebug("destroy") + maybeEndSession() + removeEventListeners() + } +} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt deleted file mode 100644 index 7233bc14..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge - -import android.util.Log - -object Logger { - var debug: Boolean = false - const val TAG = "AdobeEdgeConnector" - - fun debug(message: String) { - if (debug) { - Log.d(TAG, message) - } - } - - fun warn(message: String) { - Log.w(TAG, message) - } - - fun error(message: String) { - Log.e(TAG, message) - } -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 38a9e7ed..7c8e5d6f 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -1,18 +1,22 @@ - package com.theoplayer.reactnative.adobe.edge +import android.app.Application +import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.MobileCore import com.facebook.react.bridge.* import com.theoplayer.ReactTHEOplayerView import com.theoplayer.util.ViewResolver private const val TAG = "AdobeEdgeModule" +private const val PROP_ENVIRONMENT_ID = "environmentId" +private const val PROP_DEBUG_ENABLED = "debugEnabled" + @Suppress("unused") class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { private val viewResolver: ViewResolver = ViewResolver(context) - private var adobeConnectors: HashMap = HashMap() override fun getName(): String { @@ -22,23 +26,29 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : @ReactMethod fun initialize( tag: Int, - baseUrl: String, - configId: String, - userAgent: String?, - debug: Boolean?, - debugSessionId: String? + config: ReadableMap, + customIdentityMap: ReadableMap? ) { + /** + * If an asset config file is provided, use it to initialize the MobileCore SDK, otherwise use + * the App ID. + * {@link https://developer.adobe.com/client-sdks/edge/media-for-edge-network/} + */ + MobileCore.initialize( + reactApplicationContext.applicationContext as Application, + config.getString(PROP_ENVIRONMENT_ID) ?: "MissingEnvironmentID" + ) + viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.playerContext?.playerView?.let { playerView -> - adobeConnectors[tag] = - AdobeEdgeConnector( - player = playerView.player, - baseUrl = baseUrl, - configId = configId, - userAgent = userAgent, - debug = debug, - debugSessionId = debugSessionId - ) + adobeConnectors[tag] = AdobeEdgeConnector( + player = playerView.player, + trackerConfig = config.toHashMap().mapValues { it.value?.toString() ?: "" }, + customIdentityMap = customIdentityMap?.toAdobeIdentityMap() + ) + if (config.hasKey(PROP_DEBUG_ENABLED)) { + setDebug(tag, config.getBoolean(PROP_DEBUG_ENABLED)) + } } } } @@ -50,33 +60,36 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : */ @ReactMethod fun setDebug(tag: Int, debug: Boolean) { - adobeConnectors[tag]?.setDebug(debug) + adobeConnectors[tag]?.setLoggingMode( + when (debug) { + true -> LoggingMode.DEBUG + false -> LoggingMode.ERROR + } + ) } /** - * Set a debugSessionID query parameter that is added to all outgoing requests. + * Sets customMetadataDetails which will be passed for the session start request. */ @ReactMethod - fun setDebugSessionId(tag: Int, id: String?) { - adobeConnectors[tag]?.setDebugSessionId(id) + fun updateMetadata(tag: Int, customMetadataDetails: ReadableMap) { + adobeConnectors[tag]?.updateMetadata(customMetadataDetails.toAdobeCustomMetadataDetails()) } /** * Sets customMetadataDetails which will be passed for the session start request. */ @ReactMethod - fun updateMetadata(tag: Int, metadataList: ReadableArray) { - adobeConnectors[tag]?.updateMetadata( - metadataList.toAdobeCustomMetadataDetails() - ) + fun setCustomIdentityMap(tag: Int, customIdentityMap: ReadableMap) { + adobeConnectors[tag]?.setCustomIdentityMap(customIdentityMap.toAdobeIdentityMap()) } /** * Dispatch error event to adobe */ @ReactMethod - fun setError(tag: Int, errorDetails: ReadableMap) { - adobeConnectors[tag]?.setError(errorDetails.toAdobeErrorDetails()) + fun setError(tag: Int, errorId: String) { + adobeConnectors[tag]?.setError(errorId) } /** @@ -88,7 +101,7 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : * @param customMetadataDetails media details information. */ @ReactMethod - fun stopAndStartNewSession(tag: Int, customMetadataDetails: ReadableArray) { + fun stopAndStartNewSession(tag: Int, customMetadataDetails: ReadableMap) { adobeConnectors[tag]?.stopAndStartNewSession(customMetadataDetails.toAdobeCustomMetadataDetails()) } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index 0305dbc1..1831c36a 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -1,51 +1,53 @@ package com.theoplayer.reactnative.adobe.edge -import com.theoplayer.android.api.ads.Ad -import com.theoplayer.android.api.ads.AdBreak -import com.theoplayer.android.api.ads.LinearAd -import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingPodDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeChapterDetails +import com.adobe.marketing.mobile.edge.identity.AuthenticatedState +import com.adobe.marketing.mobile.edge.identity.IdentityItem +import com.facebook.react.bridge.ReadableMap +import com.adobe.marketing.mobile.edge.identity.IdentityMap +import java.util.Calendar fun sanitiseContentLength(mediaLength: Double?): Int { return if (mediaLength == Double.POSITIVE_INFINITY) { 86400 } else mediaLength?.toInt() ?: 0 } -fun calculateAdvertisingPodDetails(adBreak: AdBreak?, lastPodIndex: Int): AdobeAdvertisingPodDetails { - val currentAdBreakTimeOffset = adBreak?.timeOffset ?: 0 - return AdobeAdvertisingPodDetails( - index = when { - currentAdBreakTimeOffset == 0 -> 0 - currentAdBreakTimeOffset < 0 -> -1 - else ->lastPodIndex + 1 - }, - offset = currentAdBreakTimeOffset - ) +fun sanitisePlayhead(playhead: Double?, mediaLength: Double?): Int { + if (playhead == null || mediaLength == null) { + return 0 + } + if (mediaLength == Double.POSITIVE_INFINITY) { + // If content is live, the playhead must be the current second of the day. + val calendar = Calendar.getInstance() + return calendar.get(Calendar.SECOND) + + 60 * (calendar.get(Calendar.MINUTE) + + 60 * calendar.get(Calendar.HOUR_OF_DAY)) + } + return playhead.toInt() } -fun calculateAdvertisingDetails(ad: Ad?, podPosition: Int): AdobeAdvertisingDetails { - return AdobeAdvertisingDetails( - podPosition = podPosition, - length = if (ad is LinearAd) ad.duration else 0, - name = "NA", - playerName = "THEOplayer" - ) +fun isValidDuration(v: Double?): Boolean { + return v != null && !v.isNaN() } -fun calculateChapterDetails(cue: TextTrackCue): AdobeChapterDetails { - val index = try { - cue.id.toInt() - } catch (_: NumberFormatException) { - 0 - } - return AdobeChapterDetails( - length = (cue.endTime - cue.startTime).toInt(), - offset = cue.endTime.toInt(), - index = index - ) +fun ReadableMap.toAdobeCustomMetadataDetails() : HashMap { + return toHashMap().mapValues { it.value?.toString() ?: "" } as HashMap } -fun isValidDuration(v: Double?): Boolean { - return v != null && !v.isNaN() +fun ReadableMap.toAdobeIdentityMap(): IdentityMap { + return IdentityMap().apply { + toHashMap().forEach { (namespace, items) -> + val itemList = items as? List<*> ?: return@forEach + itemList.forEach { item -> + val itemMap = item as? Map<*, *> ?: return@forEach + addItem(IdentityItem( + itemMap["id"] as? String ?: "", + when (itemMap["authenticatedState"] as? String) { + "authenticated" -> AuthenticatedState.AUTHENTICATED + "loggedOut" -> AuthenticatedState.LOGGED_OUT + else -> AuthenticatedState.AMBIGUOUS + }, + itemMap["primary"] as? Boolean ?: false + ), namespace) + } + } + } } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt deleted file mode 100644 index 40516cf9..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Advertising details information. - * - * @see Adobe XDM AdvertisingDetails Schema - */ -data class AdobeAdvertisingDetails( - // ID of the ad. Any integer and/or letter combination. - val id: String? = null, - // Company/Brand whose product is featured in the ad. - val advertiser: String? = null, - // ID of the ad campaign. - val campaignID: String? = null, - // ID of the ad creative. - val creativeID: String? = null, - // URL of the ad creative. - val creativeURL: String? = null, - // Ad is completed. - val isCompleted: Boolean? = null, - // Ad is started. - val isStarted: Boolean? = null, - // Length of video ad in seconds. - val length: Int, - // Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - val name: String, - // Placement ID of the ad. - val placementID: String? = null, - // The name of the player responsible for rendering the ad. - val playerName: String, - // The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has index 1. - val podPosition: Int, - // ID of the ad site. - val siteID: String? = null, - // The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - val timePlayed: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt deleted file mode 100644 index b70e799b..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Advertising Pod details information. - * - * @see Adobe XDM AdvertisingPodDetails Schema - */ -data class AdobeAdvertisingPodDetails( - // The ID of the ad break. - val id: String? = null, - // The friendly name of the Ad Break. - val friendlyName: String? = null, - // The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad has index 1. - val index: Int, - // The offset of the ad break inside the content, in seconds. - val offset: Int -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt deleted file mode 100644 index 5c9c7e1c..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Chapter details information. - * - * @see Adobe XDM ChapterDetails Schema - */ -data class AdobeChapterDetails( - // The ID of the chapter. - val id: String? = null, - // The friendly name of the chapter. - val friendlyName: String? = null, - // The position (index, integer) of the chapter inside the content. - val index: Int, - // Chapter is completed. - val isCompleted: Boolean? = null, - // Chapter is started. - val isStarted: Boolean? = null, - // The length of the chapter, in seconds. - val length: Int, - // The offset of the chapter inside the content (in seconds) from the start. - val offset: Int, - // The time spent on the chapter, in seconds. - val timePlayed: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt deleted file mode 100644 index deb74779..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Custom metadata details information. - * - * @see Adobe XDM CustomMetadataDetails Schema - */ -data class AdobeCustomMetadataDetails( - // The name of the custom field. - val name: String? = null, - // The value of the custom field. - val value: String? = null -) - diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt deleted file mode 100644 index 93676e31..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Error details information. - * - * @see Adobe XDM ErrorDetails Schema - */ -data class AdobeErrorDetails( - // The error ID. - val name: String, - // The error source. - val source: ErrorSource -) - -enum class ErrorSource { - PLAYER, - EXTERNAL -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt deleted file mode 100644 index 211915b2..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Details about the SDK, library, or service used in an application or web page implementation of a service. - * - * @see Adobe XDM ImplementationDetails Schema - */ -data class AdobeImplementationDetails( - // The environment of the implementation - val environment: AdobeEnvironment? = null, - // SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - val name: String? = null, - // The version identifier of the API, e.g h.18. - val version: String? = null -) - -/** - * The environment of the implementation. - */ -enum class AdobeEnvironment { - BROWSER, - APP, - SERVER, - IOT -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt deleted file mode 100644 index 9a7c8c20..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Media details information. - * - * @see Adobe XDM MediaDetails Schema - */ -data class AdobeMediaDetails( - // If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - // If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - val playhead: Int? = null, - // Identifies an instance of a content stream unique to an individual playback. - val sessionID: String? = null, - // Session details information related to the experience event. - val sessionDetails: AdobeSessionDetails? = null, - // Advertising details information related to the experience event. - val advertisingDetails: AdobeAdvertisingDetails? = null, - // Advertising Pod details information - val advertisingPodDetails: AdobeAdvertisingPodDetails? = null, - // Chapter details information related to the experience event. - val chapterDetails: AdobeChapterDetails? = null, - // Error details information related to the experience event. - val errorDetails: AdobeErrorDetails? = null, - // Qoe data details information related to the experience event. - val qoeDataDetails: AdobeQoeDataDetails? = null, - // The list of states start. - val statesStart: List? = null, - // The list of states end. - val statesEnd: List? = null, - // The list of states. - val states: List? = null, - // The list of custom metadata. - val customMetadata: List? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt deleted file mode 100644 index 86728f59..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Player state data information. - * - * @see Adobe XDM PlayerStateData Schema - */ -data class AdobePlayerStateData( - // The name of the player state. - val name: String, - // Whether or not the player state is set on that state. - val isSet: Boolean? = null, - // The number of times that player state was set on the stream. - val count: Int? = null, - // The total duration of that player state. - val time: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt deleted file mode 100644 index 066815b5..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Qoe data details information related to the experience event. - * - * @see Adobe XDM QoeDataDetails Schema - */ -data class AdobeQoeDataDetails( - // The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. - val bitrateAverage: String? = null, - // The bitrate value (in kbps). - val bitrate: Int? = null, - // The average bitrate (in kbps, integer). - val bitrateAverageBucket: Int? = null, - // The number of streams in which bitrate changes occurred. - val hasBitrateChangeImpactedStreams: Boolean? = null, - // The number of bitrate changes. - val bitrateChangeCount: Int? = null, - // The number of streams in which frames were dropped. - val hasDroppedFrameImpactedStreams: Boolean? = null, - // The number of frames dropped during playback of the main content. - val droppedFrames: Int? = null, - // The number of times a user quit the video before its start. - val isDroppedBeforeStart: Boolean? = null, - // The current value of the stream frame-rate (in frames per second). - val framesPerSecond: Int? = null, - // Describes the duration (in seconds) passed between video load and start. - val timeToStart: Int? = null, - // The number of streams impacted by buffering. - val hasBufferImpactedStreams: Boolean? = null, - // The number of buffer events. - val bufferCount: Int? = null, - // The total amount of time, in seconds, spent buffering. - val bufferTime: Int? = null, - // The number of streams in which an error event occurred. - val hasErrorImpactedStreams: Boolean? = null, - // The number of errors that occurred. - val errorCount: Int? = null, - // The number of streams in which a stalled event occurred. - val hasStallImpactedStreams: Boolean? = null, - // The number of times the playback was stalled during a playback session. - val stallCount: Int? = null, - // The total time (seconds) the playback was stalled during a playback session. - val stallTime: Int? = null, - // The unique error IDs generated by the player SDK. - val playerSdkErrors: List? = null, - // The unique error IDs from any external source, e.g., CDN errors. - val externalErrors: List? = null, - // The unique error IDs generated by Media SDK during playback. - val mediaSdkErrors: List? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt deleted file mode 100644 index f2c77212..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Session details information related to the experience event. - * - * @see Adobe XDM SessionDetails Schema - */ -data class AdobeSessionDetails( - // This identifies an instance of a content stream unique to an individual playback. - val ID: String? = null, - // The number of ads started during the playback. - val adCount: Int? = null, - // The type of ad loaded as defined by each customer's internal representation. - val adLoad: String? = null, - // The name of the album that the music recording or video belongs to. - val album: String? = null, - // The SDK version used by the player. - val appVersion: String? = null, - // The name of the album artist or group performing the music recording or video. - val artist: String? = null, - // This is the unique identifier for the content of the media asset. - val assetID: String? = null, - // Name of the media author. - val author: String? = null, - // Describes the average content time spent for a specific media item. - val averageMinuteAudience: Int? = null, - // Distribution channel from where the content was played. - val channel: String, - // The number of chapters started during the playback. - val chapterCount: Int? = null, - // The type of the stream delivery. - val contentType: ContentType, - // A property that defines the time of the day when the content was broadcast or played. - val dayPart: String? = null, - // The number of the episode. - val episode: String? = null, - // The estimated number of video or audio streams per each individual content. - val estimatedStreams: Int? = null, - // The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of the feed like a URL. - val feed: String? = null, - // The date when the content first aired on television. - val firstAirDate: String? = null, - // The date when the content first aired on any digital channel or platform. - val firstDigitalDate: String? = null, - // This is the “friendly” (human-readable) name of the content. - val friendlyName: String? = null, - // Type or grouping of content as defined by content producer. - val genre: String? = null, - // Indicates if one or more pauses occurred during the playback of a single media item. - val hasPauseImpactedStreams: Boolean? = null, - // Indicates that the playhead passed the 10% marker of media based on stream length. - val hasProgress10: Boolean? = null, - // Indicates that the playhead passed the 25% marker of media based on stream length. - val hasProgress25: Boolean? = null, - // Indicates that the playhead passed the 50% marker of media based on stream length. - val hasProgress50: Boolean? = null, - // Indicates that the playhead passed the 75% marker of media based on stream length. - val hasProgress75: Boolean? = null, - // Indicates that the playhead passed the 95% marker of media based on stream length. - val hasProgress95: Boolean? = null, - // Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - val hasResume: Boolean? = null, - // Indicates when at least one frame, not necessarily the first has been viewed. - val hasSegmentView: Boolean? = null, - // The user has been authorized via Adobe authentication. - val isAuthorized: Boolean? = null, - // Indicates if a timed media asset was watched to completion. - val isCompleted: Boolean? = null, - // The stream was played locally on the device after being downloaded. - val isDownloaded: Boolean? = null, - // Set to true when the hit is federated. - val isFederated: Boolean? = null, - // First frame of media is consumed. - val isPlayed: Boolean? = null, - // Load event for the media. - val isViewed: Boolean? = null, - // Name of the record label. - val label: String? = null, - // Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - val length: Int, - // MVPD provided via Adobe authentication. - val mvpd: String? = null, - // Content ID of the content, which can be used to tie back to other industry / CMS IDs. - val name: String, - // The network/channel name. - val network: String? = null, - // Creator of the content. - val originator: String? = null, - // The number of pause periods that occurred during playback. - val pauseCount: Int? = null, - // Describes the duration in seconds in which playback was paused by the user. - val pauseTime: Int? = null, - // Name of the content player. - val playerName: String, - // Name of the audio content publisher. - val publisher: String? = null, - // Rating as defined by TV Parental Guidelines. - val rating: String? = null, - // The season number the show belongs to. - val season: String? = null, - // Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment the session was closed. - val secondsSinceLastCall: Int? = null, - // The interval that describes the part of the content that has been viewed in minutes. - val segment: String? = null, - // Program/Series Name. - val show: String? = null, - // The type of content for example, trailer or full episode. - val showType: String? = null, - // The radio station name on which the audio is played. - val station: String? = null, - // Format of the stream (HD, SD). - val streamFormat: String? = null, - // The type of the media stream. - val streamType: StreamType? = null, - // Sums the event duration (in seconds) for all events of type PLAY on the main content. - val timePlayed: Int? = null, - // Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent watching ads. - val totalTimePlayed: Int? = null, - // Describes the sum of the unique intervals seen by a user on a timed media asset. - val uniqueTimePlayed: Int? = null -) - -/** - * The type of the stream delivery. - */ -enum class ContentType { - // Video-on-demand - VOD, - // Live streaming - LIVE, - // Linear playback of the media asset - LINEAR, - // User-generated content - UGC, - // Downloaded video-on-demand - DVOD, - // Radio show - RADIO, - // Audio podcast - PODCAST, - // Audiobook - AUDIOBOOK, - // Song - SONG -} - -/** - * The type of the media stream. - */ -enum class StreamType { - VIDEO, - AUDIO -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt deleted file mode 100644 index 881013e1..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Enum representing the types of media events. - */ -enum class EventType(val value: String) { - SESSION_START("media.sessionStart"), - PLAY("media.play"), - PING("media.ping"), - BITRATE_CHANGE("media.bitrateChange"), - BUFFER_START("media.bufferStart"), - PAUSE_START("media.pauseStart"), - AD_BREAK_START("media.adBreakStart"), - AD_START("media.adStart"), - AD_COMPLETE("media.adComplete"), - AD_SKIP("media.adSkip"), - AD_BREAK_COMPLETE("media.adBreakComplete"), - CHAPTER_START("media.chapterStart"), - CHAPTER_SKIP("media.chapterSkip"), - CHAPTER_COMPLETE("media.chapterComplete"), - ERROR("media.error"), - SESSION_END("media.sessionEnd"), - SESSION_COMPLETE("media.sessionComplete"), - STATES_UPDATE("media.statesUpdate") -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt deleted file mode 100644 index 33a368cf..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt +++ /dev/null @@ -1,366 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -import com.theoplayer.reactnative.adobe.edge.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import org.json.JSONObject - -data class QueuedEvent( - val path: String, - val mediaDetails: Map -) - -class MediaEdgeAPI( - private val baseUrl: String, - private val configId: String, - private val userAgent: String, - private var debugSessionId: String? = null -) { - private val client = OkHttpClient() - private val gson = Gson() - var sessionId: String? = null - private set - - var hasSessionFailed = false - private set - - private val eventQueue = mutableListOf() - - private val scope = CoroutineScope(Dispatchers.Main) - - fun setDebugSessionId(id: String?) { - debugSessionId = id - } - - fun hasSessionStarted(): Boolean = sessionId != null - - fun reset() { - sessionId = null - hasSessionFailed = false - eventQueue.clear() - } - - fun play(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent("/play", mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails)) - } - - fun pause(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/pauseStart", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun error( - playhead: Double?, - errorDetails: AdobeErrorDetails, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/error", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "qoeDataDetails" to qoeDataDetails, - "errorDetails" to errorDetails - ) - ) - } - - fun ping(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - scope.launch { - sessionId?.let { sessionId -> - postEvent( - sessionId, - "/ping", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - } - } - - fun bufferStart(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/bufferStart", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun sessionComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/sessionComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun sessionEnd(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/sessionEnd", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - sessionId = null - } - - fun statesUpdate( - playhead: Double?, - statesStart: List? = null, - statesEnd: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/statesUpdate", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "statesStart" to statesStart, - "statesEnd" to statesEnd, - "qoeDataDetails" to qoeDataDetails - ) - ) - } - - fun bitrateChange(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails) { - maybeQueueEvent( - "/bitrateChange", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun chapterSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/chapterSkip", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun chapterStart( - playhead: Double?, - chapterDetails: AdobeChapterDetails, - customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/chapterStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "chapterDetails" to chapterDetails, - "customMetadata" to customMetadata, - "qoeDataDetails" to qoeDataDetails - ) - ) - } - - fun chapterComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/chapterComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun adBreakStart( - playhead: Double, - advertisingPodDetails: AdobeAdvertisingPodDetails, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/adBreakStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "advertisingPodDetails" to advertisingPodDetails, - "qoeDataDetails" to qoeDataDetails - ) - ) - } - - fun adBreakComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/adBreakComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - fun adStart( - playhead: Double, - advertisingDetails: AdobeAdvertisingDetails, - customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/adStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "advertisingDetails" to advertisingDetails, - "customMetadata" to customMetadata, - "qoeDataDetails" to qoeDataDetails - ) - ) - } - - fun adSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent("/adSkip", mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails)) - } - - fun adComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/adComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - - private fun createUrlWithClientParams(baseUrl: String): HttpUrl { - return baseUrl.toHttpUrl().newBuilder().apply { - addQueryParameter("configId", configId) - debugSessionId?.let { addQueryParameter("debugSessionId", it) } - }.build() - } - - private suspend fun sendRequest( - url: String, - body: String - ): Response? = withContext(Dispatchers.IO) { - return@withContext try { - val request = Request.Builder() - .url(createUrlWithClientParams(url)) - .post(body.toRequestBody("application/json".toMediaType())) - .header("User-Agent", userAgent) - .build() - - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - throw IOException("Unexpected code $response") - } else - response - } catch (e: Exception) { - throw e - } - } - - suspend fun startSession( - sessionDetails: AdobeSessionDetails, - customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - try { - val body = JsonObject().apply { - add("events", JsonArray().apply { - add(JsonObject().apply { - add("xdm", JsonObject().apply { - addProperty("eventType", EventType.SESSION_START.value) - addProperty("timestamp", Date().toISOString()) - add("mediaCollection", JsonObject().apply { - addProperty("playhead", 0) - add("sessionDetails", gson.toJsonTree(sessionDetails)) - qoeDataDetails?.let { - add("qoeDataDetails", gson.toJsonTree(qoeDataDetails)) - } - customMetadata?.let { - add("customMetadata", JsonArray().apply { - it.forEach { metadata -> - add(gson.toJsonTree(metadata)) - } - }) - } - }) - }) - }) - }) - } - - val response = sendRequest("$baseUrl/sessionStart", body.toString()) - - val responseBody = response?.body?.string() ?: throw IOException("Empty response body") - val jsonResponse = JSONObject(responseBody) - val error = jsonResponse.optJSONObject("error") ?: jsonResponse.optJSONObject("data") - ?.optJSONArray("errors") - if (error != null) { - throw Exception(error.toString()) - } - - val handle = jsonResponse.optJSONArray("handle") - sessionId = handle?.let { array -> - (0 until array.length()).firstNotNullOfOrNull { i -> - array.optJSONObject(i)?.takeIf { it.optString("type") == "media-analytics:new-session" } - ?.optJSONArray("payload")?.optJSONObject(0)?.optString("sessionId") - } - } - - if (eventQueue.isNotEmpty()) { - sessionId?.let { sessionId -> - eventQueue.forEach { event -> postEvent(sessionId, event.path, event.mediaDetails) } - } - eventQueue.clear() - } - } catch (e: Exception) { - Logger.error("Failed to start session. ${e.message}") - hasSessionFailed = true - } - } - - private fun maybeQueueEvent(path: String, mediaDetails: Map) { - if (hasSessionFailed) return - sessionId?.let { sessionId -> - scope.launch { - postEvent(sessionId, path, mediaDetails) - } - } ?: eventQueue.add(QueuedEvent(path, mediaDetails)) - } - - private suspend fun postEvent(sessionId: String, path: String, mediaDetails: Map) { - try { - val body = JsonObject().apply { - add("events", JsonArray().apply { - add(JsonObject().apply { - add("xdm", JsonObject().apply { - addProperty("eventType", pathToEventTypeMap[path]?.value) - addProperty("timestamp", Date().toISOString()) - add("mediaCollection", JsonObject().apply { - mediaDetails.forEach { (key, value) -> - add(key, gson.toJsonTree(value)) - } - addProperty("sessionID", sessionId) - }) - }) - }) - }) - }.toString() - - Logger.debug("postEvent - $path $body") - - val response = sendRequest("$baseUrl$path", body) - val responseBody = response?.body?.string() ?: throw IOException("Empty response body") - - // Optionally parse errors - if (responseBody.isNotEmpty()) { - val jsonResponse = JSONObject(responseBody) - val error = jsonResponse.optJSONObject("error") ?: jsonResponse.optJSONObject("data") - ?.optJSONArray("errors") - if (error != null) { - Logger.error("Failed to send event. $error") - } - } - } catch (e: Exception) { - Logger.error("Failed to send event. ${e.message}") - } - } -} - -fun Date.toISOString(): String { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply { - timeZone = TimeZone.getTimeZone("UTC") - }.format(this) -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt deleted file mode 100644 index 50079396..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -val pathToEventTypeMap: Map = mapOf( - "/adBreakComplete" to EventType.AD_BREAK_COMPLETE, - "/adBreakStart" to EventType.AD_BREAK_START, - "/adComplete" to EventType.AD_COMPLETE, - "/adSkip" to EventType.AD_SKIP, - "/adStart" to EventType.AD_START, - "/bitrateChange" to EventType.BITRATE_CHANGE, - "/bufferStart" to EventType.BUFFER_START, - "/chapterComplete" to EventType.CHAPTER_COMPLETE, - "/chapterSkip" to EventType.CHAPTER_SKIP, - "/chapterStart" to EventType.CHAPTER_START, - "/error" to EventType.ERROR, - "/pauseStart" to EventType.PAUSE_START, - "/ping" to EventType.PING, - "/play" to EventType.PLAY, - "/sessionComplete" to EventType.SESSION_COMPLETE, - "/sessionEnd" to EventType.SESSION_END, - "/sessionStart" to EventType.SESSION_START, - "/statesUpdate" to EventType.STATES_UPDATE, -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt deleted file mode 100644 index 5bcacfcc..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -import android.os.Build -import android.os.LocaleList -import java.util.Locale - -fun buildUserAgent(): String { - val locale: Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList.getDefault().get(0) - } else { - Locale.getDefault() - } - // Example: Mozilla/5.0 (Linux; U; Android 7.1.2; en-US; AFTN Build/NS6296) - return "Mozilla/5.0 (Linux; U; Android ${Build.VERSION.RELEASE}; $locale; ${Build.MODEL} Build/${Build.ID})" -} - -fun sanitisePlayhead(playhead: Double?): Int { - if (playhead == null) { - return 0 - } - if (playhead == Double.POSITIVE_INFINITY) { - // If content is live, the playhead must be the current second of the day. - val now = System.currentTimeMillis() - return ((now / 1000) % 86400).toInt() - } - return playhead.toInt() -} diff --git a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift index 6ccb9f82..aeb4950e 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift @@ -5,450 +5,36 @@ import Foundation import THEOplayerSDK import UIKit - -let CONTENT_PING_INTERVAL = 10.0 -let AD_PING_INTERVAL = 1.0 +import AEPServices +import AEPEdgeIdentity class AdobeEdgeConnector { - private weak var player: THEOplayer? - private var baseUrl: String - private var configId: String - private var debug: Bool = false - private var debugSessionId: String? - - private var mediaApi: MediaEdgeAPI - - private var customMetadata: [AdobeCustomMetadataDetails] = [] - private var sessionInProgress = false - private var isPlayingAd = false - private var pingTimer: Timer? - private var adBreakPodIndex: Int = 0 - private var adPodPosition: Int = 0 - private var currentChapter: TextTrackCue? = nil - - // MARK: Player Listeners - private var playingListener: EventListener? - private var pauseListener: EventListener? - private var endedListener: EventListener? - private var waitingListener: EventListener? - private var sourceChangeListener: EventListener? - private var loadedMetadataListener: EventListener? - private var errorListener: EventListener? - private var addTextTrackListener: EventListener? - private var removeTextTrackListener: EventListener? - private var addVideoTrackListener: EventListener? - - // MARK: Ad Listeners - private var adBreakBeginListener: EventListener? - private var adBreakEndListener: EventListener? - private var adBeginListener: EventListener? - private var adEndListener: EventListener? - - // MARK: MediaTrack listeners - private var videoAddTrackListener: EventListener? - private var videoRemoveTrackListener: EventListener? - private var videoQualityChangeListeners: [Int:EventListener] = [:] - private var audioQualityChangeListeners: [Int:EventListener] = [:] - - init(player: THEOplayer, baseUrl: String, configId: String, userAgent: String?, debug: Bool, debugSessionId: String?) { - self.player = player - self.baseUrl = baseUrl - self.configId = configId - self.debug = debug - self.debugSessionId = debugSessionId - self.mediaApi = MediaEdgeAPI(baseUrl: baseUrl, configId: configId, userAgent: userAgent ?? AdobeUtils.buildUserAgent(), debugSessionId: debugSessionId) - - self.addEventListeners() - - self.log("Connector initialized.") + private var handler: AdobeEdgeHandler + init(player: THEOplayer, trackerConfig: [String:String], customIdentityMap: [String:Any]? = nil) { + self.handler = AdobeEdgeHandler(player: player, trackerConfig: trackerConfig, customIdentityMap: customIdentityMap) } - func setDebug(_ debug: Bool) -> Void { - self.debug = debug + func updateMetadata(_ metadata: [String:String]) -> Void { + self.handler.updateMetadata(metadata) } - func setDebugSessionId(_ debugId: String?) -> Void { - self.mediaApi.setDebugSessionId(debugId: debugId) + func setCustomIdentityMap(_ customIdentityMap: [String:Any]) -> Void { + self.handler.setCustomIdentityMap(customIdentityMap) } - func updateMetadata(_ metadata: [AdobeCustomMetadataDetails]) -> Void { - self.customMetadata.append(contentsOf: metadata) + func stopAndStartNewSession(_ metadata: [String:String]) -> Void { + self.handler.stopAndStartNewSession(metadata) } - func setError(_ errorDetails: AdobeErrorDetails) -> Void { - guard let player = self.player else {return} - self.mediaApi.error(playhead: player.currentTime, errorDetails: errorDetails) + func setLoggingMode(_ debug: LogLevel) -> Void { + self.handler.setLoggingMode(debug) } - func stopAndStartNewSession(_ metadata: [AdobeCustomMetadataDetails]) -> Void { - self.maybeEndSession() - self.updateMetadata(metadata) - self.maybeStartSession() - if let player = self.player { - if player.paused { - self.onPause(event: PauseEvent(currentTime: player.currentTime)) - } else { - self.onPlaying(event: PlayingEvent(currentTime: player.currentTime)) - } - } + func setError(_ errorId: String) -> Void { + self.handler.setError(errorId) } func destroy() -> Void { - self.log("destroy.") - self.removeEventListeners() - self.maybeEndSession() - } - - func addEventListeners() -> Void { - guard let player = self.player else {return} - - // Player events - self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING, listener: self.onPlaying(event:)) - self.pauseListener = player.addEventListener(type: PlayerEventTypes.PAUSE, listener: self.onPause(event:)) - self.endedListener = player.addEventListener(type: PlayerEventTypes.ENDED, listener: self.onEnded(event:)) - self.waitingListener = player.addEventListener(type: PlayerEventTypes.WAITING, listener: self.onWaiting(event:)) - self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.onSourceChange(event:)) - self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA, listener: self.onLoadedMetadata(event:)) - self.errorListener = player.addEventListener(type: PlayerEventTypes.ERROR, listener: self.onError(event:)) - - // Bitrate - self.videoAddTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.ADD_TRACK) { [weak self] event in - guard let welf = self else { return } - if let videoTrack = event.track as? VideoTrack { - // start listening for qualityChange events on this track - welf.videoQualityChangeListeners[videoTrack.uid] = videoTrack.addEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: welf.onActiveQualityChange(event:)) - } - } - self.videoRemoveTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.REMOVE_TRACK) { [weak self] event in - guard let welf = self else { return } - if let videoTrack = event.track as? VideoTrack { - if let videoQualityChangeListener = welf.videoQualityChangeListeners.removeValue(forKey: videoTrack.uid) { - videoTrack.removeEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: videoQualityChangeListener) - } - } - } - - // Ad events - self.adBreakBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_BEGIN, listener: self.onAdBreakBegin(event:)) - self.adBreakEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_END, listener: self.onAdBreakEnd(event:)) - self.adBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BEGIN, listener: self.onAdBegin(event:)) - self.adEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_END, listener: self.onAdEnd(event:)) - - self.log("Listeners attached.") - } - - func removeEventListeners() -> Void { - guard let player = self.player else {return} - - // Player events - if let playingListener = self.playingListener { - player.removeEventListener(type: PlayerEventTypes.PLAYING, listener: playingListener) - } - if let pauseListener = self.pauseListener { - player.removeEventListener(type: PlayerEventTypes.PAUSE, listener: pauseListener) - } - if let endedListener = self.endedListener { - player.removeEventListener(type: PlayerEventTypes.ENDED, listener: endedListener) - } - if let waitingListener = self.waitingListener { - player.removeEventListener(type: PlayerEventTypes.WAITING, listener: waitingListener) - } - if let sourceChangeListener = self.sourceChangeListener { - player.removeEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: sourceChangeListener) - } - if let loadedMetadataListener = self.loadedMetadataListener { - player.removeEventListener(type: PlayerEventTypes.LOADED_META_DATA, listener: loadedMetadataListener) - } - if let errorListener = self.errorListener { - player.removeEventListener(type: PlayerEventTypes.ERROR, listener: errorListener) - } - - // Bitrate - let videoTrackCount = player.videoTracks.count - if videoTrackCount > 0 { - for i in 0.. Void { - self.log("onLoadedMetadata triggered.") - self.maybeStartSession() - } - - func onPlaying(event: PlayingEvent) -> Void { - guard let player = self.player else {return} - self.log("onPlayingEvent triggered.") - //self.maybeStartSession(mediaLengthSec: player.duration) - self.mediaApi.play(playhead: player.currentTime) - } - - func onPause(event: PauseEvent) -> Void { - guard let player = self.player else {return} - self.log("onPause triggered.") - self.mediaApi.pause(playhead: player.currentTime) - } - - func onWaiting(event: WaitingEvent) -> Void { - guard let player = self.player else {return} - self.log("onWaiting triggered.") - self.mediaApi.bufferStart(playhead: player.currentTime) - } - - func onEnded(event: EndedEvent) -> Void { - guard let player = self.player else {return} - self.log("onEnded triggered.") - self.mediaApi.sessionComplete(playhead: player.currentTime) - self.reset() - } - - func onSourceChange(event: SourceChangeEvent) -> Void { - self.log("onSourceChange triggered.") - self.maybeEndSession() - } - - func onActiveQualityChange(event: ActiveQualityChangedEvent) -> Void { - guard let player = self.player else {return} - self.log("onActiveQualityChange triggered.") - var bitrate = 0 - if let activeTrack = self.activeTrack(tracks: player.videoTracks) { - bitrate = activeTrack.activeQuality?.bandwidth ?? 0 - } - self.mediaApi.bitrateChange( - playhead: player.currentTime, qoeDataDetails: AdobeQoeDataDetails(bitrate: bitrate) - ) - } - - private func activeTrack(tracks: THEOplayerSDK.MediaTrackList) -> MediaTrack? { - guard tracks.count > 0 else { - return nil; - } - var track: MediaTrack? - for i in 0...tracks.count-1 { - track = tracks.get(i) - if (track != nil && track!.enabled) { - return track - } - } - return nil; - } - - func onError(event: ErrorEvent) -> Void { - guard let player = self.player else {return} - self.log("onError triggered.") - var errorCodeString = "-1" - if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { - errorCodeString = String(errorCodeValue) - } - self.mediaApi.error( - playhead: player.currentTime, - errorDetails: AdobeErrorDetails( - name: errorCodeString, - source: .player - ) - ) - } - - func onAdBreakBegin(event: AdBreakBeginEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBreakBegin triggered.") - self.isPlayingAd = true - self.startPinger(AD_PING_INTERVAL) - let podDetails = AdobeUtils.calculateAdvertisingPodDetails(adBreak: event.ad, lastPodIndex: self.adBreakPodIndex) - self.mediaApi.adBreakStart(playhead: player.currentTime, advertisingPodDetails: podDetails) - if (podDetails.index > adBreakPodIndex) { - adBreakPodIndex += 1 - } - } - - func onAdBreakEnd(event: AdBreakEndEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBreakEnd triggered.") - self.isPlayingAd = false - self.adPodPosition = 1 - self.startPinger(CONTENT_PING_INTERVAL) - self.mediaApi.adBreakComplete(playhead: player.currentTime) - } - - func onAdBegin(event: AdBeginEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBegin triggered.") - self.mediaApi.adStart( - playhead: player.currentTime, - advertisingDetails: AdobeUtils.calculateAdvertisingDetails(ad: event.ad, podPosition: self.adPodPosition), - customMetadata: self.customMetadata - ) - self.adPodPosition += 1 - } - - func onAdEnd(event: AdEndEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdEnd triggered.") - self.mediaApi.adComplete(playhead: player.currentTime) - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLength - * @private - */ - func maybeStartSession(mediaLengthSec: Double? = nil) -> Void { - guard let player = self.player else { - return - } - - let mediaLength = self.getContentLength() - let hasValidSource = player.source != nil - let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) - self.log("maybeStartSession - mediaLength: \(mediaLength)") - self.log("maybeStartSession - hasValidSource: \(hasValidSource)") - self.log("maybeStartSession - hasValidDuration: \(hasValidDuration)") - self.log("maybeStartSession - sessionInProgress: \(self.sessionInProgress)") - self.log("maybeStartSession - isPlayingAd: \(self.isPlayingAd)") - - guard !sessionInProgress else { - self.log("maybeStartSession - NOT started: already in progress") - return - } - - guard !isPlayingAd else { - self.log("maybeStartSession - NOT started: playing ad") - return - } - - guard hasValidSource && hasValidDuration else { - let reason = hasValidSource ? "duration" : "source" - self.log("maybeStartSession - NOT started: invalid \(reason)") - return - } - - Task { @MainActor in - - let sessionDetails = AdobeSessionDetails( - ID: "N/A", - channel: "N/A", - contentType: getContentType(), - length: mediaLength, - name: player.source?.metadata?.title ?? "N/A", - playerName: "THEOplayer" - ) - - self.log("maybeStartSession - call startSession") - await self.mediaApi.startSession(sessionDetails: sessionDetails, customMetadata: self.customMetadata) - - guard self.mediaApi.hasSessionStarted() else { - self.log("maybeStartSession - session was not started") - return - } - - sessionInProgress = true - self.log("maybeStartSession - STARTED sessionId: \(self.mediaApi.sessionId ?? "")") - - if !isPlayingAd { - startPinger(CONTENT_PING_INTERVAL) - } else { - startPinger(AD_PING_INTERVAL) - } - } - } - - private func maybeEndSession() -> Void { - guard let player = self.player else {return} - self.log("maybeEndSession") - if (self.mediaApi.hasSessionStarted()) { - self.mediaApi.sessionEnd(playhead: player.currentTime) - } - reset() - } - - private func reset() -> Void { - self.log("reset"); - self.mediaApi.reset() - self.adBreakPodIndex = 0; - self.adPodPosition = 1; - self.isPlayingAd = false; - self.sessionInProgress = false; - self.pingTimer?.invalidate(); - self.pingTimer = nil - self.currentChapter = nil; - } - - private func startPinger(_ interval: Double) { - DispatchQueue.main.async { - self.pingTimer?.invalidate() - self.pingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { t in - guard let player = self.player else {return} - self.mediaApi.ping(playhead: player.currentTime) - }) - self.log("Pinger started with interval \(interval).") - } - } - - /** - * Get the current media length in seconds. - * - * - In case of a live stream, set it to 24h. - * - * @param mediaLengthInMSec optional mediaLengthInMSec provided by a player event. - * @private - */ - private func getContentLength(mediaLengthInSec: Double? = nil) -> Int { - if let mediaLength = mediaLengthInSec { - return mediaLength == Double.infinity ? 86400 : Int(mediaLength) - } - - if let player = self.player, - let duration = player.duration { - return duration == Double.infinity ? 86400 : Int(duration) - } - - return 86400 - } - - private func getContentType() -> ContentType { - if let player = self.player, - let duration = player.duration { - if duration != Double.infinity { - return ContentType.vod - } - } - return ContentType.live - } - - private func log(_ text: String) { - if self.debug { - print("[adobe-edge-connector]", text) - } + self.handler.destroy() } } diff --git a/adobe-edge/ios/Connector/AdobeEdgeEvent.swift b/adobe-edge/ios/Connector/AdobeEdgeEvent.swift new file mode 100644 index 00000000..8db463f7 --- /dev/null +++ b/adobe-edge/ios/Connector/AdobeEdgeEvent.swift @@ -0,0 +1,31 @@ +// +// AdobeEdgeEvent.swift +// + +enum AdobeEdgeEventType: Int { + case PLAYING = 0 + case PAUSE + case AD_BREAK_START + case AD_BREAK_COMPLETE + case AD_START + case AD_COMPLETE + case AD_SKIP + case SEEK_START + case SEEK_COMPLETE + case BUFFER_START + case BUFFER_COMPLETE + case BITRATE_CHANGE + case STATE_START + case STATE_END + case PLAYHEAD_UPDATE + case ERROR + case COMPLETE + case QOE_UPDATE + case SESSION_END +} + +struct AdobeEdgeEvent { + var type: AdobeEdgeEventType + var info: [String: Any]? = nil + var metadata: [String: String]? = nil +} diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift new file mode 100644 index 00000000..6c9aa05c --- /dev/null +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -0,0 +1,531 @@ +// +// AdobeEdgeConnector.swift +// + +import Foundation +import THEOplayerSDK +import UIKit +import AEPServices +import AEPCore +import AEPEdgeMedia +import AEPEdgeIdentity + +let CONTENT_PING_INTERVAL = 10.0 +let AD_PING_INTERVAL = 1.0 + +let PROP_NA: String = "NA" +let PROP_CURRENTTIME: String = "currentTime" +let PROP_ERRORID: String = "errorId" + +let TAG: String = "[AdobeEdgeConnector]" + +class AdobeEdgeHandler { + private weak var player: THEOplayer? + private var trackerConfig: [String:String] + private var sessionInProgress = false + private var adBreakPodIndex: Int = 0 + private var adPodPosition: Int = 0 + private var isPlayingAd = false + private var customMetadata: [String:String] = [:] + private var currentChapter: TextTrackCue? = nil + private var loggingMode: LogLevel = .error + private var tracker: MediaTracker = Media.createTracker() + private var eventQueue: [AdobeEdgeEvent] = [] + + // MARK: Player Listeners + private var playingListener: THEOplayerSDK.EventListener? + private var pauseListener: THEOplayerSDK.EventListener? + private var endedListener: THEOplayerSDK.EventListener? + private var timeUpdateListener: THEOplayerSDK.EventListener? + private var waitingListener: THEOplayerSDK.EventListener? + private var seekingListener: THEOplayerSDK.EventListener? + private var seekedListener: THEOplayerSDK.EventListener? + private var sourceChangeListener: THEOplayerSDK.EventListener? + private var errorListener: THEOplayerSDK.EventListener? + private var addTextTrackListener: THEOplayerSDK.EventListener? + private var removeTextTrackListener: THEOplayerSDK.EventListener? + private var addVideoTrackListener: THEOplayerSDK.EventListener? + + // MARK: Ad Listeners + private var adBreakBeginListener: THEOplayerSDK.EventListener? + private var adBreakEndListener: THEOplayerSDK.EventListener? + private var adBeginListener: THEOplayerSDK.EventListener? + private var adEndListener: THEOplayerSDK.EventListener? + private var adSkipListener: THEOplayerSDK.EventListener? + + // MARK: MediaTrack listeners + private var videoAddTrackListener: THEOplayerSDK.EventListener? + private var videoRemoveTrackListener: THEOplayerSDK.EventListener? + private var videoQualityChangeListeners: [Int:THEOplayerSDK.EventListener] = [:] + private var audioQualityChangeListeners: [Int:THEOplayerSDK.EventListener] = [:] + + private func logDebug(_ text: String) { + if self.loggingMode >= .debug { + print(TAG, text) + } + } + + init(player: THEOplayer, trackerConfig: [String:String], customIdentityMap: [String:Any]? = nil) { + self.player = player + self.trackerConfig = trackerConfig + if let identityMap = customIdentityMap { + self.setCustomIdentityMap(identityMap) + } + self.addEventListeners() + + let environmentId = trackerConfig["environmentId"] ?? "MissingEnvironmentID" + MobileCore.setLogLevel(.error) + MobileCore.initialize(appId: environmentId) { + self.logDebug("MobileCore successfully initialized with App ID: \(environmentId)") + } + + self.logDebug("Connector Initialized.") + } + + func setLoggingMode(_ debug: LogLevel) -> Void { + self.loggingMode = debug + MobileCore.setLogLevel(debug) + } + + func updateMetadata(_ metadata: [String:String]) -> Void { + self.customMetadata.merge(metadata) { (_, new) in new } + } + + func setCustomIdentityMap(_ customIdentityMap: [String:Any]) -> Void { + if let identityMap = AdobeEdgeUtils.toIdentityMap(customIdentityMap) { + Identity.updateIdentities(with: identityMap) + } + } + + func setError(_ errorId: String) -> Void { + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .ERROR, info: [PROP_ERRORID: errorId])) + } + + func stopAndStartNewSession(_ metadata: [String:String]) -> Void { + guard let player = self.player else { return } + self.maybeEndSession() + self.updateMetadata(metadata) + self.maybeStartSession() + player.paused ? self.onPause() : self.onPlaying() + } + + func addEventListeners() -> Void { + guard let player = self.player else { return } + + // Player events + self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING, listener: self.handlePlaying(event:)) + self.pauseListener = player.addEventListener(type: PlayerEventTypes.PAUSE, listener: self.handlePause(event:)) + self.endedListener = player.addEventListener(type: PlayerEventTypes.ENDED, listener: self.handleEnded(event:)) + self.waitingListener = player.addEventListener(type: PlayerEventTypes.WAITING, listener: self.handleWaiting(event:)) + self.seekingListener = player.addEventListener(type: PlayerEventTypes.SEEKING, listener: self.handleSeeking(event:)) + self.seekedListener = player.addEventListener(type: PlayerEventTypes.SEEKED, listener: self.handleSeeked(event:)) + self.timeUpdateListener = player.addEventListener(type: PlayerEventTypes.TIME_UPDATE, listener: self.handleTimeUpdate(event:)) + self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.handleSourceChange(event:)) + self.errorListener = player.addEventListener(type: PlayerEventTypes.ERROR, listener: self.handleError(event:)) + + // Bitrate + self.videoAddTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.ADD_TRACK) { [weak self] event in + guard let welf = self else { return } + if let videoTrack = event.track as? VideoTrack { + // start listening for qualityChange events on this track + welf.videoQualityChangeListeners[videoTrack.uid] = videoTrack.addEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: welf.handleActiveQualityChange(event:)) + } + } + self.videoRemoveTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.REMOVE_TRACK) { [weak self] event in + guard let welf = self else { return } + if let videoTrack = event.track as? VideoTrack { + if let videoQualityChangeListener = welf.videoQualityChangeListeners.removeValue(forKey: videoTrack.uid) { + videoTrack.removeEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: videoQualityChangeListener) + } + } + } + + // Ad events + self.adBreakBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_BEGIN, listener: self.handleAdBreakBegin(event:)) + self.adBreakEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_END, listener: self.handleAdBreakEnd(event:)) + self.adBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BEGIN, listener: self.handleAdBegin(event:)) + self.adEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_END, listener: self.handleAdEnd(event:)) + self.adSkipListener = player.ads.addEventListener(type: AdsEventTypes.AD_SKIP, listener: self.handleAdSkip(event:)) + + self.logDebug("Listeners attached.") + } + + func removeEventListeners() -> Void { + guard let player = self.player else {return} + + // Player events + if let playingListener = self.playingListener { + player.removeEventListener(type: PlayerEventTypes.PLAYING, listener: playingListener) + } + if let pauseListener = self.pauseListener { + player.removeEventListener(type: PlayerEventTypes.PAUSE, listener: pauseListener) + } + if let endedListener = self.endedListener { + player.removeEventListener(type: PlayerEventTypes.ENDED, listener: endedListener) + } + if let waitingListener = self.waitingListener { + player.removeEventListener(type: PlayerEventTypes.WAITING, listener: waitingListener) + } + if let seekingListener = self.seekingListener { + player.removeEventListener(type: PlayerEventTypes.SEEKING, listener: seekingListener) + } + if let seekedListener = self.seekedListener { + player.removeEventListener(type: PlayerEventTypes.SEEKED, listener: seekedListener) + } + if let timeUpdateListener = self.timeUpdateListener { + player.removeEventListener(type: PlayerEventTypes.TIME_UPDATE, listener: timeUpdateListener) + } + if let sourceChangeListener = self.sourceChangeListener { + player.removeEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: sourceChangeListener) + } + if let errorListener = self.errorListener { + player.removeEventListener(type: PlayerEventTypes.ERROR, listener: errorListener) + } + + // Bitrate + let videoTrackCount = player.videoTracks.count + if videoTrackCount > 0 { + for i in 0.. Void { + guard self.player != nil else { return } + self.logDebug("onWaiting") + + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .BUFFER_START)) + } + + func handleSeeking(event: SeekingEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onSeeking") + + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SEEK_START)) + } + + func handleSeeked(event: SeekedEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onSeeked") + + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SEEK_COMPLETE)) + } + + func handleEnded(event: EndedEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onEnded") + + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .COMPLETE)) + self.reset() + } + + func handleSourceChange(event: SourceChangeEvent) -> Void { + self.logDebug("onSourceChange") + self.maybeEndSession() + } + + func handleActiveQualityChange(event: ActiveQualityChangedEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onActiveQualityChange") + + var bitrate = 0 + if let activeTrack = self.activeTrack(tracks: player.videoTracks) { + bitrate = activeTrack.activeQuality?.bandwidth ?? 0 + } + if let qoe = Media.createQoEObjectWith(bitrate: bitrate, startupTime: 0, fps: 0, droppedFrames: 0) { + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .QOE_UPDATE, info: qoe)) + } + } + + private func activeTrack(tracks: THEOplayerSDK.MediaTrackList) -> MediaTrack? { + guard tracks.count > 0 else { + return nil; + } + var track: MediaTrack? + for i in 0...tracks.count-1 { + track = tracks.get(i) + if (track != nil && track!.enabled) { + return track + } + } + return nil; + } + + func handleError(event: ErrorEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onError") + var errorCodeString = "-1" + if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { + errorCodeString = String(errorCodeValue) + } + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .ERROR, info: [PROP_ERRORID: errorCodeString])) + } + + func handleAdBreakBegin(event: AdBreakBeginEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onAdBreakBegin") + self.isPlayingAd = true + let currentAdBreakTimeOffset = event.ad?.timeOffset ?? 0 + let position = currentAdBreakTimeOffset <= 0 ? 1 : self.adBreakPodIndex + 1 + let adBreakObject = Media.createAdBreakObjectWith(name: PROP_NA, position: position, startTime: currentAdBreakTimeOffset) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_BREAK_START, info: adBreakObject)) + if (position > self.adBreakPodIndex) { + self.adBreakPodIndex += 1 + } + } + + func handleAdBreakEnd(event: AdBreakEndEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onAdBreakEnd") + self.isPlayingAd = false + self.adPodPosition = 1 + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_BREAK_COMPLETE)) + } + + func handleAdBegin(event: AdBeginEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onAdBegin") + let duration = event.ad?.duration ?? 0 + let adObject = Media.createAdObjectWith(name: PROP_NA, id: PROP_NA, position: self.adPodPosition, length: duration) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_START, info: adObject)) + self.adPodPosition += 1 + } + + func handleAdEnd(event: AdEndEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onAdEnd") + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_COMPLETE)) + } + + func handleAdSkip(event: AdSkipEvent) -> Void { + guard self.player != nil else { return } + self.logDebug("onAdSkip") + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_SKIP)) + } + + private func maybeEndSession() -> Void { + guard self.player != nil else { return } + self.logDebug("maybeEndSession") + if self.sessionInProgress { + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SESSION_END)) + self.sessionInProgress = false + } + self.reset() + } + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLength + * @private + */ + func maybeStartSession(mediaLengthSec: Double? = nil) -> Void { + guard let player = self.player else { return } + + let mediaLength = self.sanitiseContentLength(mediaLengthSec) + let hasValidSource = player.source != nil + let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) + let streamType = self.getStreamType() + self.logDebug("maybeStartSession - mediaLength: \(mediaLength)") + self.logDebug("maybeStartSession - hasValidSource: \(hasValidSource)") + self.logDebug("maybeStartSession - hasValidDuration: \(hasValidDuration)") + self.logDebug("maybeStartSession - sessionInProgress: \(self.sessionInProgress)") + self.logDebug("maybeStartSession - isPlayingAd: \(self.isPlayingAd)") + self.logDebug("maybeStartSession - streamType: \(streamType)") + + guard !self.sessionInProgress else { + self.logDebug("maybeStartSession - NOT started: already in progress") + return + } + + guard !self.isPlayingAd else { + self.logDebug("maybeStartSession - NOT started: playing ad") + return + } + + guard hasValidSource && hasValidDuration else { + let reason = hasValidSource ? "duration" : "source" + self.logDebug("maybeStartSession - NOT started: invalid \(reason)") + return + } + + var metadata: [String: Any] = player.source?.metadata?.metadataKeys ?? [:] + metadata.merge(self.customMetadata) { (_, new) in new } + + if let mediaObject = Media.createMediaObjectWith( + name: metadata["friendlyName"] as? String ?? metadata["title"] as? String ?? PROP_NA, + id: metadata["name"] as? String ?? metadata["id"] as? String ?? PROP_NA, + length: mediaLength, + streamType: streamType, + mediaType: MediaType.Video + ) { + self.tracker.trackSessionStart(info: mediaObject, metadata: self.customMetadata) + self.sessionInProgress = true + self.logDebug("maybeStartSession - STARTED") + + // Send any queued events + if !self.eventQueue.isEmpty { + self.logDebug("Sending \(self.eventQueue.count) queued events.") + for event in self.eventQueue { + self.sendEvent(event: event) + } + self.eventQueue.removeAll() + } + } + } + + private func sendEvent(event: AdobeEdgeEvent) { + if event.type != AdobeEdgeEventType.PLAYHEAD_UPDATE { // don't clutter output with timeUpdates... + self.logDebug("sendEvent: \(event.type)") + } + + switch event.type { + case .AD_BREAK_START: self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: event.info, metadata: event.metadata) + case .AD_BREAK_COMPLETE: self.tracker.trackEvent(event: MediaEvent.AdBreakComplete, info: event.info, metadata: event.metadata) + case .AD_START: self.tracker.trackEvent(event: MediaEvent.AdStart, info: event.info, metadata: event.metadata) + case .AD_COMPLETE: self.tracker.trackEvent(event: MediaEvent.AdComplete, info: event.info, metadata: event.metadata) + case .AD_SKIP: self.tracker.trackEvent(event: MediaEvent.AdSkip, info: event.info, metadata: event.metadata) + case .SEEK_START: self.tracker.trackEvent(event: MediaEvent.SeekStart, info: event.info, metadata: event.metadata) + case .SEEK_COMPLETE: self.tracker.trackEvent(event: MediaEvent.SeekComplete, info: event.info, metadata: event.metadata) + case .BUFFER_START: self.tracker.trackEvent(event: MediaEvent.BufferStart, info: event.info, metadata: event.metadata) + case .BUFFER_COMPLETE: self.tracker.trackEvent(event: MediaEvent.BufferComplete, info: event.info, metadata: event.metadata) + case .BITRATE_CHANGE: self.tracker.trackEvent(event: MediaEvent.BitrateChange, info: event.info, metadata: event.metadata) + case .STATE_START: self.tracker.trackEvent(event: MediaEvent.StateStart, info: event.info, metadata: event.metadata) + case .STATE_END: self.tracker.trackEvent(event: MediaEvent.StateEnd, info: event.info, metadata: event.metadata) + case .PLAYHEAD_UPDATE: self.tracker.updateCurrentPlayhead(time: event.info?[PROP_CURRENTTIME] as? Int ?? 0) + case .ERROR: self.tracker.trackError(errorId: event.info?[PROP_ERRORID] as? String ?? PROP_NA) + case .COMPLETE: self.tracker.trackComplete() + case .QOE_UPDATE: self.tracker.updateQoEObject(qoe: event.info ?? [:]) + case .SESSION_END: self.tracker.trackSessionEnd() + case .PLAYING: self.tracker.trackPlay() + case .PAUSE: self.tracker.trackPause() + } + } + + private func queueOrSendEvent(event: AdobeEdgeEvent) { + if self.sessionInProgress { + self.sendEvent(event: event) + } else { + self.logDebug("Queueing event: \(event.type)") + self.eventQueue.append(event) + } + } + + private func reset() -> Void { + self.logDebug("reset") + self.adBreakPodIndex = 0 + self.adPodPosition = 1 + self.isPlayingAd = false + self.sessionInProgress = false + self.currentChapter = nil + self.eventQueue.removeAll() + self.customMetadata.removeAll() + } + + func destroy() -> Void { + self.logDebug("destroy.") + self.removeEventListeners() + self.maybeEndSession() + } + + private func getStreamType() -> String { + if let player = self.player, + let duration = player.duration { + if duration != Double.infinity { + return MediaConstants.StreamType.VOD + } + } + return MediaConstants.StreamType.LIVE + } + + private func sanitisePlayhead(playhead: Double?, mediaLength: Double?) -> Int { + guard let playhead = playhead, let mediaLength = mediaLength else { + return 0 + } + + if mediaLength == Double.infinity { + // If content is live, the playhead must be the current second of the day. + let now = Date() + let calendar = Calendar.current + let seconds = + calendar.component(.hour, from: now) * 3600 + + calendar.component(.minute, from: now) * 60 + + calendar.component(.second, from: now) + + return seconds + } + + return Int(playhead) + } + + private func sanitiseContentLength(_ mediaLength: Double?) -> Int { + mediaLength == .infinity ? 86400 : Int(mediaLength ?? 0) + } +} diff --git a/adobe-edge/ios/Connector/AdobeEdgeUtils.swift b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift new file mode 100644 index 00000000..ea2d47c5 --- /dev/null +++ b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift @@ -0,0 +1,35 @@ +// AdobeEdgeUtils.swift + +import Foundation +import THEOplayerSDK +import AEPEdgeIdentity + +class AdobeEdgeUtils { + class func toStringMap(_ map: [String: Any]) -> [String: String] { + var result = [String: String]() + for (key, value) in map { + if let stringValue = value as? String { + result[key] = stringValue + } else if let optionalValue = value as? CustomStringConvertible { + // Convert other types (Int, Bool, Double, etc.) to String + result[key] = String(describing: optionalValue) + } else { + // If value is nil or not convertible, use empty string + result[key] = "" + } + } + + return result + } + + class func toIdentityMap(_ map: [String: Any]) -> IdentityMap? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: map) else { + return nil + } + guard let identityMap = try? JSONDecoder().decode(IdentityMap.self, from: jsonData) else { + return nil + } + return identityMap + } +} + diff --git a/adobe-edge/ios/Connector/AdobeUtils.swift b/adobe-edge/ios/Connector/AdobeUtils.swift deleted file mode 100644 index 56302032..00000000 --- a/adobe-edge/ios/Connector/AdobeUtils.swift +++ /dev/null @@ -1,55 +0,0 @@ -// AdobeUtils.swift - -import Foundation -import THEOplayerSDK - -class AdobeUtils { - class func calculateAdvertisingPodDetails(adBreak: AdBreak?, lastPodIndex: Int) -> AdobeAdvertisingPodDetails { - let currentAdBreakTimeOffset = adBreak?.timeOffset ?? 0 - - let index: Int - if currentAdBreakTimeOffset == 0 { - index = 0 - } else if currentAdBreakTimeOffset < 0 { - index = -1 - } else { - index = lastPodIndex + 1 - } - - return AdobeAdvertisingPodDetails( - index: index, - offset: currentAdBreakTimeOffset - ) - } - - class func calculateAdvertisingDetails(ad: Ad?, podPosition: Int) -> AdobeAdvertisingDetails { - let length = (ad as? LinearAd)?.duration ?? 0 - - return AdobeAdvertisingDetails( - length: length, - name: "NA", - playerName: "THEOplayer", - podPosition: podPosition - ) - } - - class func buildUserAgent() -> String { - let device = UIDevice.current - let model = device.model - let osVersion = device.systemVersion.replacingOccurrences(of: ".", with: "_") - let locale = (UserDefaults.standard.array(forKey: "AppleLanguages")?.first as? String) ?? Locale.current.identifier - let userAgent = "Mozilla/5.0 (\(model); CPU OS \(osVersion) like Mac OS X; \(locale))" - return userAgent - } - - class func toDictionary(_ value: T) -> [String: Any] { - let encoder = JSONEncoder() - guard let data = try? encoder.encode(value), - let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), - let dictionary = jsonObject as? [String: Any] else { - return [:] - } - return dictionary - } -} - diff --git a/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift b/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift deleted file mode 100644 index 6e16bee5..00000000 --- a/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift +++ /dev/null @@ -1,48 +0,0 @@ -// AdobeAdvertisingDetails.swift - -/// Advertising details information. -/// -/// - SeeAlso: [Adobe XDM AdvertisingDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md) -struct AdobeAdvertisingDetails: Codable { - /// ID of the ad. Any integer and/or varter combination. - var id: String? - - /// Company/Brand whose product is featured in the ad. - var advertiser: String? - - /// ID of the ad campaign. - var campaignID: String? - - /// ID of the ad creative. - var creativeID: String? - - /// URL of the ad creative. - var creativeURL: String? - - /// Ad is compvared. - var isCompvared: Bool? - - /// Ad is started. - var isStarted: Bool? - - /// Length of video ad in seconds. - var length: Int - - /// Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - var name: String - - /// Placement ID of the ad. - var placementID: String? - - /// The name of the player responsible for rendering the ad. - var playerName: String - - /// The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has index 1. - var podPosition: Int - - /// ID of the ad site. - var siteID: String? - - /// The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - var timePlayed: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift b/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift deleted file mode 100644 index 568452e9..00000000 --- a/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift +++ /dev/null @@ -1,18 +0,0 @@ -// AdobeAdvertisingPodDetails.swift - -/// Advertising Pod details information. -/// -/// - SeeAlso: [Adobe XDM AdvertisingPodDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md) -struct AdobeAdvertisingPodDetails: Codable { - /// The ID of the ad break. - var id: String? - - /// The friendly name of the Ad Break. - var friendlyName: String? - - /// The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad has index 1. - var index: Int - - /// The offset of the ad break inside the content, in seconds. - var offset: Int -} diff --git a/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift b/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift deleted file mode 100644 index cfe87e5e..00000000 --- a/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift +++ /dev/null @@ -1,30 +0,0 @@ -// AdobeChapterDetails.swift - -/// Chapter details information. -/// -/// - SeeAlso: [Adobe XDM ChapterDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md) -struct AdobeChapterDetails: Codable { - /// The ID of the chapter. - var id: String? - - /// The friendly name of the chapter. - var friendlyName: String? - - /// The position (index, integer) of the chapter inside the content. - var index: Int - - /// Chapter is compvared. - var isCompvared: Bool? - - /// Chapter is started. - var isStarted: Bool? - - /// The length of the chapter, in seconds. - var length: Int - - /// The offset of the chapter inside the content (in seconds) from the start. - var offset: Int - - /// The time spent on the chapter, in seconds. - var timePlayed: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift b/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift deleted file mode 100644 index 9997b81c..00000000 --- a/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift +++ /dev/null @@ -1,12 +0,0 @@ -// AdobeCustomMetadataDetails.swift - -/// Custom metadata details information. -/// -/// - SeeAlso: [Adobe XDM CustomMetadataDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md) -struct AdobeCustomMetadataDetails: Codable { - /// The name of the custom field. - let name: String? - - /// The value of the custom field. - let value: String? -} diff --git a/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift b/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift deleted file mode 100644 index dd7ca0b1..00000000 --- a/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift +++ /dev/null @@ -1,17 +0,0 @@ -// AdobeErrorDetails.swift - -/// Error details information. -/// -/// - SeeAlso: [Adobe XDM ErrorDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md) -struct AdobeErrorDetails: Codable { - /// The error ID. - let name: String - - /// The error source. - let source: ErrorSource -} - -enum ErrorSource: String, Codable { - case player = "player" - case external = "external" -} diff --git a/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift b/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift deleted file mode 100644 index 68ed1f9d..00000000 --- a/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift +++ /dev/null @@ -1,23 +0,0 @@ -// AdobeImplementationDetails.swift - -/// Details about the SDK, library, or service used in an application or web page implementation of a service. -/// -/// - SeeAlso: [Adobe XDM ImplementationDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md) -struct AdobeImplementationDetails: Codable { - /// The environment of the implementation - let environment: AdobeEnvironment? - - /// SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - let name: String? - - /// The version identifier of the API, e.g h.18. - let version: String? -} - -/// The environment of the implementation. -enum AdobeEnvironment: String, Codable { - case browser = "BROWSER" - case app = "APP" - case server = "SERVER" - case iot = "IOT" -} diff --git a/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift b/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift deleted file mode 100644 index 9a670e35..00000000 --- a/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift +++ /dev/null @@ -1,43 +0,0 @@ -// AdobeMediaDetails.swift - -/// Media details information. -/// -/// - SeeAlso: [Adobe XDM MediaDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md) -struct AdobeMediaDetails: Codable { - /// If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - /// If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - let playhead: Int? - - /// Identifies an instance of a content stream unique to an individual playback. - let sessionID: String? - - /// Session details information related to the experience event. - let sessionDetails: AdobeSessionDetails? - - /// Advertising details information related to the experience event. - let advertisingDetails: AdobeAdvertisingDetails? - - /// Advertising Pod details information - let advertisingPodDetails: AdobeAdvertisingPodDetails? - - /// Chapter details information related to the experience event. - let chapterDetails: AdobeChapterDetails? - - /// Error details information related to the experience event. - let errorDetails: AdobeErrorDetails? - - /// Qoe data details information related to the experience event. - let qoeDataDetails: AdobeQoeDataDetails? - - /// The list of states start. - let statesStart: [AdobePlayerStateData]? - - /// The list of states end. - let statesEnd: [AdobePlayerStateData]? - - /// The list of states. - let states: [AdobePlayerStateData]? - - /// The list of custom metadata. - let customMetadata: [AdobeCustomMetadataDetails]? -} diff --git a/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift b/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift deleted file mode 100644 index 8c1ff97c..00000000 --- a/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift +++ /dev/null @@ -1,18 +0,0 @@ -// AdobePlayerStateData.swift - -/// Player state data information. -/// -/// - SeeAlso: [Adobe XDM PlayerStateData Schema](https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json) -struct AdobePlayerStateData: Codable { - /// The name of the player state. - let name: String - - /// Whether or not the player state is set on that state. - let isSet: Bool? - - /// The number of times that player state was set on the stream. - let count: Int? - - /// The total duration of that player state. - let time: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift b/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift deleted file mode 100644 index 0086a3ea..00000000 --- a/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift +++ /dev/null @@ -1,69 +0,0 @@ -// AdobeQoeDataDetails.swift - -/// Qoe data details information related to the experience event. -/// -/// - SeeAlso: [Adobe XDM QoeDataDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md) -struct AdobeQoeDataDetails: Codable { - /// The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. - var bitrateAverage: String? - - /// The bitrate value (in kbps). - var bitrate: Int? - - /// The average bitrate (in kbps, integer). - var bitrateAverageBucket: Int? - - /// The number of streams in which bitrate changes occurred. - var hasBitrateChangeImpactedStreams: Bool? - - /// The number of bitrate changes. - var bitrateChangeCount: Int? - - /// The number of streams in which frames were dropped. - var hasDroppedFrameImpactedStreams: Bool? - - /// The number of frames dropped during playback of the main content. - var droppedFrames: Int? - - /// The number of times a user quit the video before its start. - var isDroppedBeforeStart: Bool? - - /// The current value of the stream frame-rate (in frames per second). - var framesPerSecond: Int? - - /// Describes the duration (in seconds) passed between video load and start. - var timeToStart: Int? - - /// The number of streams impacted by buffering. - var hasBufferImpactedStreams: Bool? - - /// The number of buffer events. - var bufferCount: Int? - - /// The total amount of time, in seconds, spent buffering. - var bufferTime: Int? - - /// The number of streams in which an error event occurred. - var hasErrorImpactedStreams: Bool? - - /// The number of errors that occurred. - var errorCount: Int? - - /// The number of streams in which a stalled event occurred. - var hasStallImpactedStreams: Bool? - - /// The number of times the playback was stalled during a playback session. - var stallCount: Int? - - /// The total time (seconds) the playback was stalled during a playback session. - var stallTime: Int? - - /// The unique error IDs generated by the player SDK. - var playerSdkErrors: [String]? - - /// The unique error IDs from any external source, e.g., CDN errors. - var externalErrors: [String]? - - /// The unique error IDs generated by Media SDK during playback. - var mediaSdkErrors: [String]? -} diff --git a/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift b/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift deleted file mode 100644 index 19d48186..00000000 --- a/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift +++ /dev/null @@ -1,193 +0,0 @@ -// AdobeSessionDetails.swift - -/// Session details information related to the experience event. -/// -/// - SeeAlso: [Adobe XDM SessionDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md) -struct AdobeSessionDetails: Codable { - /// This identifies an instance of a content stream unique to an individual playback. - var ID: String? - - /// The number of ads started during the playback. - var adCount: Int? - - /// The type of ad loaded as defined by each customer's internal representation. - var adLoad: String? - - /// The name of the album that the music recording or video belongs to. - var album: String? - - /// The SDK version used by the player. - var appVersion: String? - - /// The name of the album artist or group performing the music recording or video. - var artist: String? - - /// This is the unique identifier for the content of the media asset. - var assetID: String? - - /// Name of the media author. - var author: String? - - /// Describes the average content time spent for a specific media item. - var averageMinuteAudience: Int? - - /// Distribution channel from where the content was played. - var channel: String - - /// The number of chapters started during the playback. - var chapterCount: Int? - - /// The type of the stream delivery. - var contentType: ContentType - - /// A property that defines the time of the day when the content was broadcast or played. - var dayPart: String? - - /// The number of the episode. - var episode: String? - - /// The estimated number of video or audio streams per each individual content. - var estimatedStreams: Int? - - /// The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of the feed like a URL. - var feed: String? - - /// The date when the content first aired on television. - var firstAirDate: String? - - /// The date when the content first aired on any digital channel or platform. - var firstDigitalDate: String? - - /// This is the "friendly" (human-readable) name of the content. - var friendlyName: String? - - /// Type or grouping of content as defined by content producer. - var genre: String? - - /// Indicates if one or more pauses occurred during the playback of a single media item. - var hasPauseImpactedStreams: Bool? - - /// Indicates that the playhead passed the 10% marker of media based on stream length. - var hasProgress10: Bool? - - /// Indicates that the playhead passed the 25% marker of media based on stream length. - var hasProgress25: Bool? - - /// Indicates that the playhead passed the 50% marker of media based on stream length. - var hasProgress50: Bool? - - /// Indicates that the playhead passed the 75% marker of media based on stream length. - var hasProgress75: Bool? - - /// Indicates that the playhead passed the 95% marker of media based on stream length. - var hasProgress95: Bool? - - /// Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - var hasResume: Bool? - - /// Indicates when at least one frame, not necessarily the first has been viewed. - var hasSegmentView: Bool? - - /// The user has been authorized via Adobe authentication. - var isAuthorized: Bool? - - /// Indicates if a timed media asset was watched to compvarion. - var isCompvared: Bool? - - /// The stream was played locally on the device after being downloaded. - var isDownloaded: Bool? - - /// Set to true when the hit is federated. - var isFederated: Bool? - - /// First frame of media is consumed. - var isPlayed: Bool? - - /// Load event for the media. - var isViewed: Bool? - - /// Name of the record label. - var label: String? - - /// Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - var length: Int - - /// MVPD provided via Adobe authentication. - var mvpd: String? - - /// Content ID of the content, which can be used to tie back to other industry / CMS IDs. - var name: String - - /// The network/channel name. - var network: String? - - /// Creator of the content. - var originator: String? - - /// The number of pause periods that occurred during playback. - var pauseCount: Int? - - /// Describes the duration in seconds in which playback was paused by the user. - var pauseTime: Int? - - /// Name of the content player. - var playerName: String - - /// Name of the audio content publisher. - var publisher: String? - - /// Rating as defined by TV Parental Guidelines. - var rating: String? - - /// The season number the show belongs to. - var season: String? - - /// Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment the session was closed. - var secondsSinceLastCall: Int? - - /// The interval that describes the part of the content that has been viewed in minutes. - var segment: String? - - /// Program/Series Name. - var show: String? - - /// The type of content for example, trailer or full episode. - var showType: String? - - /// The radio station name on which the audio is played. - var station: String? - - /// Format of the stream (HD, SD). - var streamFormat: String? - - /// The type of the media stream. - var streamType: StreamType? - - /// Sums the event duration (in seconds) for all events of type PLAY on the main content. - var timePlayed: Int? - - /// Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent watching ads. - var totalTimePlayed: Int? - - /// Describes the sum of the unique intervals seen by a user on a timed media asset. - var uniqueTimePlayed: Int? -} - -/// The type of the stream delivery. -enum ContentType: String, Codable { - case vod = "VOD" - case live = "LIVE" - case linear = "LINEAR" - case ugc = "UGC" - case dvod = "DVOD" - case radio = "RADIO" - case podcast = "PODCAST" - case audiobook = "AUDIOBOOK" - case song = "SONG" -} - -/// The type of the media stream. -enum StreamType: String, Codable { - case video = "VIDEO" - case audio = "AUDIO" -} diff --git a/adobe-edge/ios/Connector/api/EventType.swift b/adobe-edge/ios/Connector/api/EventType.swift deleted file mode 100644 index 6484bc84..00000000 --- a/adobe-edge/ios/Connector/api/EventType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// EventType.swift - -/// Enum representing the types of media events. -enum EventType: String, Codable { - case sessionStart = "media.sessionStart" - case play = "media.play" - case ping = "media.ping" - case bitrateChange = "media.bitrateChange" - case bufferStart = "media.bufferStart" - case pauseStart = "media.pauseStart" - case adBreakStart = "media.adBreakStart" - case adStart = "media.adStart" - case adComplete = "media.adComplete" - case adSkip = "media.adSkip" - case adBreakComplete = "media.adBreakComplete" - case chapterStart = "media.chapterStart" - case chapterSkip = "media.chapterSkip" - case chapterComplete = "media.chapterComplete" - case error = "media.error" - case sessionEnd = "media.sessionEnd" - case sessionComplete = "media.sessionComplete" - case statesUpdate = "media.statesUpdate" -} diff --git a/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift b/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift deleted file mode 100644 index ebd1b23e..00000000 --- a/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift +++ /dev/null @@ -1,397 +0,0 @@ -// MediaEdgeAPI.swift - -import Foundation - -struct QueuedEvent { - let path: String - let mediaDetails: [String: Any?] -} - -class MediaEdgeAPI { - private let baseUrl: String - private let configId: String - private let userAgent: String - private var debugSessionId: String? - private let urlSession: URLSession - private let jsonEncoder = JSONEncoder() - private let jsonDecoder = JSONDecoder() - private(set) var sessionId: String? - private var hasSessionFailed = false - private var eventQueue = [QueuedEvent]() - private let dispatchQueue = DispatchQueue.main - - private let pathToEventTypeMap: [String: EventType] = [ - "/play": .play, - "/pauseStart": .pauseStart, - "/error": .error, - "/ping": .ping, - "/bufferStart": .bufferStart, - "/sessionComplete": .sessionComplete, - "/sessionEnd": .sessionEnd, - "/statesUpdate": .statesUpdate, - "/bitrateChange": .bitrateChange, - "/chapterSkip": .chapterSkip, - "/chapterStart": .chapterStart, - "/chapterComplete": .chapterComplete, - "/adBreakStart": .adBreakStart, - "/adBreakComplete": .adBreakComplete, - "/adStart": .adStart, - "/adSkip": .adSkip, - "/adComplete": .adComplete - ] - - init(baseUrl: String, configId: String, userAgent: String, debugSessionId: String? = nil) { - self.baseUrl = baseUrl - self.configId = configId - self.userAgent = userAgent - self.debugSessionId = debugSessionId - self.urlSession = URLSession.shared - self.jsonEncoder.dateEncodingStrategy = .iso8601 - self.jsonDecoder.dateDecodingStrategy = .iso8601 - } - - func setDebugSessionId(debugId: String?) { - debugSessionId = debugId - } - - func hasSessionStarted() -> Bool { - return sessionId != nil - } - - func reset() { - sessionId = nil - hasSessionFailed = false - eventQueue.removeAll() - } - - func play(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/play", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func pause(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/pauseStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func error(playhead: Double?, errorDetails: AdobeErrorDetails, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/error", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails, - "errorDetails": errorDetails - ]) - } - - func ping(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - guard let sessionId = sessionId else { return } - Task { @MainActor in - await postEvent(sessionId: sessionId, path: "/ping", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - } - - func bufferStart(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/bufferStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func sessionComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/sessionComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func sessionEnd(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/sessionEnd", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - sessionId = nil - } - - func statesUpdate( - playhead: Double?, - statesStart: [AdobePlayerStateData]? = nil, - statesEnd: [AdobePlayerStateData]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/statesUpdate", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "statesStart": statesStart, - "statesEnd": statesEnd, - "qoeDataDetails": qoeDataDetails - ]) - } - - func bitrateChange(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails) { - maybeQueueEvent(path: "/bitrateChange", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/chapterSkip", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterStart( - playhead: Double?, - chapterDetails: AdobeChapterDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/chapterStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "chapterDetails": chapterDetails, - "customMetadata": customMetadata, - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/chapterComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adBreakStart( - playhead: Double, - advertisingPodDetails: AdobeAdvertisingPodDetails, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/adBreakStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "advertisingPodDetails": advertisingPodDetails, - "qoeDataDetails": qoeDataDetails - ]) - } - - func adBreakComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adBreakComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adStart( - playhead: Double, - advertisingDetails: AdobeAdvertisingDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/adStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "advertisingDetails": advertisingDetails, - "customMetadata": customMetadata, - "qoeDataDetails": qoeDataDetails - ]) - } - - func adSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adSkip", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - private func createUrlWithClientParams(baseUrl: String) -> URL? { - var components = URLComponents(string: baseUrl) - components?.queryItems = [ - URLQueryItem(name: "configId", value: configId) - ] - if let debugSessionId = debugSessionId { - components?.queryItems?.append(URLQueryItem(name: "debugSessionId", value: debugSessionId)) - } - return components?.url - } - - private func sendRequest(url: String, body: String) async throws -> Data? { - guard let url = createUrlWithClientParams(baseUrl: url) else { - throw URLError(.badURL) - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue(userAgent, forHTTPHeaderField: "User-Agent") - request.httpBody = body.data(using: .utf8) - - do { - let (data, response) = try await urlSession.data(for: request) - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) - } - print("Adobe sendRequest responseCode: \(httpResponse)") - return data - } catch { - throw error - } - } - - func startSession( - sessionDetails: AdobeSessionDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) async { - do { - var mediaCollection: [String:Any] = [:] - mediaCollection["playhead"] = 0 - mediaCollection["sessionDetails"] = sessionDetails.toJSONEncodableDictionary() - if let qoeData = qoeDataDetails { - mediaCollection["qoeDataDetails"] = qoeData.toJSONEncodableDictionary() - } - if let metadata = customMetadata { - mediaCollection["customMetadata"] = metadata.map({ $0.toJSONEncodableDictionary() }) - } - let xdm: [String:Any] = [ - "eventType": EventType.sessionStart.rawValue, - "timestamp": ISO8601DateFormatter().string(from: Date()), - "mediaCollection": mediaCollection - ] - let event: [String:Any] = ["xdm" : xdm] - let body: [String:Any] = ["events": [event]] - - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw EncodingError.invalidValue(body, EncodingError.Context(codingPath: [], debugDescription: "Failed to convert data to string")) - } - - let responseData = try await sendRequest(url: "\(baseUrl)/sessionStart", body: jsonString) - - if let responseData = responseData, - let jsonResponse = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any] { - - if let error = jsonResponse["error"] as? [String: Any] { - throw NSError(domain: "MediaEdgeAPI", code: 1, userInfo: error) - } else if let errors = (jsonResponse["data"] as? [String: Any])?["errors"] as? [[String: Any]] { - throw NSError(domain: "MediaEdgeAPI", code: 1, userInfo: ["errors": errors]) - } - - if let handle = jsonResponse["handle"] as? [[String: Any]] { - let sessionIdHandle = handle.first { handleItem in - handleItem["type"] as? String == "media-analytics:new-session" - } - - if let handle = sessionIdHandle, - let payload = handle["payload"] as? [[String:Any]], - let sessionId = payload.first?["sessionId"] as? String { - self.sessionId = sessionId - print("Adobe sessionId received: \(sessionId)") - } - } - } - - if !eventQueue.isEmpty, let sessionId = sessionId { - for event in eventQueue { - await postEvent(sessionId: sessionId, path: event.path, mediaDetails: event.mediaDetails) - } - eventQueue.removeAll() - } - } catch { - print("Failed to start session. \(error.localizedDescription)") - hasSessionFailed = true - } - } - - private func maybeQueueEvent(path: String, mediaDetails: [String: Any?]) { - guard !hasSessionFailed else { return } - - if let sessionId = sessionId { - Task { @MainActor in - await postEvent(sessionId: sessionId, path: path, mediaDetails: mediaDetails) - } - } else { - eventQueue.append(QueuedEvent(path: path, mediaDetails: mediaDetails)) - } - } - - private func postEvent(sessionId: String, path: String, mediaDetails: [String: Any?]) async { - do { - var mediaCollection: [String:Any] = [:] - mediaCollection["playhead"] = 0 - if let qoeData = mediaDetails["qoeDataDetails"] as? AdobeQoeDataDetails { - mediaCollection["qoeDataDetails"] = qoeData.toJSONEncodableDictionary() - } - if let metadata = mediaDetails["customMetadata"] as? [AdobeCustomMetadataDetails] { - mediaCollection["customMetadata"] = metadata.map({ $0.toJSONEncodableDictionary() }) - } - if let advertisingPodDetails = mediaDetails["advertisingPodDetails"] as? AdobeAdvertisingPodDetails { - mediaCollection["advertisingPodDetails"] = advertisingPodDetails.toJSONEncodableDictionary() - } - if let advertisingDetails = mediaDetails["advertisingDetails"] as? AdobeAdvertisingDetails { - mediaCollection["advertisingDetails"] = advertisingDetails.toJSONEncodableDictionary() - } - if let chapterDetails = mediaDetails["chapterDetails"] as? AdobeChapterDetails { - mediaCollection["chapterDetails"] = chapterDetails.toJSONEncodableDictionary() - } - if let errorDetails = mediaDetails["errorDetails"] as? AdobeErrorDetails { - mediaCollection["errorDetails"] = errorDetails.toJSONEncodableDictionary() - } - mediaCollection["sessionID"] = sessionId - - let xdm: [String:Any] = [ - "eventType": pathToEventTypeMap[path]?.rawValue ?? path, - "timestamp": ISO8601DateFormatter().string(from: Date()), - "mediaCollection": mediaCollection - ] - let event: [String:Any] = ["xdm" : xdm] - let body: [String:Any] = ["events": [event]] - - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw EncodingError.invalidValue(body, EncodingError.Context(codingPath: [], debugDescription: "Failed to convert data to string")) - } - - print("postEvent - \(path) \(jsonString)") - - let responseData = try await sendRequest(url: "\(baseUrl)\(path)", body: jsonString) - - if let responseData = responseData, - let jsonResponse = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any] { - - if let error = jsonResponse["error"] as? [String: Any] { - print("Failed to send event. \(error)") - } else if let errors = (jsonResponse["data"] as? [String: Any])?["errors"] as? [[String: Any]] { - print("Failed to send event. \(errors)") - } - } - } catch { - print("Failed to send event. \(error.localizedDescription)") - } - } - - private func sanitisePlayhead(_ playhead: Double?) -> Int { - guard let playhead = playhead else { - return 0 - } - - if playhead.isInfinite { - // If content is live, the playhead must be the current second of the day - let now = Date().timeIntervalSince1970 - return Int(now) % 86400 - } - - return Int(playhead.rounded()) - } -} diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index 5d797cbb..90727d95 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -19,20 +19,19 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { return false } - @objc(initialize:baseUrl:configId:userAgent:debug:debugSessionId:) - func initialize(_ node: NSNumber, baseUrl: String, configId: String, userAgent: String?, debug: Bool = false, debugSessionId: String?) -> Void { - self.debug = debug + @objc(initialize:config:customIdentityMap:) + func initialize(_ node: NSNumber, config: NSDictionary, customIdentityMap: NSDictionary) -> Void { log("initialize triggered.") + + self.debug = config["debugEnabled"] as? Bool ?? false + + DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { - let connector = AdobeEdgeConnector( - player: player, - baseUrl: baseUrl, - configId: configId, - userAgent: userAgent, - debug: debug, - debugSessionId: debugSessionId - ) + let trackerConfig: [String: String] = AdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) + let customIdentityMap = customIdentityMap as? [String: Any] + let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig, customIdentityMap: customIdentityMap) + connector.setLoggingMode(self.debug ? .debug : .error) self.connectors[node] = connector self.log("added connector to view \(node)") } else { @@ -47,70 +46,50 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { self.debug = debug DispatchQueue.main.async { if let connector = self.connectors[node] { - connector.setDebug(debug) + connector.setLoggingMode(debug ? .debug : .error) } } } - @objc(setDebugSessionId:debugSessionId:) - func setDebugSessionId(_ node: NSNumber, debugSessionId: String?) -> Void { - log("setDebugSessionId triggered.") + @objc(updateMetadata:metadata:) + func updateMetadata(_ node: NSNumber, metadata: NSDictionary) -> Void { + log("updateMetadata triggered.") DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.setDebugSessionId(debugSessionId) + if let connector = self.connectors[node], + let newMetadata = metadata as? [String: Any] { + connector.updateMetadata(AdobeEdgeUtils.toStringMap(newMetadata)) } } } - @objc(updateMetadata:metadata:) - func updateMetadata(_ node: NSNumber, metadata: [NSDictionary]) -> Void { - log("updateMetadata triggered.") + @objc(setCustomIdentityMap:customIdentityMap:) + func setCustomIdentityMap(_ node: NSNumber, customIdentityMap: NSDictionary) -> Void { + log("setCustomIdentityMap triggered.") DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.updateMetadata( - metadata.flatMap { dict in - dict.map { (key, value) in - AdobeCustomMetadataDetails( - name: key as? String, - value: "\(value)" - ) - } - } - ) + if let connector = self.connectors[node], + let newCustomIdentityMap = customIdentityMap as? [String: Any] { + connector.setCustomIdentityMap(newCustomIdentityMap) } } } - @objc(setError:errorDetails:) - func setError(_ node: NSNumber, errorDetails: [String:Any]) -> Void { + @objc(setError:errorId:) + func setError(_ node: NSNumber, errorId: String) -> Void { log("setError triggered.") DispatchQueue.main.async { if let connector = self.connectors[node] { - connector.setError( - AdobeErrorDetails( - name: errorDetails["name"] as? String ?? "", - source: (errorDetails["source"] as? String ?? "") == "player" ? .player : .external - ) - ) + connector.setError(errorId) } } } @objc(stopAndStartNewSession:customMetadataDetails:) - func stopAndStartNewSession(_ node: NSNumber, customMetadataDetails: [NSDictionary]) -> Void { + func stopAndStartNewSession(_ node: NSNumber, customMetadataDetails: NSDictionary) -> Void { log("stopAndStartNewSession triggered") DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.stopAndStartNewSession( - customMetadataDetails.flatMap { dict in - dict.map { (key, value) in - AdobeCustomMetadataDetails( - name: key as? String, - value: "\(value)" - ) - } - } - ) + if let connector = self.connectors[node], + let newMetadata = customMetadataDetails as? [String: Any] { + connector.stopAndStartNewSession(AdobeEdgeUtils.toStringMap(newMetadata)) } } } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m index 7a2e6354..b9833171 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m @@ -8,27 +8,23 @@ @interface RCT_EXTERN_REMAP_MODULE(AdobeEdgeModule, THEOplayerAdobeEdgeRCTAdobeEdgeAPI, NSObject) RCT_EXTERN_METHOD(initialize:(nonnull NSNumber *)node - baseUrl:(nonnull NSString *)baseUrl - configId:(nonnull NSString *)configId - userAgent:(nullable NSString*)userAgent - debug:(BOOL)debug - debugSessionId:(nullable NSString*)debugSessionId - ) + config:(NSDictionary)config + customIdentityMap:(NSDictionary *)customIdentityMap) RCT_EXTERN_METHOD(setDebug:(nonnull NSNumber *)node debug:(BOOL)debug) -RCT_EXTERN_METHOD(setDebugSessionId:(nonnull NSNumber *)node - debugSessionId:(nullable NSString*)debugSessionId) - RCT_EXTERN_METHOD(updateMetadata:(nonnull NSNumber *)node - metadata:(NSArray *)metadata) + metadata:(NSDictionary *)metadata) + +RCT_EXTERN_METHOD(setCustomIdentityMap:(nonnull NSNumber *)node + customIdentityMap:(NSDictionary *)customIdentityMap) RCT_EXTERN_METHOD(setError:(nonnull NSNumber *)node - errorDetails:(NSDictionary *)errorDetails) + errorId:(NSString *)errorId) RCT_EXTERN_METHOD(stopAndStartNewSession:(nonnull NSNumber *)node - customMetadataDetails:(NSArray *)customMetadataDetails) + customMetadataDetails:(NSDictionary *)customMetadataDetails) RCT_EXTERN_METHOD(destroy:(nonnull NSNumber *)node) diff --git a/adobe-edge/package.json b/adobe-edge/package.json index 1075f217..133aaf44 100644 --- a/adobe-edge/package.json +++ b/adobe-edge/package.json @@ -34,7 +34,7 @@ "manifest": "node manifest.js > src/manifest.json", "prepare": "npm run manifest && npm run build", "clean": "rimraf lib android/build example/android/build example/android/app/build example/ios/build", - "generate-types": "openapi-typescript ./src/internal/media-edge/media-edge-0.1.json -o ./src/internal/media-edge/MediaEdge.d.ts" + "generate-types": "openapi-typescript src/internal/web/media-edge-0.1.json -o src/internal/web/MediaEdge.d.ts" }, "keywords": [ "react-native", @@ -84,6 +84,10 @@ ] }, "dependencies": { + "@adobe/alloy": "^2.30.0", "openapi-fetch": "^0.9.8" + }, + "devDependencies": { + "metro-react-native-babel-preset": "^0.77.0" } } diff --git a/adobe-edge/react-native-theoplayer-adobe-edge.podspec b/adobe-edge/react-native-theoplayer-adobe-edge.podspec index 55439cde..d5f087ff 100644 --- a/adobe-edge/react-native-theoplayer-adobe-edge.podspec +++ b/adobe-edge/react-native-theoplayer-adobe-edge.podspec @@ -17,7 +17,11 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "react-native-theoplayer" - + s.dependency 'AEPCore', '~> 5.0' + s.dependency 'AEPEdge', '~> 5.0' + s.dependency 'AEPEdgeIdentity', '~> 5.0' + s.dependency 'AEPEdgeMedia', '~> 5.0' + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 9fbe2010..9426a54b 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -1,43 +1,77 @@ import type { THEOplayer } from 'react-native-theoplayer'; -import { NativeAdobeConnectorAdapter } from '../internal/NativeAdobeConnectorAdapter'; -import type { AdobeCustomMetadataDetails } from './details/AdobeCustomMetadataDetails'; -import type { AdobeErrorDetails } from './details/AdobeErrorDetails'; +import { AdobeConnectorAdapterNative } from '../internal/AdobeConnectorAdapterNative'; import { Platform } from 'react-native'; import { AdobeConnectorAdapter } from '../internal/AdobeConnectorAdapter'; -import { DefaultAdobeConnectorAdapter } from '../internal/DefaultAdobeConnectorAdapter'; +import { AdobeConnectorAdapterWeb } from '../internal/AdobeConnectorAdapterWeb'; +import { AdobeEdgeConfig } from './AdobeEdgeConfig'; +import { AdobeIdentityMap } from './details/AdobeIdentityMap'; +import { AdobeMetadata } from './details/AdobeMetadata'; export class AdobeConnector { - private connectorAdapter: AdobeConnectorAdapter; + private connectorAdapter?: AdobeConnectorAdapter; - constructor( - player: THEOplayer, - baseUrl: string, - dataStreamId: string, - userAgent?: string, - useDebug?: boolean, - debugSessionId?: string, - useNative: boolean = false, - ) { - // By default, use a default typescript connector on all platforms, unless explicitly requested. - if (['ios', 'android'].includes(Platform.OS) && useNative) { - this.connectorAdapter = new NativeAdobeConnectorAdapter(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId); + /** + * Creates an instance of AdobeConnector. + */ + constructor(player: THEOplayer, config: AdobeEdgeConfig, customIdentityMap?: AdobeIdentityMap) { + if (['ios', 'android'].includes(Platform.OS)) { + if (config.mobile) { + this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobile, customIdentityMap); + } else { + console.error('AdobeConnector Error: Missing config for mobile platform'); + } } else { - this.connectorAdapter = new DefaultAdobeConnectorAdapter(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId); + if (config.web) { + this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.web, customIdentityMap); + } else { + console.error('AdobeConnector Error: Missing config for Web platform'); + } } } /** * Sets customMetadataDetails which will be passed for the session start request. */ - updateMetadata(customMetadataDetails: AdobeCustomMetadataDetails[]): void { - this.connectorAdapter.updateMetadata(customMetadataDetails); + updateMetadata(customMetadataDetails: AdobeMetadata): void { + this.connectorAdapter?.updateMetadata(customMetadataDetails); + } + + /** + * Sets custom identity map. + * + * @example + * ```typescript + * { + * "EMAIL": [ + * { + * "id": "user@example.com", + * "authenticatedState": "authenticated", + * "primary": "false" + * }, + * { + * "id" : "useralias@example.com", + * "authenticatedState": "ambiguous", + * "primary": false + * } + * ], + * "Email_LC_SHA256": [ + * { + * "id": "2394509340-9b942f32f709db2c57e79cecec4462836ca1efef1c336a939c4b1674bcc74320", + * "authenticatedState": "authenticated", + * "primary": "false" + * } + * ] + * } + */ + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this.connectorAdapter?.setCustomIdentityMap(customIdentityMap); } /** * Dispatch error event to adobe */ - setError(errorDetails: AdobeErrorDetails): void { - this.connectorAdapter.setError(errorDetails); + setError(errorId: string): void { + this.connectorAdapter?.setError(errorId); } /** @@ -46,14 +80,7 @@ export class AdobeConnector { * @param debug whether to write debug info or not. */ setDebug(debug: boolean) { - this.connectorAdapter.setDebug(debug); - } - - /** - * Set a debugSessionID query parameter that is added to all outgoing requests. - */ - setDebugSessionId(id: string | undefined) { - this.connectorAdapter.setDebugSessionId(id); + this.connectorAdapter?.setDebug(debug); } /** @@ -64,14 +91,14 @@ export class AdobeConnector { * * @param customMetadataDetails media details information. */ - stopAndStartNewSession(customMetadataDetails: AdobeCustomMetadataDetails[]): void { - void this.connectorAdapter.stopAndStartNewSession(customMetadataDetails); + stopAndStartNewSession(customMetadataDetails: AdobeMetadata): void { + void this.connectorAdapter?.stopAndStartNewSession(customMetadataDetails); } /** * Stops video and ad analytics and closes all sessions. */ destroy(): void { - void this.connectorAdapter.destroy(); + void this.connectorAdapter?.destroy(); } } diff --git a/adobe-edge/src/api/AdobeEdgeConfig.ts b/adobe-edge/src/api/AdobeEdgeConfig.ts new file mode 100644 index 00000000..93f460b2 --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeConfig.ts @@ -0,0 +1,14 @@ +import { AdobeEdgeMobileConfig } from './AdobeEdgeMobileConfig'; +import { AdobeEdgeWebConfig } from './AdobeEdgeWebConfig'; + +export interface AdobeEdgeConfig { + /** + * Configuration for Adobe Edge on mobile platforms. + */ + mobile?: AdobeEdgeMobileConfig; + + /** + * Configuration for Adobe Edge on web platforms. + */ + web?: AdobeEdgeWebConfig; +} diff --git a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts new file mode 100644 index 00000000..8c619e55 --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts @@ -0,0 +1,16 @@ +export interface AdobeEdgeMobileConfig { + /** + * The unique environment ID that the SDK uses to retrieve your configuration. + * This ID is generated when an app configuration is created and published to a given environment. + * The app is first launched and then the SDK retrieves and uses this Adobe-hosted configuration. + * {@link https://developer.adobe.com/client-sdks/edge/media-for-edge-network/#initialize-adobe-experience-platform-sdk-with-media-for-edge-network-extension} + */ + environmentId?: string; + + /** + * The debugEnabled property allows you to enable or disable debugging using SDK code. + * + * @default `false`. + */ + debugEnabled?: boolean; +} diff --git a/adobe-edge/src/api/AdobeEdgeWebConfig.ts b/adobe-edge/src/api/AdobeEdgeWebConfig.ts new file mode 100644 index 00000000..ccae1df2 --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeWebConfig.ts @@ -0,0 +1,228 @@ +export type AutoCollect = 'always' | 'never' | 'decoratedElementsOnly'; + +export type Context = 'web' | 'device' | 'placeContext' | 'timestamp' | 'highEntropyUserAgentHints'; + +export type Consent = 'in' | 'out' | 'pending'; + +export interface AdobeEdgeWebConfig { + /** + * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want + * to send data to. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid} + */ + datastreamId: string; + + /** + * The orgId property is a string that tells Adobe which organization that data is sent to. This property is + * required for all data sent using the SDK. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid} + */ + orgId: string; + + /** + * The edgeBasePath property alters the destination path when you interact with Adobe services. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath} + */ + edgeBasePath: string; + + /** + * The autoCollectPropositionInteractions property is an optional setting that determines if the SDK automatically + * collects proposition interactions. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/autocollectpropositioninteractions} + */ + autoCollectPropositionInteractions?: { + /** + * Adobe Journey Optimizer. + */ + AJO?: AutoCollect; + /** + * Adobe Target. + */ + TGT?: AutoCollect; + }; + + /** + * The clickCollection object contains several variables that help you control automatically collected link data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/clickcollection} + */ + clickCollection?: { + /** + * Determines if links within the current domain are automatically tracked. + */ + internalLinkEnabled?: boolean; + + /** + * Determines if the library tracks links that qualify as downloads based on the downloadLinkQualifier property. + */ + downloadLinkEnabled?: boolean; + + /** + * Determines if links to external domains are automatically tracked. + */ + externalLinkEnabled?: boolean; + + /** + * Determines if the library waits until the next “page view” event to send link tracking data. + */ + eventGroupingEnabled?: boolean; + + /** + * Determines if link tracking data is stored in session storage instead of local variables. + */ + sessionStorageEnabled?: boolean; + + /** + * A callback function that provides full controls over link tracking data that you collect. + */ + filterClickDetails?: (content: object) => void; + }; + + /** + * The clickCollectionEnabled property is a boolean that determines if the SDK automatically collects link data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/clickcollectionenabled} + * + * @default `true`. + */ + clickCollectionEnabled?: boolean; + + /** + * The context property is an array of strings that determines what the SDK can automatically collect. + */ + context?: Context[]; + + /** + * The debugEnabled property allows you to enable or disable debugging using SDK code. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/debugenabled} + * + * @default `false`. + */ + debugEnabled?: boolean; + + /** + * The defaultConsent property determines how you handle data collection consent before you call the setConsent + * command. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/defaultconsent} + * + * @default `in`. + */ + defaultConsent?: Consent; + + /** + * If you enable automatic link tracking using {@link clickCollectionEnabled}, the downloadLinkQualifier property + * helps determine the criteria for a URL to be considered a download link. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/downloadlinkqualifier} + * + * @default `'\.(exe|zip|wav|mp3|mov|mpg|avi|wmv|pdf|doc|docx|xls|xlsx|ppt|pptx)$'`. + */ + downloadLinkQualifier?: string; + + /** + * The edgeConfigOverrides object allows you to override configuration settings for commands run on the current page. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgeconfigoverrides} + */ + edgeConfigOverrides?: object; + + /** + * The edgeDomain property allows you to change the domain where the SDK sends data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgedomain} + * + * @default `'edge.adobedc.net'`. + */ + edgeDomain?: string; + + /** + * The idMigrationEnabled property allows the SDK to read AMCV cookies set by previous Adobe Experience Cloud + * implementations. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/idmigrationenabled} + * + * @default `true`. + */ + idMigrationEnabled?: boolean; + + /** + * The onBeforeEventSend callback allows you to register a JavaScript function that can alter the data you send just + * before that data is sent to Adobe. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/onbeforeeventsend} + */ + onBeforeEventSend?: (content: object) => void; + + /** + * The prehidingStyle property allows you to define a CSS selector to hide personalized content until it loads. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/prehidingstyle} + */ + prehidingStyle?: string; + + /** + * The pushNotifications property lets you configure push notifications for web applications. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/pushnotifications} + */ + pushNotifications?: { + /** + * The VAPID public key used for push subscriptions. Must be a Base64-encoded string. + */ + vapidPublicKey: string; + + /** + * The application ID associated with the VAPID public key. + */ + applicationId: string; + + /** + * The system dataset ID used for push notification tracking. + */ + trackingDatasetId: string; + }; + + /** + * The streamingMedia component helps you collect data related to media sessions on your website. + *{@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} + */ + streamingMedia?: { + /** + * The name of the channel where streaming media collection occurs. + */ + channel?: string; + + /** + * The name of the media player. + */ + playerName?: string; + + /** + * The version of the media player application. + */ + appVersion?: string; + + /** + * Frequency of pings for main content, in seconds. + * + * @default `10`. + */ + mainPingInterval?: number; + + /** + * Frequency of pings for ad content, in seconds. + * + * @default `10`. + */ + adPingInterval?: number; + }; + + /** + * The targetMigrationEnabled property is a boolean that allows the SDK to read and write the mbox and + * mboxEdgeCluster cookies that the Adobe Target 1.x and 2.x libraries use. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/targetmigrationenabled} + * + * @default `false`. + */ + targetMigrationEnabled?: boolean; + + /** + * The thirdPartyCookiesEnabled property is a boolean that determines if the SDK sets cookies in a third-party + * context. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/thirdpartycookiesenabled} + * + * @default `true`. + */ + thirdPartyCookiesEnabled?: boolean; +} diff --git a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts deleted file mode 100644 index a7d66c3b..00000000 --- a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Advertising details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md - */ -export interface AdobeAdvertisingDetails { - // ID of the ad. Any integer and/or letter combination. - ID?: string; - - // Company/Brand whose product is featured in the ad. - advertiser?: string; - - // ID of the ad campaign. - campaignID?: string; - - // ID of the ad creative. - creativeID?: string; - - // URL of the ad creative. - creativeURL?: string; - - // Ad is completed. - isCompleted?: boolean; - - // Ad is started. - isStarted?: boolean; - - // Length of video ad in seconds. - length: number; - - // Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - name: string; - - // Placement ID of the ad. - placementID?: string; - - // The name of the player responsible for rendering the ad. - playerName: string; - - // The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has - // index 1. - podPosition: number; - - // ID of the ad site. - siteID?: string; - - // The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - timePlayed?: number; -} diff --git a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts deleted file mode 100644 index ebaf57d3..00000000 --- a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Advertising Pod details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md - */ -export interface AdobeAdvertisingPodDetails { - // The ID of the ad break. - ID?: string; - - // The friendly name of the Ad Break. - friendlyName?: string; - - // The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad - // has index 1. - index: number; - - // The offset of the ad break inside the content, in seconds. - offset: number; -} diff --git a/adobe-edge/src/api/details/AdobeChapterDetails.ts b/adobe-edge/src/api/details/AdobeChapterDetails.ts deleted file mode 100644 index 08550d1e..00000000 --- a/adobe-edge/src/api/details/AdobeChapterDetails.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Chapter details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md - */ -export interface AdobeChapterDetails { - // The ID of the ad break. - ID?: string; - - // The friendly name of the Ad Break. - friendlyName?: string; - - // The position (index, integer) of the chapter inside the content. - index: number; - - // Chapter is completed. - isCompleted?: boolean; - - // Chapter is started. - isStarted?: boolean; - - // The length of the chapter, in seconds. - length: number; - - // The offset of the chapter inside the content (in seconds) from the start. - offset: number; - - // The time spent on the chapter, in seconds. - timePlayed?: number; -} diff --git a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts b/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts deleted file mode 100644 index 16fec641..00000000 --- a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Custom metadata details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md - */ -export type AdobeCustomMetadataDetails = { - // The name of the custom field. - name?: string; - - // The value of the custom field. - value?: string; -}; diff --git a/adobe-edge/src/api/details/AdobeErrorDetails.ts b/adobe-edge/src/api/details/AdobeErrorDetails.ts deleted file mode 100644 index 25512689..00000000 --- a/adobe-edge/src/api/details/AdobeErrorDetails.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Error details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md - */ -export interface AdobeErrorDetails { - // The error ID. - name: string; - - // The error source. - source: ErrorSource; -} - -export enum ErrorSource { - PLAYER = 'player', - EXTERNAL = 'external', -} diff --git a/adobe-edge/src/api/details/AdobeIdentityItem.ts b/adobe-edge/src/api/details/AdobeIdentityItem.ts new file mode 100644 index 00000000..3ab6569f --- /dev/null +++ b/adobe-edge/src/api/details/AdobeIdentityItem.ts @@ -0,0 +1,35 @@ +/** + * The state this identity is authenticated as for this observed ExperienceEvent. + * + * - `ambiguous`: Ambiguous. + * - `authenticated`: User identified by a login or similar action that was valid at the time of the event observation. + * - `loggedOut`: User was identified by a login action at some point of time previously, but is not currently logged in. + */ +export type AuthenticatedState = 'ambiguous' | 'authenticated' | 'loggedOut'; + +/** + * An end user identity item, to be included in an instance of `context/identitymap`. + * + * {@link https://github.com/adobe/xdm/blob/master/components/datatypes/identityitem.schema.json} + */ +export interface AdobeIdentityItem { + /** + * Identity of the consumer in the related namespace. + */ + id: string; + + /** + * The state this identity is authenticated as for this observed ExperienceEvent. + * + * @default `ambiguous`. + */ + authenticatedState: AuthenticatedState; + + /** + * Indicates this identity is the preferred identity. Is used as a hint to help systems better organize how + * identities are queried. + * + * @default `false`. + */ + primary: boolean; +} diff --git a/adobe-edge/src/api/details/AdobeIdentityMap.ts b/adobe-edge/src/api/details/AdobeIdentityMap.ts new file mode 100644 index 00000000..bb68ef13 --- /dev/null +++ b/adobe-edge/src/api/details/AdobeIdentityMap.ts @@ -0,0 +1,10 @@ +import { AdobeIdentityItem } from './AdobeIdentityItem'; + +/** + * Defines a map containing a set of end user identities, keyed on either namespace integration code or the + * namespace ID of the identity. The values of the map are an array, meaning that more than one identity of each + * namespace may be carried. Use identityMap if bringing in data from systems having identities stored in a map structure. + * + * {@link https://github.com/adobe/xdm/blob/master/components/fieldgroups/shared/identitymap.schema.json} + */ +export type AdobeIdentityMap = { [namespace: string]: AdobeIdentityItem[] }; diff --git a/adobe-edge/src/api/details/AdobeImplementationDetails.ts b/adobe-edge/src/api/details/AdobeImplementationDetails.ts deleted file mode 100644 index 5242d3b3..00000000 --- a/adobe-edge/src/api/details/AdobeImplementationDetails.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Details about the SDK, library, or service used in an application or web page implementation of a service. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md - */ -export interface AdobeImplementationDetails { - // The environment of the implementation - environment?: AdobeEnvironment; - - // SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - name?: string; - - // The version identifier of the API, e.g h.18. - version?: string; -} - -/** - * The environment of the implementation. - */ -export enum AdobeEnvironment { - BROWSER = 'browser', - APP = 'app', - SERVER = 'server', - IOT = 'iot', -} diff --git a/adobe-edge/src/api/details/AdobeMediaDetails.ts b/adobe-edge/src/api/details/AdobeMediaDetails.ts deleted file mode 100644 index 357690e4..00000000 --- a/adobe-edge/src/api/details/AdobeMediaDetails.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AdobeSessionDetails } from './AdobeSessionDetails'; -import type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; -import type { AdobeCustomMetadataDetails } from './AdobeCustomMetadataDetails'; -import type { AdobeAdvertisingDetails } from './AdobeAdvertisingDetails'; -import type { AdobeAdvertisingPodDetails } from './AdobeAdvertisingPodDetails'; -import type { AdobeChapterDetails } from './AdobeChapterDetails'; -import type { AdobeErrorDetails } from './AdobeErrorDetails'; -import type { AdobePlayerStateData } from './AdobePlayerStateData'; - -/** - * Media details information. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md - */ -export interface AdobeMediaDetails { - // If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - // If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - playhead?: number; - - // Identifies an instance of a content stream unique to an individual playback. - sessionID?: string; - - // Session details information related to the experience event. - sessionDetails?: AdobeSessionDetails; - - // Advertising details information related to the experience event. - advertisingDetails?: AdobeAdvertisingDetails; - - // Advertising Pod details information - advertisingPodDetails?: AdobeAdvertisingPodDetails; - - // Chapter details information related to the experience event. - chapterDetails?: AdobeChapterDetails; - - // Error details information related to the experience event. - errorDetails?: AdobeErrorDetails; - - // Qoe data details information related to the experience event. - qoeDataDetails?: AdobeQoeDataDetails; - - // The list of states start. - statesStart?: AdobePlayerStateData[]; - - // The list of states end. - statesEnd?: AdobePlayerStateData[]; - - // The list of states. - states?: AdobePlayerStateData[]; - - // The list of custom metadata. - customMetadata?: AdobeCustomMetadataDetails[]; -} diff --git a/adobe-edge/src/api/details/AdobeMetadata.ts b/adobe-edge/src/api/details/AdobeMetadata.ts new file mode 100644 index 00000000..04cc7197 --- /dev/null +++ b/adobe-edge/src/api/details/AdobeMetadata.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AdobeMetadata = { [key: string]: any }; diff --git a/adobe-edge/src/api/details/AdobePlayerStateData.ts b/adobe-edge/src/api/details/AdobePlayerStateData.ts deleted file mode 100644 index bacb3d3c..00000000 --- a/adobe-edge/src/api/details/AdobePlayerStateData.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Player state data information. - * - * https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json - */ -export interface AdobePlayerStateData { - // The name of the player state. - name: string; - - // Whether or not the player state is set on that state. - isSet?: boolean; - - // The number of times that player state was set on the stream. - count?: number; - - // he total duration of that player state. - time?: number; -} diff --git a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts b/adobe-edge/src/api/details/AdobeQoeDataDetails.ts deleted file mode 100644 index 63cc2894..00000000 --- a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Qoe data details information related to the experience event. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md - */ -export interface AdobeQoeDataDetails { - // The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. The Average Bitrate is - // computed as a weighted average of all bitrate values related to the play duration that occurred during a playback - // session. - bitrateAverage?: string; - - // The bitrate value (in kbps). - bitrate?: number; - - // The average bitrate (in kbps, integer). This metric is computed as a weighted average of all bitrate - // values related to the play duration that occurred during a playback session. - bitrateAverageBucket?: number; - - // The number of streams in which bitrate changes occurred. This metric is set to true only if at least one bitrate - // change event occurred during a playback session. - hasBitrateChangeImpactedStreams?: boolean; - - // The number of bitrate changes. This value is computed as a sum of all bitrate change events that occurred - // during a playback session. - bitrateChangeCount?: number; - - // The number of streams in which frames were dropped. This metric is set to true only if at least one frame was - // dropped during a playback session. - hasDroppedFrameImpactedStreams?: boolean; - - // The number of frames dropped during playback of the main content. - droppedFrames?: number; - - // The number of times a user quit the video before its start. This metric is set to true only if no content was - // rendered, regardless of ads. - isDroppedBeforeStart?: boolean; - - // The current value of the stream frame-rate (in frames per second). The field is mapped to the fps field on the - // close call and can be accessed through processing rules. - framesPerSecond?: number; - - // Describes the duration (in seconds) passed between video load and start. - timeToStart?: number; - - // The number of streams impacted by buffering. This metric is set to true only if at least one buffer event - // occurred during a playback session. - hasBufferImpactedStreams?: boolean; - - // The number of buffer events. This metric is computed as a count of the different buffer states that occurred - // during a playback session. This is a count of how many times the player enters a buffer state from other states, - // e.g., playing or pausing. - bufferCount?: number; - - // The total amount of time, in seconds, spent buffering. This value is computed as a sum of all buffer events - // durations that occurred during a playback session. - bufferTime?: number; - - // The number of streams in which an error event occurred (i.e., trackError was called during the playback session, - // and a type=error heartbeat call was generated). This metric is set to true only if at least one error occurred - // during playback. - hasErrorImpactedStreams?: boolean; - - // The number of errors that occurred (Integer). This value is computed as a sum of all error events that occurred - // during a playback session. - errorCount?: number; - - // The number of streams in which a stalled event occurred. This metric is set to true only if at least one stall - // occurred during playback. - hasStallImpactedStreams?: boolean; - - // The number of times the playback was stalled during a playback session. - stallCount?: number; - - // The total time (seconds; integer) the playback was stalled during a playback session. - stallTime?: number; - - // The unique error IDs generated by the player SDK. Customers must provide the error codes/ids at implementation - // time via provided error APIs. - playerSdkErrors?: string[]; - - // The unique error IDs from any external source, e.g., CDN errors. Customers must provide the error codes/ids at - // implementation time via provided error APIs. - externalErrors?: string[]; - - // The unique error IDs generated by Media SDK during playback. - mediaSdkErrors?: string[]; -} diff --git a/adobe-edge/src/api/details/AdobeSessionDetails.ts b/adobe-edge/src/api/details/AdobeSessionDetails.ts deleted file mode 100644 index 184b082d..00000000 --- a/adobe-edge/src/api/details/AdobeSessionDetails.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Session details information related to the experience event. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md - */ -export interface AdobeSessionDetails { - // This identifies an instance of a content stream unique to an individual playback. - ID?: string; - - // The number of ads started during the playback. - adCount?: number; - - // The type of ad loaded as defined by each customer's internal representation. - adLoad?: string; - - // The name of the album that the music recording or video belongs to. - album?: string; - - // The SDK version used by the player. This could have any custom value that makes sense for your player. - appVersion?: string; - - // The name of the album artist or group performing the music recording or video. - artist?: string; - - // This is the unique identifier for the content of the media asset, such as the TV series episode identifier, - // movie asset identifier, or live event identifier. Typically, these IDs are derived from metadata authorities such - // as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. - assetID?: string; - - // Name of the media author. - author?: string; - - // Describes the average content time spent for a specific media item - i.e. the total content time spent divided - // by the length for all the playback sessions. - averageMinuteAudience?: number; - - // Distribution channel from where the content was played. - channel: string; - - // The number of chapters started during the playback. - chapterCount?: number; - - // The type of the stream delivery. Available values per Stream Type: Audio: “song”, “podcast”, “audiobook”, - // “radio”; Video: “VoD”, “Live”, “Linear”, “UGC”, “DVoD”. Customers can provide custom values for this parameter. - contentType: ContentType; - - // A property that defines the time of the day when the content was broadcast or played. This could have any - // value set as necessary by customers. - dayPart?: string; - - // The number of the episode. - episode?: string; - - // The estimated number of video or audio streams per each individual content. - estimatedStreams?: number; - - // The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of - // the feed like a URL. - feed?: string; - - // The date when the content first aired on television. Any date format is acceptable, - // but Adobe recommends: YYYY-MM-DD. - firstAirDate?: string; - - // The date when the content first aired on any digital channel or platform. Any date format is acceptable but - // Adobe recommends: YYYY-MM-DD. - firstDigitalDate?: string; - - // This is the “friendly” (human-readable) name of the content. - friendlyName?: string; - - // Type or grouping of content as defined by content producer. Values should be comma delimited in variable - // implementation. - genre?: string; - - // Indicates if one or more pauses occurred during the playback of a single media item. - hasPauseImpactedStreams?: boolean; - - // Indicates that the playhead passed the 10% marker of media based on stream length. The marker is only counted - // once, even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress10?: boolean; - - // Indicates that the playhead passed the 25% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress25?: boolean; - - // Indicates that the playhead passed the 50% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress50?: boolean; - - // Indicates that the playhead passed the 75% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress75?: boolean; - - // Indicates that the playhead passed the 95% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress95?: boolean; - - // Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - hasResume?: boolean; - - // Indicates when at least one frame, not necessarily the first has been viewed. - hasSegmentView?: boolean; - - // The user has been authorized via Adobe authentication. - isAuthorized?: boolean; - - // Indicates if a timed media asset was watched to completion, this does not necessarily mean the viewer watched - // the whole video; viewer could have skipped ahead. - isCompleted?: boolean; - - // The stream was played locally on the device after being downloaded. - isDownloaded?: boolean; - - // Set to true when the hit is federated (i.e., received by the customer as part of a federated data share, - // rather than their own implementation). - isFederated?: boolean; - - // First frame of media is consumed. If the user drops during ad, buffering, etc., then there would be no - // “Content Start” event. - isPlayed?: boolean; - - // Load event for the media. (This occurs when the viewer clicks the Play button). - // This would count even if there are pre-roll ads, buffering, errors, and so on. - isViewed?: boolean; - - // Name of the record label. - label?: string; - - // Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - length: number; - - // MVPD provided via Adobe authentication. - mvpd?: string; - - // Content ID of the content, which can be used to tie back to other industry / CMS IDs. - name: string; - - // The network/channel name. - network?: string; - - // Creator of the content. - originator?: string; - - // The number of pause periods that occurred during playback. - pauseCount?: number; - - // Describes the duration in seconds in which playback was paused by the user. - pauseTime?: number; - - // Name of the content player. - playerName: string; - - // Name of the audio content publisher. - publisher?: string; - - // Rating as defined by TV Parental Guidelines. - rating?: string; - - // The season number the show belongs to. Season Series is required only if the show is part of a series. - season?: string; - - // Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment - // the session was closed. - secondsSinceLastCall?: number; - - // The interval that describes the part of the content that has been viewed in minutes. - segment?: string; - - // Program/Series Name. Program Name is required only if the show is part of a series. - show?: string; - - // The type of content for example, trailer or full episode. - showType?: string; - - // The radio station name on which the audio is played. - station?: string; - - // Format of the stream (HD, SD). - streamFormat?: string; - - // The type of the media stream. - streamType?: StreamType; - - // Sums the event duration (in seconds) for all events of type PLAY on the main content. - timePlayed?: number; - - // Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent - // watching ads. - totalTimePlayed?: number; - - // Describes the sum of the unique intervals seen by a user on a timed media asset - i.e. the length playback - // intervals viewed multiple times are only counted once. - uniqueTimePlayed?: number; -} - -/** - * The type of the stream delivery. Available values per Stream Type: Audio: “song”, “podcast”, “audiobook”, - * “radio”; Video: “VoD”, “Live”, “Linear”, “UGC”, “DVoD”. Customers can provide custom values for this parameter. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md#xdmcontenttype-known-values - */ -export enum ContentType { - // Video-on-demand - VOD = 'VOD', - - // Live streaming - LIVE = 'Live', - - // Linear playback of the media asset - LINEAR = 'Linear', - - // User-generated content - UGC = 'UGC', - - // Downloaded video-on-demand - DVOD = 'DVOD', - - // Radio show - RADIO = 'Radio', - - // Audio podcast - PODCAST = 'Podcast', - - // Audiobook - AUDIOBOOK = 'Audiobook', - - // Song - SONG = 'Song', -} - -/** - * The type of the media stream. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md#xdmstreamtype - */ -export enum StreamType { - VIDEO = 'video', - AUDIO = 'audio', -} diff --git a/adobe-edge/src/api/details/barrel.ts b/adobe-edge/src/api/details/barrel.ts index fbf5a629..2bcc1fdf 100644 --- a/adobe-edge/src/api/details/barrel.ts +++ b/adobe-edge/src/api/details/barrel.ts @@ -1,11 +1,3 @@ -export type { AdobeAdvertisingDetails } from './AdobeAdvertisingDetails'; -export type { AdobeAdvertisingPodDetails } from './AdobeAdvertisingPodDetails'; -export type { AdobeChapterDetails } from './AdobeChapterDetails'; -export type { AdobeCustomMetadataDetails } from './AdobeCustomMetadataDetails'; -export type { AdobeErrorDetails } from './AdobeErrorDetails'; -export { ErrorSource } from './AdobeErrorDetails'; -export type { AdobeImplementationDetails, AdobeEnvironment } from './AdobeImplementationDetails'; -export type { AdobeMediaDetails } from './AdobeMediaDetails'; -export type { AdobePlayerStateData } from './AdobePlayerStateData'; -export type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; -export type { AdobeSessionDetails, ContentType, StreamType } from './AdobeSessionDetails'; +export * from './AdobeIdentityMap'; +export * from './AdobeIdentityItem'; +export * from './AdobeMetadata'; diff --git a/adobe-edge/src/api/hooks/useAdobe.ts b/adobe-edge/src/api/hooks/useAdobe.ts index 225ba312..72e7c980 100644 --- a/adobe-edge/src/api/hooks/useAdobe.ts +++ b/adobe-edge/src/api/hooks/useAdobe.ts @@ -1,14 +1,12 @@ import { PlayerEventType, THEOplayer } from 'react-native-theoplayer'; import { RefObject, useEffect, useRef } from 'react'; import { AdobeConnector } from '../AdobeConnector'; +import { AdobeEdgeConfig } from '../AdobeEdgeConfig'; +import { AdobeIdentityMap } from '../details/AdobeIdentityMap'; export function useAdobe( - baseUrl: string, - dataStreamId: string, - userAgent?: string, - useDebug?: boolean, - debugSessionId?: string, - useNative: boolean = true, + config: AdobeEdgeConfig, + customIdentityMap?: AdobeIdentityMap, ): [RefObject, (player: THEOplayer | undefined) => void] { const connector = useRef(undefined); const theoPlayer = useRef(undefined); @@ -19,7 +17,7 @@ export function useAdobe( theoPlayer.current = player; if (player) { - connector.current = new AdobeConnector(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId, useNative); + connector.current = new AdobeConnector(player, config, customIdentityMap); player.addEventListener(PlayerEventType.DESTROY, onDestroy); } else { throw new Error('Invalid THEOplayer instance'); diff --git a/adobe-edge/src/api/hooks/useAdobeNative.ts b/adobe-edge/src/api/hooks/useAdobeNative.ts deleted file mode 100644 index 452ad3d1..00000000 --- a/adobe-edge/src/api/hooks/useAdobeNative.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { THEOplayer } from 'react-native-theoplayer'; -import { RefObject } from 'react'; -import { AdobeConnector } from '../AdobeConnector'; -import { useAdobe } from './useAdobe'; - -export function useAdobeNative( - baseUrl: string, - dataStreamId: string, - userAgent?: string, - useDebug?: boolean, - debugSessionId?: string, -): [RefObject, (player: THEOplayer | undefined) => void] { - return useAdobe(baseUrl, dataStreamId, userAgent, useDebug, debugSessionId, true); -} diff --git a/adobe-edge/src/index.ts b/adobe-edge/src/index.ts index aa36dcbb..76a743d4 100644 --- a/adobe-edge/src/index.ts +++ b/adobe-edge/src/index.ts @@ -1,5 +1,7 @@ export { AdobeConnector } from './api/AdobeConnector'; +export type { AdobeEdgeConfig } from './api/AdobeEdgeConfig'; +export * from './api/AdobeEdgeMobileConfig'; +export * from './api/AdobeEdgeWebConfig'; export * from './api/details/barrel'; export { useAdobe } from './api/hooks/useAdobe'; -export { useAdobeNative } from './api/hooks/useAdobeNative'; export { sdkVersions } from './internal/version/Version'; diff --git a/adobe-edge/src/internal/AdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapter.ts index 20239b26..ff608685 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapter.ts @@ -1,15 +1,15 @@ -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; export interface AdobeConnectorAdapter { setDebug(debug: boolean): void; - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void; + updateMetadata(metadata: AdobeMetadata): void; - setError(metadata: AdobeErrorDetails): void; + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void; - setDebugSessionId(id: string | undefined): void; + setError(errorId: string): void; - stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise; + stopAndStartNewSession(metadata?: AdobeMetadata): Promise; destroy(): Promise; } diff --git a/adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts similarity index 68% rename from adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts rename to adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index 8161d7bd..d35b5b7f 100644 --- a/adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -1,25 +1,19 @@ import type { NativeHandleType, THEOplayer } from 'react-native-theoplayer'; import { NativeModules } from 'react-native'; -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; +import { AdobeEdgeMobileConfig } from '../api/AdobeEdgeMobileConfig'; const TAG = 'AdobeEdgeConnector'; const ERROR_MSG = 'AdobeConnectorAdapter Error'; -export class NativeAdobeConnectorAdapter implements AdobeConnectorAdapter { +export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { private readonly nativeHandle: NativeHandleType; - constructor( - player: THEOplayer, - baseUrl: string, - configId: string, - userAgent?: string, - debug = false, - debugSessionId: string | undefined = undefined, - ) { + constructor(player: THEOplayer, config: AdobeEdgeMobileConfig, customIdentityMap?: AdobeIdentityMap) { this.nativeHandle = player.nativeHandle || -1; try { - NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, baseUrl, configId, userAgent, debug, debugSessionId); + NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, config, customIdentityMap); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } @@ -33,7 +27,7 @@ export class NativeAdobeConnectorAdapter implements AdobeConnectorAdapter { } } - updateMetadata(metadata: AdobeCustomMetadataDetails[]) { + updateMetadata(metadata: AdobeMetadata) { try { NativeModules.AdobeEdgeModule.updateMetadata(this.nativeHandle || -1, metadata); } catch (error: unknown) { @@ -41,23 +35,23 @@ export class NativeAdobeConnectorAdapter implements AdobeConnectorAdapter { } } - setError(errorDetails: AdobeErrorDetails) { + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { try { - NativeModules.AdobeEdgeModule.setError(this.nativeHandle || -1, errorDetails); + NativeModules.AdobeEdgeModule.setCustomIdentityMap(this.nativeHandle || -1, customIdentityMap); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } } - setDebugSessionId(id: string | undefined) { + setError(errorId: string) { try { - NativeModules.AdobeEdgeModule.setDebugSessionId(this.nativeHandle || -1, id); + NativeModules.AdobeEdgeModule.setError(this.nativeHandle || -1, errorId); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } } - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { + async stopAndStartNewSession(metadata?: AdobeMetadata) { try { NativeModules.AdobeEdgeModule.stopAndStartNewSession(this.nativeHandle || -1, metadata ?? []); } catch (error: unknown) { diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts new file mode 100644 index 00000000..1bb916d3 --- /dev/null +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -0,0 +1,38 @@ +import type { THEOplayer } from 'react-native-theoplayer'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; +import { AdobeEdgeWebConfig } from '../api/AdobeEdgeWebConfig'; +import { AdobeEdgeConnector } from './web/AdobeEdgeConnector'; +import { ChromelessPlayer } from 'theoplayer'; + +export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { + private connector: AdobeEdgeConnector; + + constructor(player: THEOplayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { + this.connector = new AdobeEdgeConnector(player.nativeHandle as ChromelessPlayer, config, customIdentityMap); + } + + setDebug(debug: boolean) { + this.connector.setDebug(debug); + } + + updateMetadata(metadata: AdobeMetadata): void { + this.connector.updateMetadata(metadata); + } + + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this.connector?.setCustomIdentityMap(customIdentityMap); + } + + setError(errorId: string): void { + void this.connector.setError(errorId); + } + + async stopAndStartNewSession(metadata?: AdobeMetadata): Promise { + this.connector.stopAndStartNewSession(metadata); + } + + async destroy(): Promise { + return this.connector.destroy(); + } +} diff --git a/adobe-edge/src/internal/AdobeIdentityMap.ts b/adobe-edge/src/internal/AdobeIdentityMap.ts deleted file mode 100644 index 625cde92..00000000 --- a/adobe-edge/src/internal/AdobeIdentityMap.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Defines a map containing a set of end user identities, keyed on either namespace integration code or the namespace - * ID of the identity. The values of the map are an array, meaning that more than one identity of each namespace may - * be carried. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/mixins/shared/identitymap.schema.md - */ -export type AdobeIdentityMap = { [key: string]: object[] }; diff --git a/adobe-edge/src/internal/AdobeRequestBody.ts b/adobe-edge/src/internal/AdobeRequestBody.ts deleted file mode 100644 index 2cb1336f..00000000 --- a/adobe-edge/src/internal/AdobeRequestBody.ts +++ /dev/null @@ -1,107 +0,0 @@ -export interface AdobeRequestBody { - events?: { - xdm: { - mediaCollection: { - playhead: number; - sessionDetails: { - adLoad?: string; - /** @description The SDK version used by the player. This could have any custom value that makes sense for your player */ - appVersion?: string; - artist?: string; - /** @description Rating as defined by TV Parental Guidelines */ - rating?: string; - /** @description Program/Series Name. Program Name is required only if the show is part of a series. */ - show?: string; - /** @description Distribution Station/Channels or where the content is played. Any string value is accepted here */ - channel: string; - /** @description The number of the episode */ - episode?: string; - /** @description Creator of the content */ - originator?: string; - /** @description The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD */ - firstAirDate?: string; - /** - * @description Identifies the stream type - * @enum {string} - */ - streamType?: 'audio' | 'video'; - /** @description The user has been authorized via Adobe authentication */ - authorized?: string; - hasResume?: boolean; - /** @description Format of the stream (HD, SD) */ - streamFormat?: string; - /** @description Name / ID of the radio station */ - station?: string; - /** @description Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight */ - genre?: string; - /** @description The season number the show belongs to. Season Series is required only if the show is part of a series */ - season?: string; - showType?: string; - /** @description Available values per Stream Type: Audio - "song", "podcast", "audiobook", "radio"; Video: "VoD", "Live", "Linear", "UGC", "DVoD" Customers can provide custom values for this parameter */ - contentType: string; - /** @description This is the "friendly" (human-readable) name of the content */ - friendlyName?: string; - /** @description Name of the player */ - playerName: string; - /** @description Name of the author (of an audiobook) */ - author?: string; - album?: string; - /** @description Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds) */ - length: number; - /** @description A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers */ - dayPart?: string; - /** @description Name of the record label */ - label?: string; - /** @description MVPD provided via Adobe authentication. */ - mvpd?: string; - /** @description Type of feed */ - feed?: string; - /** @description This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. */ - assetID?: string; - /** @description Content ID of the content, which can be used to tie back to other industry / CMS IDs */ - name: string; - /** @description Name of the audio content publisher */ - publisher?: string; - /** @description The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD */ - firstDigitalDate?: string; - /** @description The network/channel name */ - network?: string; - /** @description Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played. */ - isDownloaded?: boolean; - }; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - implementationDetails?: { - version?: string; - }; - identityMap?: { - FPID?: { - id?: string; - /** - * @default ambiguous - * @enum {string} - */ - authenticatedState?: 'ambiguous' | 'authenticated' | 'loggedOut'; - primary?: boolean; - }[]; - }; - /** @default media.sessionStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; -} diff --git a/adobe-edge/src/internal/AdobeResponseBody.ts b/adobe-edge/src/internal/AdobeResponseBody.ts deleted file mode 100644 index 6892b66c..00000000 --- a/adobe-edge/src/internal/AdobeResponseBody.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface AdobeResponse { - status: number; - body?: AdobeResponseBody; -} - -export interface AdobeResponseBody { - requestId: string; - handle?: { - type: string; - payload: { [key: string]: any }[]; - }[]; -} diff --git a/adobe-edge/src/internal/AdobeTimeSeries.ts b/adobe-edge/src/internal/AdobeTimeSeries.ts deleted file mode 100644 index 2d71409c..00000000 --- a/adobe-edge/src/internal/AdobeTimeSeries.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { AdobeIdentityMap } from './AdobeIdentityMap'; -import type { AdobeMediaDetails } from '../api/details/AdobeMediaDetails'; -import type { AdobeImplementationDetails } from '../api/details/AdobeImplementationDetails'; -import type { EventType } from './EventType'; - -/** - * Used to indicate the behavior of time partitioned semantics when composed into data schemas. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/behaviors/time-series.schema.md - */ -export interface AdobeTimeSeries { - // The primary event type for this time-series record. - eventType: EventType; - - // The time when an event or observation occurred. - timestamp: string; - - // Custom metadata details information. - mediaCollection: AdobeMediaDetails; - - // Defines a map containing a set of end user identities - identityMap?: AdobeIdentityMap; - - // Details about the SDK, library, or service used in an application or web page implementation of a service. - implementationDetails?: AdobeImplementationDetails; -} diff --git a/adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts b/adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts deleted file mode 100644 index 6d5fdb1a..00000000 --- a/adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts +++ /dev/null @@ -1,350 +0,0 @@ -import type { - Ad, - AdBreak, - AdEvent, - ErrorEvent, - LoadedMetadataEvent, - MediaTrackEvent, - TextTrackCue, - TextTrackEvent, - THEOplayer, -} from 'react-native-theoplayer'; -import { AdEventType, MediaTrackEventType, PlayerEventType, TextTrackEventType } from 'react-native-theoplayer'; -import { calculateAdvertisingPodDetails, calculateAdvertisingDetails, calculateChapterDetails, sanitiseContentLength } from '../utils/Utils'; -import { Platform } from 'react-native'; -import { MediaEdgeAPI } from './media-edge/MediaEdgeAPI'; -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; -import { ContentType } from '../api/details/AdobeSessionDetails'; -import { ErrorSource } from '../api/details/AdobeErrorDetails'; -import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; - -const TAG = 'AdobeConnector'; -const CONTENT_PING_INTERVAL = 10000; -const AD_PING_INTERVAL = 1000; - -export class DefaultAdobeConnectorAdapter implements AdobeConnectorAdapter { - private player: THEOplayer; - - /** Timer handling the ping event request */ - private pingInterval: ReturnType | undefined; - - /** Whether we are in a current session or not */ - private sessionInProgress = false; - - private adBreakPodIndex = 0; - - private adPodPosition = 1; - - private isPlayingAd = false; - - private customMetadata: AdobeCustomMetadataDetails[] = []; - - private currentChapter: TextTrackCue | undefined; - - private debug = false; - - private mediaApi: MediaEdgeAPI; - - constructor( - player: THEOplayer, - baseUrl: string, - configId: string, - userAgent?: string, - debug = false, - debugSessionId: string | undefined = undefined, - ) { - this.player = player; - this.mediaApi = new MediaEdgeAPI(baseUrl, configId, userAgent, debugSessionId); - this.debug = debug; - this.addEventListeners(); - this.logDebug('Initialized connector'); - } - - setDebug(debug: boolean) { - this.debug = debug; - } - - setDebugSessionId(id: string | undefined) { - this.mediaApi.setDebugSessionId(id); - } - - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { - this.customMetadata = [...this.customMetadata, ...metadata]; - } - - setError(errorDetails: AdobeErrorDetails): void { - void this.mediaApi.error(this.player.currentTime, errorDetails); - } - - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise { - await this.maybeEndSession(); - if (metadata !== undefined) { - this.updateMetadata(metadata); - } - await this.maybeStartSession(); - - if (this.player.paused) { - this.onPause(); - } else { - this.onPlaying(); - } - } - - private addEventListeners(): void { - this.player.addEventListener(PlayerEventType.PLAYING, this.onPlaying); - this.player.addEventListener(PlayerEventType.PAUSE, this.onPause); - this.player.addEventListener(PlayerEventType.ENDED, this.onEnded); - this.player.addEventListener(PlayerEventType.WAITING, this.onWaiting); - this.player.addEventListener(PlayerEventType.SOURCE_CHANGE, this.onSourceChange); - this.player.addEventListener(PlayerEventType.TEXT_TRACK, this.onTextTrackEvent); - this.player.addEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); - this.player.addEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); - this.player.addEventListener(PlayerEventType.ERROR, this.onError); - - this.player.addEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - - if (Platform.OS === 'web') { - window.addEventListener('beforeunload', this.onBeforeUnload); - } - } - - private removeEventListeners(): void { - this.player.removeEventListener(PlayerEventType.PLAYING, this.onPlaying); - this.player.removeEventListener(PlayerEventType.PAUSE, this.onPause); - this.player.removeEventListener(PlayerEventType.ENDED, this.onEnded); - this.player.removeEventListener(PlayerEventType.WAITING, this.onWaiting); - this.player.removeEventListener(PlayerEventType.SOURCE_CHANGE, this.onSourceChange); - this.player.removeEventListener(PlayerEventType.TEXT_TRACK, this.onTextTrackEvent); - this.player.removeEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); - this.player.removeEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); - this.player.removeEventListener(PlayerEventType.ERROR, this.onError); - - this.player.removeEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - - if (Platform.OS === 'web') { - window.removeEventListener('beforeunload', this.onBeforeUnload); - } - } - - private onLoadedMetadata = (e: LoadedMetadataEvent) => { - this.logDebug('onLoadedMetadata'); - - // NOTE: In case of a pre-roll ad: - // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; - // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, - // and again after the pre-roll with a correct duration. - void this.maybeStartSession(e.duration); - }; - - private onPlaying = () => { - this.logDebug('onPlaying'); - void this.mediaApi.play(this.player.currentTime); - }; - - private onPause = () => { - this.logDebug('onPause'); - void this.mediaApi.pause(this.player.currentTime); - }; - - private onWaiting = () => { - this.logDebug('onWaiting'); - void this.mediaApi.bufferStart(this.player.currentTime); - }; - - private onEnded = async () => { - this.logDebug('onEnded'); - await this.mediaApi.sessionComplete(this.player.currentTime); - this.reset(); - }; - - private onSourceChange = () => { - this.logDebug('onSourceChange'); - void this.maybeEndSession(); - }; - - private onMediaTrackEvent = (event: MediaTrackEvent) => { - if (event.subType === MediaTrackEventType.ACTIVE_QUALITY_CHANGED) { - const quality = Array.isArray(event.qualities) ? event.qualities[0] : event.qualities; - void this.mediaApi.bitrateChange(this.player.currentTime, { - bitrate: quality?.bandwidth ?? 0, - }); - } - }; - - private onTextTrackEvent = (event: TextTrackEvent) => { - const track = this.player.textTracks.find((track) => track.uid === event.trackUid); - if (track !== undefined && track.kind === 'chapters') { - switch (event.subType) { - case TextTrackEventType.ENTER_CUE: { - const chapterCue = event.cue; - if (this.currentChapter && this.currentChapter.endTime !== chapterCue.startTime) { - this.mediaApi.chapterSkip(this.player.currentTime); - } - const chapterDetails = calculateChapterDetails(chapterCue); - this.mediaApi.chapterStart(this.player.currentTime, chapterDetails, this.customMetadata); - this.currentChapter = chapterCue; - break; - } - case TextTrackEventType.EXIT_CUE: { - this.mediaApi.chapterComplete(this.player.currentTime); - break; - } - } - } - }; - - private onError = (error: ErrorEvent) => { - void this.mediaApi.error(this.player.currentTime, { - name: error.error.errorCode, - source: ErrorSource.PLAYER, - }); - }; - - private onAdEvent = (event: AdEvent) => { - switch (event.subType) { - case AdEventType.AD_BREAK_BEGIN: { - this.isPlayingAd = true; - this.startPinger(AD_PING_INTERVAL); - const adBreak = event.ad as AdBreak; - const podDetails = calculateAdvertisingPodDetails(adBreak, this.adBreakPodIndex); - void this.mediaApi.adBreakStart(this.player.currentTime, podDetails); - if (podDetails.index > this.adBreakPodIndex) { - this.adBreakPodIndex++; - } - break; - } - case AdEventType.AD_BREAK_END: { - this.isPlayingAd = false; - this.adPodPosition = 1; - this.startPinger(CONTENT_PING_INTERVAL); - void this.mediaApi.adBreakComplete(this.player.currentTime); - break; - } - case AdEventType.AD_BEGIN: { - const ad = event.ad as Ad; - const adDetails = calculateAdvertisingDetails(ad, this.adPodPosition); - void this.mediaApi.adStart(this.player.currentTime, adDetails, this.customMetadata); - this.adPodPosition++; - break; - } - case AdEventType.AD_END: { - void this.mediaApi.adComplete(this.player.currentTime); - break; - } - case AdEventType.AD_SKIP: { - void this.mediaApi.adSkip(this.player.currentTime); - break; - } - } - }; - - private onBeforeUnload = () => { - void this.maybeEndSession(); - }; - - private async maybeEndSession(): Promise { - this.logDebug(`maybeEndSession`); - if (this.mediaApi.hasSessionStarted()) { - await this.mediaApi.sessionEnd(this.player.currentTime); - } - this.reset(); - return Promise.resolve(); - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLengthMsec - * @private - */ - private async maybeStartSession(mediaLengthMsec?: number): Promise { - const mediaLength = this.getContentLength(mediaLengthMsec); - const hasValidSource = this.player.source !== undefined; - const hasValidDuration = isValidDuration(mediaLength); - const isPlayingAd = await this.player.ads.playing(); - - this.logDebug( - `maybeStartSession -`, - `mediaLength: ${mediaLength},`, - `hasValidSource: ${hasValidSource},`, - `hasValidDuration: ${hasValidDuration},`, - `isPlayingAd: ${isPlayingAd}`, - ); - - if (this.sessionInProgress || !hasValidSource || !hasValidDuration || isPlayingAd) { - this.logDebug('maybeStartSession - NOT started'); - return; - } - - const sessionDetails = { - ID: 'N/A', - name: this.player?.source?.metadata?.title ?? 'N/A', - channel: 'N/A', - contentType: this.getContentType(), - playerName: 'THEOplayer', - length: mediaLength, - }; - - await this.mediaApi.startSession(sessionDetails, this.customMetadata); - if (!this.mediaApi.hasSessionStarted()) { - return; - } - - this.sessionInProgress = true; - this.logDebug('maybeStartSession - STARTED', `sessionId: ${this.mediaApi.sessionId}`); - - if (!this.isPlayingAd) { - this.startPinger(CONTENT_PING_INTERVAL); - } else { - this.startPinger(AD_PING_INTERVAL); - } - } - - private startPinger(interval: number): void { - if (this.pingInterval !== undefined) { - clearInterval(this.pingInterval); - } - this.pingInterval = setInterval(() => { - void this.mediaApi.ping(this.player.currentTime); - }, interval); - } - - private getContentLength(mediaLengthMsec?: number): number { - return sanitiseContentLength(mediaLengthMsec !== undefined ? mediaLengthMsec : this.player.duration); - } - - private getContentType(): ContentType { - return this.player.duration === Infinity ? ContentType.LIVE : ContentType.VOD; - } - - reset(): void { - this.logDebug('reset'); - this.mediaApi.reset(); - this.adBreakPodIndex = 0; - this.adPodPosition = 1; - this.isPlayingAd = false; - this.sessionInProgress = false; - clearInterval(this.pingInterval); - this.pingInterval = undefined; - this.currentChapter = undefined; - } - - async destroy(): Promise { - await this.maybeEndSession(); - this.removeEventListeners(); - } - - private logDebug(message?: any, ...optionalParams: any[]) { - if (this.debug) { - console.debug(TAG, message, ...optionalParams); - } - } -} - -function isValidDuration(v: number | undefined): boolean { - return v !== undefined && !Number.isNaN(v); -} diff --git a/adobe-edge/src/internal/EventType.ts b/adobe-edge/src/internal/EventType.ts deleted file mode 100644 index 02cdd971..00000000 --- a/adobe-edge/src/internal/EventType.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum EventType { - sessionStart = 'media.sessionStart', - play = 'media.play', - ping = 'media.ping', - bitrateChange = 'media.bitrateChange', - bufferStart = 'media.bufferStart', - pauseStart = 'media.pauseStart', - adBreakStart = 'media.adBreakStart', - adStart = 'media.adStart', - adComplete = 'media.adComplete', - adSkip = 'media.adSkip', - adBreakComplete = 'media.adBreakComplete', - chapterStart = 'media.chapterStart', - chapterSkip = 'media.chapterSkip', - chapterComplete = 'media.chapterComplete', - error = 'media.error', - sessionEnd = 'media.sessionEnd', - sessionComplete = 'media.sessionComplete', - statesUpdate = 'media.statesUpdate', -} diff --git a/adobe-edge/src/internal/media-edge/MediaEdge.d.ts b/adobe-edge/src/internal/media-edge/MediaEdge.d.ts deleted file mode 100644 index 78596e9e..00000000 --- a/adobe-edge/src/internal/media-edge/MediaEdge.d.ts +++ /dev/null @@ -1,1614 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/adBreakComplete': { - /** @description Signals the completion of an ad break */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adBreakComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adBreakStart': { - /** @description Signals the start of an ad break */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - advertisingPodDetails: { - /** @description The friendly name of the Ad Break. */ - friendlyName?: string; - /** @description The index of the ad break inside the content starting at 1. */ - index: number; - /** @description The offset of the ad break inside the content, in seconds. */ - offset: number; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adBreakStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adComplete': { - /** @description Signals the completion of an ad */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adSkip': { - /** @description Signals an ad skip */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adSkip */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adStart': { - /** @description Signals the start of an ad */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - advertisingDetails: { - /** @description ID of the ad. (Any integer and/or letter combination) */ - name: string; - /** @description Company/Brand whose product is featured in the ad */ - advertiser?: string; - /** @description ID of the ad campaign */ - campaignID?: string; - /** @description ID of the ad creative */ - creativeID?: string; - /** @description URL of the ad creative */ - creativeURL?: string; - /** @description Length of video ad in seconds */ - length: number; - /** @description Placement ID of the ad */ - placementID?: string; - /** @description Friendly name of the ad */ - friendlyName?: string; - /** @description The name of the player responsible for rendering the ad */ - playerName: string; - /** @description ID of the ad site */ - siteID?: string; - /** @description The position (index) of the ad inside the parent ad break. The first ad has index 0, the second ad has index 1 etc */ - podPosition: number; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/bitrateChange': { - /** @description Sent when the bitrage changes */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.bitrateChange */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/bufferStart': { - /** @description Sent when buffering starts. Note: Because there is no bufferResume event type, it is inferred when you send a play event after bufferStart. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.bufferStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterComplete': { - /** @description Signals the completion of a chapter */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterSkip': { - /** @description Signals a chapter skip */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterSkip */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterStart': { - /** @description Signals the start of a chapter segment */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - chapterDetails: { - /** @description The offset of the chapter inside the content (in seconds) from the start */ - offset: number; - /** @description The length of the chapter, in seconds */ - length: number; - /** @description The position (index, integer) of the chapter inside the content */ - index: number; - /** @description The name of the chapter and/or segment */ - friendlyName?: string; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/error': { - /** @description Signals that an error has occurred */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - errorDetails: { - name: string; - /** @enum {string} */ - source: 'player' | 'external'; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.error */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/pauseStart': { - /** @description Sent when the user presses Pause. Because there is no resume event type, it is inferred when you send a play event after a pauseStart. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.pauseStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/ping': { - /** @description Use the Ping request during main content playback in cases when content must be sent every 10 seconds, regardless of other API events that have been sent. The first ping event should fire 10 seconds after main content playback has begun. For ad content, it must be sent every 1 second during ad tracking. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.ping */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/play': { - /** @description Sent when the player changes state to "playing" from another state, such as when the on ('Playing') callback is triggered by the player. Other states from which the player moves to "playing" include "buffering", when the user resumes from "paused", when the player recovers from an error, and during autoplay. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.play */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionComplete': { - /** @description Sent when the end of the main content is reached */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.sessionComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionEnd': { - /** @description Notifies the Media Analytics backend to immediately close the session when the user has abandoned their viewing of the content and they are unlikely to return. If you don't send a sessionEnd, an abandoned session will time-out normally (after no events are received for 10 minutes, or when no playhead movement occurs for 30 minutes), and the session is deleted by the backend. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.sessionEnd */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionStart': { - /** @description Signals the start of a new session. When the response returns, the "sessionId" must be extracted and sent for all subsequent event calls to the Edge API server. */ - post: { - parameters: { - query: { - /** @description The datastream id */ - configId: string; - }; - }; - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - sessionDetails: { - adLoad?: string; - /** @description The SDK version used by the player. This could have any custom value that makes sense for your player */ - appVersion?: string; - artist?: string; - /** @description Rating as defined by TV Parental Guidelines */ - rating?: string; - /** @description Program/Series Name. Program Name is required only if the show is part of a series. */ - show?: string; - /** @description Distribution Station/Channels or where the content is played. Any string value is accepted here */ - channel: string; - /** @description The number of the episode */ - episode?: string; - /** @description Creator of the content */ - originator?: string; - /** @description The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD */ - firstAirDate?: string; - /** - * @description Identifies the stream type - * @enum {string} - */ - streamType?: 'audio' | 'video'; - /** @description The user has been authorized via Adobe authentication */ - authorized?: string; - hasResume?: boolean; - /** @description Format of the stream (HD, SD) */ - streamFormat?: string; - /** @description Name / ID of the radio station */ - station?: string; - /** @description Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight */ - genre?: string; - /** @description The season number the show belongs to. Season Series is required only if the show is part of a series */ - season?: string; - showType?: string; - /** @description Available values per Stream Type: Audio - "song", "podcast", "audiobook", "radio"; Video: "VoD", "Live", "Linear", "UGC", "DVoD" Customers can provide custom values for this parameter */ - contentType: string; - /** @description This is the "friendly" (human-readable) name of the content */ - friendlyName?: string; - /** @description Name of the player */ - playerName: string; - /** @description Name of the author (of an audiobook) */ - author?: string; - album?: string; - /** @description Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds) */ - length: number; - /** @description A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers */ - dayPart?: string; - /** @description Name of the record label */ - label?: string; - /** @description MVPD provided via Adobe authentication. */ - mvpd?: string; - /** @description Type of feed */ - feed?: string; - /** @description This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. */ - assetID?: string; - /** @description Content ID of the content, which can be used to tie back to other industry / CMS IDs */ - name: string; - /** @description Name of the audio content publisher */ - publisher?: string; - /** @description The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD */ - firstDigitalDate?: string; - /** @description The network/channel name */ - network?: string; - /** @description Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played. */ - isDownloaded?: boolean; - }; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - implementationDetails?: { - version?: string; - }; - identityMap?: { - FPID?: { - id?: string; - /** - * @default ambiguous - * @enum {string} - */ - authenticatedState?: 'ambiguous' | 'authenticated' | 'loggedOut'; - primary?: boolean; - }[]; - }; - /** @default media.sessionStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - 'application/json': { - /** @description The request ID. */ - requestId?: string; - handle?: { - payload?: { - /** @description The session ID generated for the media session that must be added for all subsequent calls of the same session */ - sessionId?: string; - }[]; - type?: string; - /** Format: int32 */ - eventIndex?: number; - }[]; - }; - }; - }; - /** @description Multi-Status */ - 207: { - content: { - 'application/json': { - /** @description The request ID. */ - requestId?: string; - handle?: { - payload?: Record[]; - type?: string; - /** Format: int32 */ - eventIndex?: number; - }[]; - /** @description Errors generated by the upstreams configured for the datastream */ - errors?: { - type?: string; - status?: number; - title?: string; - report?: { - /** Format: int32 */ - eventIndex?: number; - report?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }[]; - }; - }; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - }; - }; - }; - }; - }; - }; - }; - '/statesUpdate': { - /** @description Signals that one or multiple states are started and/or ended */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - statesStart?: { - name: string; - }[]; - statesEnd?: { - name: string; - }[]; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.statesUpdate */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; -} - -export type webhooks = Record; - -export type components = Record; - -export type $defs = Record; - -export type external = Record; - -export type operations = Record; diff --git a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts b/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts deleted file mode 100644 index 3caa4208..00000000 --- a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { paths } from './MediaEdge'; -import createClient, { type ClientMethod } from 'openapi-fetch'; -import type { MediaType } from 'openapi-typescript-helpers'; -import type { - AdobeAdvertisingDetails, - AdobeAdvertisingPodDetails, - AdobeChapterDetails, - AdobeCustomMetadataDetails, - AdobeErrorDetails, - AdobeMediaDetails, - AdobeQoeDataDetails, - AdobeSessionDetails, -} from '@theoplayer/react-native-analytics-adobe-edge'; -import { pathToEventTypeMap } from './PathToEventTypeMap'; -import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; -import { sanitisePlayhead } from '../../utils/Utils'; -import { buildUserAgent } from '../../utils/UserAgent'; - -// eslint-disable-next-line @typescript-eslint/ban-types -type PostRequestType = ClientMethod; - -// eslint-disable-next-line @typescript-eslint/ban-types -interface MediaEdgeClient { - /** Call a POST endpoint */ - POST: PostRequestType; -} - -const TAG = 'AdobeEdge'; - -export class MediaEdgeAPI { - private readonly _client: MediaEdgeClient; - private readonly _configId: string; - private _debugSessionId: string | undefined; - private _sessionId: string | undefined; - private _hasSessionFailed: boolean; - private _eventQueue: (() => Promise)[] = []; - - constructor(baseUrl: string, configId: string, userAgent?: string, debugSessionId?: string) { - this._configId = configId; - this._debugSessionId = debugSessionId; - this._hasSessionFailed = false; - this._client = createClient({ - baseUrl, - headers: { 'User-Agent': userAgent || buildUserAgent() }, - }); - } - - setDebugSessionId(id: string | undefined) { - this._debugSessionId = id; - } - - get sessionId(): string | undefined { - return this._sessionId; - } - - hasSessionStarted(): boolean { - return !!this._sessionId; - } - - hasSessionFailed(): boolean { - return this._hasSessionFailed; - } - - reset() { - this._sessionId = undefined; - this._hasSessionFailed = false; - this._eventQueue = []; - } - - async play(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/play', { playhead, qoeDataDetails }); - } - - async pause(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/pauseStart', { playhead, qoeDataDetails }); - } - - async error(playhead: number | undefined, errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/error', { playhead, qoeDataDetails, errorDetails }); - } - - async ping(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - // Only send pings if the session has started, never queue them. - if (this.hasSessionStarted()) { - void this.postEvent('/ping', { playhead, qoeDataDetails }); - } - } - - async bufferStart(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/bufferStart', { playhead, qoeDataDetails }); - } - - async sessionComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/sessionComplete', { playhead, qoeDataDetails }); - } - - async sessionEnd(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - await this.maybeQueueEvent('/sessionEnd', { playhead, qoeDataDetails }); - this._sessionId = undefined; - } - - async statesUpdate( - playhead: number | undefined, - statesStart?: AdobePlayerStateData[], - statesEnd?: AdobePlayerStateData[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/statesUpdate', { - playhead, - qoeDataDetails, - statesStart, - statesEnd, - }); - } - - async bitrateChange(playhead: number | undefined, qoeDataDetails: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/bitrateChange', { playhead, qoeDataDetails }); - } - - async chapterSkip(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/chapterSkip', { playhead, qoeDataDetails }); - } - - async chapterStart( - playhead: number | undefined, - chapterDetails: AdobeChapterDetails, - customMetadata?: AdobeCustomMetadataDetails[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/chapterStart', { playhead, chapterDetails, customMetadata, qoeDataDetails }); - } - - async chapterComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/chapterComplete', { playhead, qoeDataDetails }); - } - - async adBreakStart(playhead: number, advertisingPodDetails: AdobeAdvertisingPodDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adBreakStart', { - playhead, - qoeDataDetails, - advertisingPodDetails, - }); - } - - async adBreakComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adBreakComplete', { playhead, qoeDataDetails }); - } - - async adStart( - playhead: number, - advertisingDetails: AdobeAdvertisingDetails, - customMetadata?: AdobeCustomMetadataDetails[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/adStart', { - playhead, - qoeDataDetails, - advertisingDetails, - customMetadata, - }); - } - - async adSkip(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adSkip', { playhead, qoeDataDetails }); - } - - async adComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adComplete', { playhead, qoeDataDetails }); - } - - private createClientParams() { - const params = { - query: { - configId: this._configId, - } as { configId: string; debugSessionID?: string }, - }; - if (this._debugSessionId) { - params.query.debugSessionID = this._debugSessionId; - } - return params; - } - - async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { - try { - const result = await this._client.POST('/sessionStart', { - params: this.createClientParams(), - body: { - events: [ - { - xdm: { - eventType: pathToEventTypeMap['/sessionStart'], - timestamp: new Date().toISOString(), - mediaCollection: { - playhead: 0, - sessionDetails, - qoeDataDetails, - customMetadata, - }, - }, - }, - ], - }, - }); - - // @ts-ignore - const error = result.error || result.data.errors; - if (error) { - // noinspection ExceptionCaughtLocallyJS - throw Error(error); - } - // @ts-ignore - this._sessionId = result.data?.handle?.find((h: { type: string }) => { - return h.type === 'media-analytics:new-session'; - })?.payload?.[0]?.sessionId; - - // empty queue - if (this._sessionId && this._eventQueue.length !== 0) { - this._eventQueue.forEach((doPostEvent) => doPostEvent()); - this._eventQueue = []; - } - } catch (e) { - console.error(TAG, `Failed to start session. ${JSON.stringify(e)}`); - this._hasSessionFailed = true; - } - } - - async maybeQueueEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { - // Do not bother queueing the event in case starting the session has failed - if (this.hasSessionFailed()) { - return; - } - const doPostEvent = () => { - return this.postEvent(path, mediaDetails); - }; - - // If the session has already started, do not queue but send it directly. - if (!this.hasSessionStarted()) { - this._eventQueue.push(doPostEvent); - } else { - return doPostEvent(); - } - } - - async postEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { - // Make sure we are positing data with a valid sessionID. - if (!this._sessionId) { - console.error(TAG, 'Invalid sessionID'); - return; - } - try { - const result = await this._client.POST(path, { - params: this.createClientParams(), - body: { - events: [ - { - xdm: { - eventType: pathToEventTypeMap[path], - timestamp: new Date().toISOString(), - mediaCollection: { - ...mediaDetails, - playhead: sanitisePlayhead(mediaDetails.playhead), - sessionID: this._sessionId, - }, - }, - }, - ], - }, - }); - // @ts-ignore - const error = result.error || result.data.errors; - if (error) { - console.error(TAG, `Failed to send event. ${JSON.stringify(error)}`); - } - } catch (e) { - console.error(TAG, `Failed to send event: ${JSON.stringify(e)}`); - } - } -} diff --git a/adobe-edge/src/internal/media-edge/PathToEventTypeMap.ts b/adobe-edge/src/internal/media-edge/PathToEventTypeMap.ts deleted file mode 100644 index 138694db..00000000 --- a/adobe-edge/src/internal/media-edge/PathToEventTypeMap.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { paths } from './MediaEdge'; -import { EventType } from '../EventType'; - -export const pathToEventTypeMap: Record = { - '/adBreakComplete': EventType.adBreakComplete, - '/adBreakStart': EventType.adBreakStart, - '/adComplete': EventType.adComplete, - '/adSkip': EventType.adSkip, - '/adStart': EventType.adStart, - '/bitrateChange': EventType.bitrateChange, - '/bufferStart': EventType.bufferStart, - '/chapterComplete': EventType.chapterComplete, - '/chapterSkip': EventType.chapterSkip, - '/chapterStart': EventType.chapterStart, - '/error': EventType.error, - '/pauseStart': EventType.pauseStart, - '/ping': EventType.ping, - '/play': EventType.play, - '/sessionComplete': EventType.sessionComplete, - '/sessionEnd': EventType.sessionEnd, - '/sessionStart': EventType.sessionStart, - '/statesUpdate': EventType.statesUpdate, -}; diff --git a/adobe-edge/src/internal/media-edge/media-edge-0.1.json b/adobe-edge/src/internal/media-edge/media-edge-0.1.json deleted file mode 100644 index c44330cc..00000000 --- a/adobe-edge/src/internal/media-edge/media-edge-0.1.json +++ /dev/null @@ -1,3688 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Media Analytics Edge API", - "description": "The OpenAPI specification for Media Analytics Edge API", - "version": "0.1" - }, - "servers": [ - { - "url": "https://edge.adobedc.net/ee-pre-prd/va/v1" - }, - { - "url": "https://edge.adobedc.net/ee/va/v1" - } - ], - "paths": { - "/adBreakComplete": { - "post": { - "description": "Signals the completion of an ad break", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adBreakComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adBreakComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adBreakStart": { - "post": { - "description": "Signals the start of an ad break", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "advertisingPodDetails": { - "type": "object", - "properties": { - "friendlyName": { - "type": "string", - "description": "The friendly name of the Ad Break." - }, - "index": { - "type": "integer", - "description": "The index of the ad break inside the content starting at 1." - }, - "offset": { - "type": "integer", - "description": "The offset of the ad break inside the content, in seconds." - } - }, - "required": ["index", "offset"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["advertisingPodDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adBreakStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adBreakStart\",\n \"mediaCollection\": {\n \"advertisingPodDetails\": {\n \"friendlyName\": \"Mid-roll\",\n \"offset\": 0,\n \"index\": 1\n },\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 15\n },\n \"timestamp\": \"2022-03-04T13:38:15+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingPodDetails.offset\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adComplete": { - "post": { - "description": "Signals the completion of an ad", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adSkip": { - "post": { - "description": "Signals an ad skip", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adSkip" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adSkip\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adStart": { - "post": { - "description": "Signals the start of an ad", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "advertisingDetails": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "ID of the ad. (Any integer and/or letter combination)" - }, - "advertiser": { - "type": "string", - "description": "Company/Brand whose product is featured in the ad" - }, - "campaignID": { - "type": "string", - "description": "ID of the ad campaign" - }, - "creativeID": { - "type": "string", - "description": "ID of the ad creative" - }, - "creativeURL": { - "type": "string", - "description": "URL of the ad creative" - }, - "length": { - "type": "integer", - "description": "Length of video ad in seconds" - }, - "placementID": { - "type": "string", - "description": "Placement ID of the ad" - }, - "friendlyName": { - "type": "string", - "description": "Friendly name of the ad" - }, - "playerName": { - "type": "string", - "description": "The name of the player responsible for rendering the ad" - }, - "siteID": { - "type": "string", - "description": "ID of the ad site" - }, - "podPosition": { - "type": "integer", - "description": "The position (index) of the ad inside the parent ad break. The first ad has index 0, the second ad has index 1 etc" - } - }, - "required": ["name", "length", "playerName", "podPosition"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["advertisingDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adStart\",\n \"mediaCollection\": {\n \"advertisingDetails\": {\n \"friendlyName\": \"Ad 1\",\n \"name\": \"/uri-reference/001\",\n \"length\": 10,\n \"advertiser\": \"Adobe Marketing\",\n \"campaignID\": \"Adobe Analytics\",\n \"creativeID\": \"creativeID\",\n \"creativeURL\": \"https://creativeurl.com\",\n \"placementID\": \"placementID\",\n \"siteID\": \"siteID\",\n \"podPosition\": 11,\n \"playerName\": \"HTML5 player\"\n },\n \"customMetadata\": [\n {\n \"name\": \"myCustomValue3\",\n \"value\": \"c3\"\n },\n {\n \"name\": \"myCustomValue2\",\n \"value\": \"c2\"\n },\n {\n \"name\": \"myCustomValue1\",\n \"value\": \"c1\"\n }\n ],\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 15\n },\n \"timestamp\": \"2022-03-04T13:38:26+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.name\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.length\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.podPosition\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.sessionID\",\n \"reason\": \"Unexpected error. Hint: InvalidApiSidLength=63\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/bitrateChange": { - "post": { - "description": "Sent when the bitrage changes", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["qoeDataDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.bitrateChange" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.bitrateChange\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 30,\n \"qoeDataDetails\": {\n \"framesPerSecond\": 1,\n \"bitrate\": 35000,\n \"droppedFrames\": 30,\n \"timeToStart\": 1364\n }\n },\n \"timestamp\": \"2022-03-04T13:38:40+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.qoeDataDetails\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/bufferStart": { - "post": { - "description": "Sent when buffering starts. Note: Because there is no bufferResume event type, it is inferred when you send a play event after bufferStart.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.bufferStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.bufferStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterComplete": { - "post": { - "description": "Signals the completion of a chapter", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterSkip": { - "post": { - "description": "Signals a chapter skip", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterSkip" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterSkip\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterStart": { - "post": { - "description": "Signals the start of a chapter segment", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "chapterDetails": { - "type": "object", - "properties": { - "offset": { - "type": "integer", - "description": "The offset of the chapter inside the content (in seconds) from the start" - }, - "length": { - "type": "integer", - "description": "The length of the chapter, in seconds" - }, - "index": { - "type": "integer", - "description": "The position (index, integer) of the chapter inside the content" - }, - "friendlyName": { - "type": "string", - "description": "The name of the chapter and/or segment" - } - }, - "required": ["index", "length", "offset"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["chapterDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 10,\n \"chapterDetails\": {\n \"friendlyName\": \"Chapter 1\",\n \"length\": 10,\n \"index\": 1,\n \"offset\": 0\n }\n },\n \"timestamp\": \"2022-03-04T13:37:56+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.chapterDetails.index\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.chapterDetails.length\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/error": { - "post": { - "description": "Signals that an error has occurred", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "errorDetails": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["player", "external"] - } - }, - "required": ["name", "source"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["errorDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.error" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.error\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 35,\n \"qoeDataDetails\": {\n \"bitrate\": 35000,\n \"droppedFrames\": 30\n },\n \"errorDetails\": {\n \"name\": \"test-buffer-start\",\n \"source\": \"player\"\n }\n },\n \"timestamp\": \"2022-03-04T13:39:15+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.errorDetails.name\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/pauseStart": { - "post": { - "description": "Sent when the user presses Pause. Because there is no resume event type, it is inferred when you send a play event after a pauseStart.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.pauseStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.pauseStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/ping": { - "post": { - "description": "Use the Ping request during main content playback in cases when content must be sent every 10 seconds, regardless of other API events that have been sent. The first ping event should fire 10 seconds after main content playback has begun. For ad content, it must be sent every 1 second during ad tracking.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.ping" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.ping\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/play": { - "post": { - "description": "Sent when the player changes state to \"playing\" from another state, such as when the on ('Playing') callback is triggered by the player. Other states from which the player moves to \"playing\" include \"buffering\", when the user resumes from \"paused\", when the player recovers from an error, and during autoplay.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.play" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.play\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionComplete": { - "post": { - "description": "Sent when the end of the main content is reached", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.sessionComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.sessionComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionEnd": { - "post": { - "description": "Notifies the Media Analytics backend to immediately close the session when the user has abandoned their viewing of the content and they are unlikely to return. If you don't send a sessionEnd, an abandoned session will time-out normally (after no events are received for 10 minutes, or when no playhead movement occurs for 30 minutes), and the session is deleted by the backend.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.sessionEnd" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.sessionEnd\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionStart": { - "post": { - "description": "Signals the start of a new session. When the response returns, the \"sessionId\" must be extracted and sent for all subsequent event calls to the Edge API server.", - "parameters": [ - { - "name": "configId", - "description": "The datastream id", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionDetails": { - "type": "object", - "properties": { - "adLoad": { - "type": "string" - }, - "appVersion": { - "type": "string", - "description": "The SDK version used by the player. This could have any custom value that makes sense for your player" - }, - "artist": { - "type": "string" - }, - "rating": { - "type": "string", - "description": "Rating as defined by TV Parental Guidelines" - }, - "show": { - "type": "string", - "description": "Program/Series Name. Program Name is required only if the show is part of a series." - }, - "channel": { - "type": "string", - "description": "Distribution Station/Channels or where the content is played. Any string value is accepted here" - }, - "episode": { - "type": "string", - "description": "The number of the episode" - }, - "originator": { - "type": "string", - "description": "Creator of the content" - }, - "firstAirDate": { - "type": "string", - "description": "The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD" - }, - "streamType": { - "type": "string", - "enum": ["audio", "video"], - "description": "Identifies the stream type" - }, - "authorized": { - "type": "string", - "description": "The user has been authorized via Adobe authentication" - }, - "hasResume": { - "type": "boolean" - }, - "streamFormat": { - "type": "string", - "description": "Format of the stream (HD, SD)" - }, - "station": { - "type": "string", - "description": "Name / ID of the radio station" - }, - "genre": { - "type": "string", - "description": "Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight" - }, - "season": { - "type": "string", - "description": "The season number the show belongs to. Season Series is required only if the show is part of a series" - }, - "showType": { - "type": "string" - }, - "contentType": { - "type": "string", - "description": "Available values per Stream Type: Audio - \"song\", \"podcast\", \"audiobook\", \"radio\"; Video: \"VoD\", \"Live\", \"Linear\", \"UGC\", \"DVoD\" Customers can provide custom values for this parameter" - }, - "friendlyName": { - "type": "string", - "description": "This is the \"friendly\" (human-readable) name of the content" - }, - "playerName": { - "type": "string", - "description": "Name of the player" - }, - "author": { - "type": "string", - "description": "Name of the author (of an audiobook)" - }, - "album": { - "type": "string" - }, - "length": { - "type": "integer", - "description": "Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds)" - }, - "dayPart": { - "type": "string", - "description": "A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers" - }, - "label": { - "type": "string", - "description": "Name of the record label" - }, - "mvpd": { - "type": "string", - "description": "MVPD provided via Adobe authentication." - }, - "feed": { - "type": "string", - "description": "Type of feed" - }, - "assetID": { - "type": "string", - "description": "This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems." - }, - "name": { - "type": "string", - "description": "Content ID of the content, which can be used to tie back to other industry / CMS IDs" - }, - "publisher": { - "type": "string", - "description": "Name of the audio content publisher" - }, - "firstDigitalDate": { - "type": "string", - "description": "The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD" - }, - "network": { - "type": "string", - "description": "The network/channel name" - }, - "isDownloaded": { - "type": "boolean", - "description": "Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played." - } - }, - "required": ["name", "playerName", "length", "channel", "contentType"] - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionDetails"] - }, - "implementationDetails": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - } - }, - "identityMap": { - "type": "object", - "properties": { - "FPID": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "authenticatedState": { - "type": "string", - "default": "ambiguous", - "enum": ["ambiguous", "authenticated", "loggedOut"] - }, - "primary": { - "type": "boolean" - } - } - } - } - } - }, - "eventType": { - "type": "string", - "default": "media.sessionStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"mediaCollection\": {\n \"sessionDetails\": {\n \"dayPart\": \"dayPart\",\n \"mvpd\": \"test-mvpd\",\n \"authorized\": \"true\",\n \"label\": \"test-label\",\n \"station\": \"test-station\",\n \"publisher\": \"test-media-publisher\",\n \"author\": \"test-author\",\n \"name\": \"Friends\",\n \"friendlyName\": \"FriendlyName\",\n \"assetID\": \"/uri-reference\",\n \"originator\": \"David Crane and Marta Kauffman\",\n \"episode\": \"4933\",\n \"genre\": \"Comedy\",\n \"rating\": \"4.8/5\",\n \"season\": \"1521\",\n \"show\": \"Friends Series\",\n \"length\": 100,\n \"firstDigitalDate\": \"releaseDate\",\n \"artist\": \"test-artist\",\n \"hasResume\": false,\n \"album\": \"test-album\",\n \"firstAirDate\": \"firstAirDate\",\n \"showType\": \"sitcom\",\n \"streamFormat\": \"streamFormat\",\n \"streamType\": \"video\",\n \"adLoad\": \"adLoadType\",\n \"channel\": \"broadcastChannel\",\n \"contentType\": \"VOD\",\n \"playerName\": \"HTML5 player\",\n \"appVersion\": \"sdk-1.0\",\n \"feed\": \"sourceFeed\",\n \"network\": \"test-network\"\n },\n \"playhead\": 0,\n \"implementationDetails\": {\n \"version\": \"libraryVersion\"\n },\n \"customMetadata\": [\n {\n \"name\": \"myCustomValue3\",\n \"value\": \"c3\"\n },\n {\n \"name\": \"myCustomValue2\",\n \"value\": \"c2\"\n },\n {\n \"name\": \"myCustomValue1\",\n \"value\": \"c1\"\n }\n ]\n },\n \"timestamp\": \"2023-04-04T11:35:16Z\",\n \"identityMap\": {\n \"FPID\": [\n {\n \"id\": \"CHANGEME\",\n \"authenticatedState\": \"ambiguous\",\n \"primary\": true\n }\n ]\n },\n \"eventType\": \"media.sessionStart\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "requestId": { - "type": "string", - "description": "The request ID." - }, - "handle": { - "type": "array", - "items": { - "type": "object", - "properties": { - "payload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sessionId": { - "type": "string", - "description": "The session ID generated for the media session that must be added for all subsequent calls of the same session" - } - } - } - }, - "type": { - "type": "string" - }, - "eventIndex": { - "type": "integer", - "format": "int32" - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"requestId\": \"dd850e05-8c3e-4ae4-9ea8-506490978004\",\n \"handle\": [\n {\n \"payload\": [\n {\n \"sessionId\": \"bfba9a5f2986d69a9a9424f6a99702562512eb244f2b65c4f1c1553e7fe9997f\"\n }\n ],\n \"type\": \"media-analytics:new-session\",\n \"eventIndex\": 0\n },\n {\n \"payload\": [\n {\n \"scope\": \"Target\",\n \"hint\": \"34\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"AAM\",\n \"hint\": \"7\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"EdgeNetwork\",\n \"hint\": \"va6\",\n \"ttlSeconds\": 1800\n }\n ],\n \"type\": \"locationHint:result\"\n },\n {\n \"payload\": [\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_cluster\",\n \"value\": \"va6\",\n \"maxAge\": 1800\n },\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_identity\",\n \"value\": \"CiY0Mzg5NTEyNzMzNTUxMDc5MzgzMzU2MjU5NDY5MTY3Mzc3MTc2OFIOCJ-YppX6MBgBKgNWQTbwAZ-YppX6MA==\",\n \"maxAge\": 34128000\n }\n ],\n \"type\": \"state:store\"\n }\n ]\n}" - } - } - } - } - }, - "207": { - "description": "Multi-Status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "requestId": { - "type": "string", - "description": "The request ID." - }, - "handle": { - "type": "array", - "items": { - "type": "object", - "properties": { - "payload": { - "type": "array", - "items": { - "type": "object" - } - }, - "type": { - "type": "string" - }, - "eventIndex": { - "type": "integer", - "format": "int32" - } - } - } - }, - "errors": { - "type": "array", - "description": "Errors generated by the upstreams configured for the datastream", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "eventIndex": { - "type": "integer", - "format": "int32" - }, - "report": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\",\n \"handle\": [\n {\n \"payload\": [\n {\n \"scope\": \"Target\",\n \"hint\": \"34\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"AAM\",\n \"hint\": \"7\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"EdgeNetwork\",\n \"hint\": \"va6\",\n \"ttlSeconds\": 1800\n }\n ],\n \"type\": \"locationHint:result\"\n },\n {\n \"payload\": [\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_cluster\",\n \"value\": \"va6\",\n \"maxAge\": 1800\n },\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_identity\",\n \"value\": \"CiY0Mzg5NTEyNzMzNTUxMDc5MzgzMzU2MjU5NDY5MTY3Mzc3MTc2OFIOCI-qtpf6MBgBKgNWQTbwAY-qtpf6MA==\",\n \"maxAge\": 34128000\n }\n ],\n \"type\": \"state:store\"\n }\n ],\n \"errors\": [\n {\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Invalid request\",\n \"report\": {\n \"eventIndex\": 0,\n \"details\": [\n {\n \"name\": \"$.xdm.mediaCollection.sessionDetails.name\",\n \"reason\": \"Missing required field\"\n }\n ]\n }\n }\n ]\n}" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/EXEG-0003-400\",\n \"status\": 400,\n \"title\": \"Invalid datastream ID\",\n \"detail\": \"The datastream ID '66b64400-e418-4184-8fed-b57636d09' referenced in your request does not exist. Update the request with a valid datastream ID and try again.\",\n \"report\": {\n \"requestId\": \"75af7733-9c8a-45a9-b2a1-bb570c58a0da\"\n }\n}" - } - } - } - } - } - } - } - }, - "/statesUpdate": { - "post": { - "description": "Signals that one or multiple states are started and/or ended", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "statesStart": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_.]{1,64}$" - } - }, - "required": ["name"] - } - }, - "statesEnd": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_.]{1,64}$" - } - }, - "required": ["name"] - } - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.statesUpdate" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.statesUpdate\",\n \"mediaCollection\": {\n \"sessionID\": \"bfba9a5f2986d69a9a9424f6a99702562512eb244f2b65c4f1c1553e7fe9997f\",\n \"playhead\": 60,\n \"statesStart\": [\n {\n \"name\": \"mute\"\n },\n {\n \"name\": \"pictureInPicture\"\n }\n ],\n \"statesEnd\": [\n {\n \"name\": \"fullScreen\"\n }\n ]\n },\n \"timestamp\": \"2022-03-04T13:40:40+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection\",\n \"reason\": \"Unexpected error. Hint: Empty statesUpdate event. At least one of statesStart or statesEnd list should be non-empty\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - } - } -} diff --git a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts new file mode 100644 index 00000000..f818396c --- /dev/null +++ b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts @@ -0,0 +1,44 @@ +import { AdobeEdgeHandler } from './AdobeEdgeHandler'; +import { AdobeEdgeWebConfig, AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; +import { ChromelessPlayer } from 'theoplayer'; + +export class AdobeEdgeConnector { + private _handler: AdobeEdgeHandler; + + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { + this._handler = new AdobeEdgeHandler(player, config, customIdentityMap); + } + + /** + * Explicitly stop the current session and start a new one. + * + * This can be used to manually mark the start of a new session during a live stream, + * for example when a new program starts. + * By default, new sessions are only started on play-out of a new source, or for an ad break. + * + * @param metadata object of key value pairs. + */ + stopAndStartNewSession(metadata?: AdobeMetadata) { + return this._handler.stopAndStartNewSession(metadata); + } + + updateMetadata(metadata: AdobeMetadata) { + this._handler.updateMetadata(metadata); + } + + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this._handler.setCustomIdentityMap(customIdentityMap); + } + + setDebug(debug: boolean) { + this._handler.setDebug(debug); + } + + setError(errorId: string) { + return this._handler.setError(errorId); + } + + destroy() { + return this._handler.destroy(); + } +} diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts new file mode 100644 index 00000000..b083c394 --- /dev/null +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -0,0 +1,494 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; +import { idToInt, isValidDuration, sanitiseChapterId, sanitiseConfig, sanitiseContentLength, sanitiseNumber, sanitisePlayhead } from './Utils'; +import { createInstance } from '@adobe/alloy'; +import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; +import { + type TextTrackCue, + TextTrack, + type ChromelessPlayer, + AdEvent, + ErrorEvent, + AddTrackEvent, + TextTrackEnterCueEvent, + RemoveTrackEvent, + MediaTrack, + QualityEvent, + AdBreakEvent, + VideoQuality, +} from 'theoplayer'; +import { EventType } from './EventType'; +import AlloyClient, { Media, MediaTracker } from './Media'; + +const TAG = 'AdobeConnector'; + +const PROP_PLAYHEAD = 'playhead'; +const PROP_ERROR_ID = 'errorId'; +const PROP_NA = 'NA'; + +/** + * Alloy globally stores clients by name. We are allowed create clients with the same config only once. + */ +interface ClientDescription { + datastreamId: string; + orgId: string; + client: AlloyClient; +} + +type EventInfo = { [key: string]: any }; +type EventMetadata = { [key: string]: string }; + +interface QueuedEvent { + type: EventType; + info: EventInfo; + metadata: EventMetadata; +} + +const createdClients: ClientDescription[] = []; + +/** + * The MediaEdgeAPI class is responsible for communicating media events to Adobe Experience Platform. + * + * Event handling for manually-tracked sessions is used. In this mode you need to pass the sessionID to the media event, + * along with the playhead value (integer value). You could also pass the Quality of Experience data details, if needed. + * + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/js-overview} + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} + */ +class AdobeEdgeHandler { + private _player: ChromelessPlayer; + + /** Whether we are in a current session or not */ + private _sessionInProgress = false; + private _adBreakPodIndex = 0; + private _adPodPosition = 1; + private _customMetadata: EventMetadata = {}; + private _customIdentityMap: AdobeIdentityMap | undefined; + private _currentChapter: TextTrackCue | undefined; + private _eventQueue: QueuedEvent[] = []; + private _debug = false; + private readonly _alloyClient: AlloyClient | undefined; + private _media: Media | undefined; + private _tracker: MediaTracker | undefined; + + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { + this._player = player; + this._customIdentityMap = customIdentityMap; + const sanitisedConfig = sanitiseConfig(config); + const { datastreamId, orgId, debugEnabled } = sanitisedConfig; + + this.addEventListeners(); + + this._alloyClient = findAlloyClient(datastreamId, orgId); + if (!this._alloyClient) { + this._alloyClient = createInstance({ + name: 'alloy', + /** + * Optional event callbacks for debugging purposes. + */ + monitors: [ + { + // onBeforeLog: (arg0: any) => void; + // onInstanceCreated: (arg0: any) => void; + // onInstanceConfigured: (arg0: any) => void; + // onBeforeCommand: (arg0: any) => void; + // onCommandResolved: (arg0: any) => void; + // onCommandRejected: (arg0: any) => void; + // onBeforeNetworkRequest: (arg0: any) => void; + // onNetworkResponse: (arg0: any) => void; + // onNetworkError: (arg0: any) => void; + // onContentHiding: (arg0: any) => void; + // onContentRendering: (arg0: any) => void; + }, + ], + }); + this._alloyClient('configure', sanitisedConfig); + + // Store created client to prevent creating duplicates. + createdClients.push({ datastreamId, orgId, client: this._alloyClient }); + } + + /** + * Acquire Media Analytics APIs & tracker. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/getmediaanalyticstracker + */ + this._alloyClient('getMediaAnalyticsTracker', {}).then((result: any) => { + this._media = result; + this._tracker = this._media?.getInstance(); + }); + + this.setDebug(debugEnabled || false); + } + + updateMetadata(metadata: AdobeMetadata) { + this._customMetadata = { ...this._customMetadata, ...metadata }; + } + + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap) { + this._customIdentityMap = customIdentityMap; + } + + setError(errorId: string) { + this.queueOrSendEvent(EventType.error, { [PROP_ERROR_ID]: errorId }); + } + + stopAndStartNewSession(metadata?: AdobeMetadata) { + this.maybeEndSession(); + if (metadata) { + this.updateMetadata(metadata); + } + this.maybeStartSession(); + if (this._player.paused) { + this.handlePause(); + } else { + this.handlePlaying(); + } + } + + private addEventListeners() { + this._player.addEventListener('playing', this.handlePlaying); + this._player.addEventListener('pause', this.handlePause); + this._player.addEventListener('ended', this.handleEnded); + this._player.addEventListener('waiting', this.handleWaiting); + this._player.addEventListener('seeking', this.handleSeeking); + this._player.addEventListener('seeked', this.handleSeeked); + this._player.addEventListener('timeupdate', this.handleTimeUpdate); + this._player.addEventListener('sourcechange', this.handleSourceChange); + this._player.textTracks.addEventListener('addtrack', this.handleAddTextTrack); + this._player.textTracks.addEventListener('removetrack', this.handleRemoveTextTrack); + this._player.videoTracks.addEventListener('addtrack', this.handleAddVideoTrack); + this._player.videoTracks.addEventListener('removetrack', this.handleRemoveVideoTrack); + this._player.addEventListener('loadedmetadata', this.onLoadedMetadata); + this._player.addEventListener('error', this.handleError); + this._player.ads?.addEventListener('adbreakbegin', this.handleAdBreakBegin); + this._player.ads?.addEventListener('adbreakend', this.handleAdBreakEnd); + this._player.ads?.addEventListener('adbegin', this.handleAdBegin); + this._player.ads?.addEventListener('adend', this.handleAdEnd); + this._player.ads?.addEventListener('adskip', this.handleAdSkip); + window.addEventListener('beforeunload', this.onBeforeUnload); + } + + private removeEventListeners() { + this._player.removeEventListener('playing', this.handlePlaying); + this._player.removeEventListener('pause', this.handlePause); + this._player.removeEventListener('ended', this.handleEnded); + this._player.removeEventListener('waiting', this.handleWaiting); + this._player.removeEventListener('seeking', this.handleSeeking); + this._player.removeEventListener('seeked', this.handleSeeked); + this._player.removeEventListener('timeupdate', this.handleTimeUpdate); + this._player.removeEventListener('sourcechange', this.handleSourceChange); + this._player.textTracks.removeEventListener('addtrack', this.handleAddTextTrack); + this._player.textTracks.removeEventListener('removetrack', this.handleRemoveTextTrack); + this._player.videoTracks.removeEventListener('addtrack', this.handleAddVideoTrack); + this._player.videoTracks.removeEventListener('removetrack', this.handleRemoveVideoTrack); + this._player.removeEventListener('loadedmetadata', this.onLoadedMetadata); + this._player.removeEventListener('error', this.handleError); + this._player.ads?.removeEventListener('adbreakbegin', this.handleAdBreakBegin); + this._player.ads?.removeEventListener('adbreakend', this.handleAdBreakEnd); + this._player.ads?.removeEventListener('adbegin', this.handleAdBegin); + this._player.ads?.removeEventListener('adend', this.handleAdEnd); + this._player.ads?.removeEventListener('adskip', this.handleAdSkip); + window.removeEventListener('beforeunload', this.onBeforeUnload); + } + + private sendEvent(eventType: EventType, info: EventInfo, metadata: EventMetadata) { + switch (eventType) { + case EventType.updatePlayhead: + this._tracker?.updatePlayhead(info[PROP_PLAYHEAD]); + break; + case EventType.error: + this._tracker?.trackError(info[PROP_ERROR_ID] || PROP_NA); + break; + case EventType.sessionComplete: + this._tracker?.trackComplete(); + break; + case EventType.sessionEnd: + this._tracker?.trackSessionEnd(); + break; + case EventType.play: + this._tracker?.trackPlay(); + break; + case EventType.pauseStart: + this._tracker?.trackPause(); + break; + default: + this._tracker?.trackEvent(eventType, info, metadata); + break; + } + } + + private queueOrSendEvent(type: EventType, info: EventInfo = {}, metadata: EventMetadata = {}) { + const extendedInfo = { ...info, [PROP_PLAYHEAD]: sanitisePlayhead(this._player.currentTime, this._player.duration) }; + if (this._sessionInProgress) { + this.sendEvent(type, extendedInfo, metadata); + } else { + this._eventQueue.push({ type, info: extendedInfo, metadata }); + } + } + + private handlePlaying = () => { + this.logDebug('onPlaying'); + this.queueOrSendEvent(EventType.play); + }; + + private handlePause = () => { + this.logDebug('onPause'); + this.queueOrSendEvent(EventType.pauseStart); + }; + + private handleTimeUpdate = () => { + this.queueOrSendEvent(EventType.updatePlayhead); + }; + + private handleWaiting = () => { + this.logDebug('onWaiting'); + this.queueOrSendEvent(EventType.bufferStart); + }; + + private handleSeeking = () => { + this.logDebug('onSeeking'); + this.queueOrSendEvent(EventType.seekStart); + }; + + private handleSeeked = () => { + this.logDebug('onSeeked'); + this.queueOrSendEvent(EventType.seekEnd); + }; + + private handleEnded = () => { + this.logDebug('onEnded'); + this.queueOrSendEvent(EventType.sessionComplete); + this.reset(); + }; + + private handleSourceChange = () => { + this.logDebug('onSourceChange'); + this.maybeEndSession(); + }; + + private handleQualityChanged = (event: QualityEvent<'activequalitychanged'>) => { + const quality = event.quality as VideoQuality; + void this.queueOrSendEvent(EventType.bitrateChange, { + qoeDataDetails: this._media?.createQoEObject(sanitiseNumber(quality?.bandwidth), 0, sanitiseNumber(quality?.frameRate), 0), + }); + }; + + private handleAddTextTrack = (event: AddTrackEvent) => { + const track = event.track as TextTrack; + if (track.kind === 'chapters') { + track.addEventListener('entercue', this.handleEnterCue); + track.addEventListener('exitcue', this.handleExitCue); + } + }; + + private handleRemoveTextTrack = (event: RemoveTrackEvent) => { + const track = event.track as TextTrack; + if (track.kind === 'chapters') { + track.removeEventListener('entercue', this.handleEnterCue); + track.removeEventListener('exitcue', this.handleExitCue); + } + }; + + private handleAddVideoTrack = (event: AddTrackEvent) => { + (event.track as MediaTrack).addEventListener('activequalitychanged', this.handleQualityChanged); + }; + + private handleRemoveVideoTrack = (event: RemoveTrackEvent) => { + (event.track as MediaTrack).removeEventListener('activequalitychanged', this.handleQualityChanged); + }; + + private handleEnterCue = (event: TextTrackEnterCueEvent) => { + const chapterCue = event.cue; + if (this._currentChapter && this._currentChapter.endTime !== chapterCue.startTime) { + this.queueOrSendEvent(EventType.chapterSkip); + } + this.queueOrSendEvent( + EventType.chapterStart, + this._media?.createChapterObject( + sanitiseChapterId(chapterCue.id), + idToInt(chapterCue.id, 1), + Math.trunc(chapterCue.endTime), + Math.trunc(chapterCue.endTime - chapterCue.startTime), + ), + this._customMetadata, + ); + this._currentChapter = chapterCue; + }; + + private handleExitCue = () => { + this.queueOrSendEvent(EventType.chapterComplete); + }; + + private handleError = (error: ErrorEvent) => { + this.setError(error.errorObject.code.toString()); + }; + + private handleAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { + this.logDebug('onAdBreakBegin'); + const currentAdBreakTimeOffset = event.adBreak.timeOffset; + let position: number; + // The pod position should start at 1. + if (currentAdBreakTimeOffset <= 0) { + position = 1; + } else { + position = this._adBreakPodIndex + 1; + } + this.queueOrSendEvent(EventType.adBreakStart, this._media?.createAdBreakObject(PROP_NA, position, currentAdBreakTimeOffset)); + if (position > this._adBreakPodIndex) { + this._adBreakPodIndex++; + } + }; + + private handleAdBreakEnd = () => { + this.logDebug('onAdBreakEnd'); + this._adPodPosition = 1; + this.queueOrSendEvent(EventType.adBreakComplete); + }; + + private handleAdBegin = (event: AdEvent<'adbegin'>) => { + this.logDebug('onAdBegin'); + this.queueOrSendEvent( + EventType.adStart, + this._media?.createAdObject(PROP_NA, PROP_NA, this._adPodPosition, event.ad.duration ? Math.trunc(event.ad.duration) : 0), + this._customMetadata, + ); + this._adPodPosition++; + }; + + private handleAdEnd = () => { + this.logDebug('onAdEnd'); + this.queueOrSendEvent(EventType.adComplete); + }; + + private handleAdSkip = () => { + this.logDebug('onAdSkip'); + this.queueOrSendEvent(EventType.adSkip); + }; + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLengthSec + * @private + */ + private maybeStartSession(mediaLengthSec?: number) { + const mediaLength = this.getContentLength(mediaLengthSec); + const hasValidSource = this._player.source !== undefined; + const hasValidDuration = isValidDuration(mediaLength); + const isPlayingAd = this._player.ads?.playing; + + this.logDebug( + `maybeStartSession -`, + `mediaLength: ${mediaLength},`, + `hasValidSource: ${hasValidSource},`, + `hasValidDuration: ${hasValidDuration},`, + `isPlayingAd: ${isPlayingAd}`, + ); + + if (this._sessionInProgress) { + this.logDebug('maybeStartSession - NOT started: already in progress'); + return; + } + + if (isPlayingAd) { + this.logDebug('maybeStartSession - NOT started: playing ad'); + return; + } + + if (!hasValidSource || !hasValidDuration) { + this.logDebug(`maybeStartSession - NOT started: invalid ${hasValidSource ? 'duration' : 'source'}`); + return; + } + + // Allow overriding metadata with custom metadata set via updateMetadata(). + const mergedMetadata = { + ...this._player?.source?.metadata, + ...this._customMetadata, + }; + this._tracker?.trackSessionStart( + this._media?.createMediaObject( + mergedMetadata.friendlyName || mergedMetadata.title || PROP_NA, + mergedMetadata.name || mergedMetadata.id || PROP_NA, + mediaLength, + this.getContentType(), + this._media.MediaType.Video, + ), + this._customMetadata, + ); + + this._sessionInProgress = true; + + // Post any queued events now that the session has started. + this._eventQueue.forEach((event) => this.sendEvent(event.type, event.info, event.metadata)); + this._eventQueue = []; + + this.logDebug('maybeStartSession - started'); + } + + private onLoadedMetadata = () => { + this.logDebug('onLoadedMetadata'); + + // NOTE: In case of a pre-roll ad: + // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; + // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, + // and again after the pre-roll with a correct duration. + this.maybeStartSession(this._player.duration); + }; + + private onBeforeUnload = () => { + this.maybeEndSession(); + }; + + private maybeEndSession() { + this.logDebug(`maybeEndSession`); + if (this._sessionInProgress) { + this.queueOrSendEvent(EventType.sessionEnd); + } + this.reset(); + } + + private getContentLength(mediaLengthSec?: number): number { + return sanitiseContentLength(mediaLengthSec !== undefined ? mediaLengthSec : this._player.duration); + } + + private getContentType(): string { + return this._player.duration === Infinity ? 'Live' : 'VOD'; + } + + reset() { + this.logDebug('reset'); + this._eventQueue = []; + this._adBreakPodIndex = 0; + this._adPodPosition = 1; + this._sessionInProgress = false; + this._currentChapter = undefined; + this._customMetadata = {}; + } + + destroy() { + this.maybeEndSession(); + this.removeEventListeners(); + } + + setDebug(debug: boolean) { + this._debug = debug; + this._alloyClient?.('setDebug', { enabled: debug }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private logDebug(message?: any, ...optionalParams: any[]) { + if (this._debug) { + console.debug(TAG, message, ...optionalParams); + } + } +} + +function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { + return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; +} + +export { AdobeEdgeHandler }; diff --git a/adobe-edge/src/internal/web/EventType.ts b/adobe-edge/src/internal/web/EventType.ts new file mode 100644 index 00000000..707a28e6 --- /dev/null +++ b/adobe-edge/src/internal/web/EventType.ts @@ -0,0 +1,22 @@ +export enum EventType { + sessionStart = 'sessionStart', + play = 'play', + bitrateChange = 'bitrateChange', + bufferStart = 'bufferStart', + pauseStart = 'pauseStart', + adBreakStart = 'adBreakStart', + adStart = 'adStart', + adComplete = 'adComplete', + adSkip = 'adSkip', + adBreakComplete = 'adBreakComplete', + chapterStart = 'chapterStart', + chapterSkip = 'chapterSkip', + chapterComplete = 'chapterComplete', + error = 'error', + sessionEnd = 'sessionEnd', + sessionComplete = 'sessionComplete', + statesUpdate = 'statesUpdate', + updatePlayhead = 'updatePlayhead', + seekStart = 'seekStart', + seekEnd = 'seekEnd', +} diff --git a/adobe-edge/src/internal/web/Media.ts b/adobe-edge/src/internal/web/Media.ts new file mode 100644 index 00000000..f7b941fa --- /dev/null +++ b/adobe-edge/src/internal/web/Media.ts @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types */ +/** + * Internal types not exported by the Adobe SDK. + */ +type AlloyClient = Function; +export default AlloyClient; + +export type StreamType = { + readonly VOD: 'vod'; + readonly Live: 'live'; + readonly Linear: 'linear'; + readonly Podcast: 'podcast'; + readonly Audiobook: 'audiobook'; + readonly AOD: 'aod'; +}; + +export type MediaType = { + readonly Video: 'video'; + readonly Audio: 'audio'; +}; + +export type MediaTracker = { + trackSessionStart: (mediaObject: any, contextData?: any) => any; + trackPlay: () => any; + trackPause: () => any; + trackSessionEnd: () => any; + trackComplete: () => any; + trackError: (errorId: any) => any; + trackEvent: (eventType: any, info: any, context: any) => any; + updatePlayhead: (time: any) => void; + updateQoEObject: (qoeObject: any) => void; + destroy: () => void; +}; + +export type MediaObject = + | { + sessionDetails: { + name: any; + friendlyName: any; + length: number; + streamType: any; + contentType: any; + }; + } + | { + sessionDetails?: undefined; + }; + +export type AdBreakObject = + | { + advertisingPodDetails: { + friendlyName: any; + offset: any; + index: any; + }; + } + | { + advertisingPodDetails?: undefined; + }; + +export type AdObject = + | { + advertisingDetails: { + friendlyName: any; + name: any; + podPosition: any; + length: any; + }; + } + | { + advertisingDetails?: undefined; + }; + +type ChapterObject = + | { + chapterDetails: { + friendlyName: any; + offset: any; + index: any; + length: any; + }; + } + | { + chapterDetails?: undefined; + }; + +export type StateObject = + | { + name: any; + } + | { + name?: undefined; + }; + +export type QoEObject = + | { + bitrate: any; + droppedFrames: any; + framesPerSecond: any; + timeToStart: any; + } + | { + bitrate?: undefined; + droppedFrames?: undefined; + framesPerSecond?: undefined; + timeToStart?: undefined; + }; + +export type Media = { + getInstance: () => MediaTracker; + createMediaObject: (friendlyName: any, name: any, length: any, contentType: any, streamType: any) => MediaObject; + createAdBreakObject: (name: any, position: any, startTime: any) => AdBreakObject; + createAdObject: (name: any, id: any, position: any, length: any) => AdObject; + createChapterObject: (name: any, position: any, length: any, startTime: any) => ChapterObject; + createStateObject: (stateName: any) => StateObject; + createQoEObject: (bitrate: any, droppedFrames: any, fps: any, startupTime: any) => QoEObject; + + StreamType: StreamType; + MediaType: MediaType; +}; diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts new file mode 100644 index 00000000..675111b1 --- /dev/null +++ b/adobe-edge/src/internal/web/Utils.ts @@ -0,0 +1,67 @@ +import type { AdobeEdgeWebConfig } from '@theoplayer/react-native-analytics-adobe-edge'; + +const PROP_NA = 'NA'; + +/** + * Sanitise the current media length. + * + * - In case of a live stream, set it to 24h. + */ +export function sanitiseContentLength(mediaLengthSec: number): number { + return mediaLengthSec === Infinity ? 86400 : Math.trunc(mediaLengthSec); +} + +/** + * Sanitise the current playhead in seconds. Adobe expects an integer value. + * + * - If undefined or NaN, set it to 0. + * - If infinite (live stream), set it to the current second of the day. + * + * @param playheadInSec + * @param mediaLengthSec + */ +export function sanitisePlayhead(playheadInSec?: number, mediaLengthSec?: number): number { + if (!playheadInSec || isNaN(playheadInSec) || !mediaLengthSec) { + return 0; + } + if (mediaLengthSec === Infinity) { + // If content is live, the playhead must be the current second of the day. + const date = new Date(); + return date.getSeconds() + 60 * (date.getMinutes() + 60 * date.getHours()); + } + return Math.trunc(playheadInSec); +} + +export function sanitiseNumber(v?: number): number { + if (v === undefined || v === null || Number.isNaN(v)) { + return 0; + } + return v; +} + +export function isValidDuration(v: number | undefined): boolean { + return v !== undefined && !Number.isNaN(v); +} + +export function sanitiseChapterId(id?: string): string { + if (!id || id.trim().length === 0) { + return PROP_NA; + } + return id; +} + +export function idToInt(id?: string, otherwise: number = 0): number { + const intId = Number(id); + return isNaN(intId) ? otherwise : intId; +} + +export function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { + return { + ...config, + streamingMedia: { + ...config.streamingMedia, + channel: config.streamingMedia?.channel || 'defaultChannel', + playerName: config.streamingMedia?.playerName || 'THEOplayer', + }, + }; +} diff --git a/adobe-edge/src/utils/UserAgent.ts b/adobe-edge/src/utils/UserAgent.ts deleted file mode 100644 index a349d904..00000000 --- a/adobe-edge/src/utils/UserAgent.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { I18nManager, Platform, Settings } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; - -const USER_AGENT_PREFIX = 'Mozilla/5.0'; -const UNKNOWN = 'unknown'; - -function nonEmptyOrUnknown(str?: string): string { - return str && str !== '' ? str : UNKNOWN; -} - -export function buildUserAgent(): string { - if (Platform.OS === 'android') { - const { Release, Model: deviceName } = Platform.constants; - const localeString = nonEmptyOrUnknown(I18nManager?.getConstants()?.localeIdentifier?.replace('_', '-')); - const operatingSystem = `Android ${Release}`; - const deviceBuildId = nonEmptyOrUnknown(DeviceInfo.getBuildIdSync()); - // operatingSystem: `Android Build.VERSION.RELEASE` - // deviceName: Build.MODEL - // Example: Mozilla/5.0 (Linux; U; Android 7.1.2; en-US; AFTN Build/NS6296) - return `${USER_AGENT_PREFIX} (Linux; U; ${operatingSystem}; ${localeString}; ${deviceName} Build/${deviceBuildId})`; - } else if (Platform.OS === 'ios') { - const localeString = Settings.get('AppleLocale') || Settings.get('AppleLanguages')[0]; - const model = DeviceInfo.getModel(); - const osVersion = DeviceInfo.getSystemVersion().replace('.', '_'); - return `${USER_AGENT_PREFIX} (${model}; CPU OS ${osVersion} like Mac OS X; ${localeString})`; - } else if (Platform.OS === 'web') { - return navigator.userAgent; - } /* if (Platform.OS === 'windows' || Platform.OS === 'macos') */ else { - // Custom User-Agent for Windows and macOS not supported - return 'Unknown'; - } -} diff --git a/adobe-edge/src/utils/UserAgent.web.ts b/adobe-edge/src/utils/UserAgent.web.ts deleted file mode 100644 index 62c690ff..00000000 --- a/adobe-edge/src/utils/UserAgent.web.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function buildUserAgent(): string | undefined { - return navigator.userAgent; -} diff --git a/adobe-edge/src/utils/Utils.ts b/adobe-edge/src/utils/Utils.ts deleted file mode 100644 index 6998a5a7..00000000 --- a/adobe-edge/src/utils/Utils.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Ad, AdBreak, TextTrackCue } from 'react-native-theoplayer'; -import type { AdobeAdvertisingDetails, AdobeAdvertisingPodDetails, AdobeChapterDetails } from '@theoplayer/react-native-analytics-adobe-edge'; - -export function sanitisePlayhead(playheadInMsec?: number): number { - if (!playheadInMsec) { - return 0; - } - if (playheadInMsec === Infinity) { - // If content is live, the playhead must be the current second of the day. - const date = new Date(); - return date.getSeconds() + 60 * (date.getMinutes() + 60 * date.getHours()); - } - return Math.trunc(playheadInMsec / 1000); -} - -/** - * Sanitise the current media length in seconds. - * - * - In case of a live stream, set it to 24h. - */ -export function sanitiseContentLength(mediaLengthMsec: number): number { - return mediaLengthMsec === Infinity ? 86400 : Math.trunc(1e-3 * mediaLengthMsec); -} - -export function calculateAdvertisingPodDetails(adBreak: AdBreak, lastPodIndex: number): AdobeAdvertisingPodDetails { - const currentAdBreakTimeOffset = adBreak.timeOffset; - let podIndex: number; - if (currentAdBreakTimeOffset === 0) { - podIndex = 0; - } else if (currentAdBreakTimeOffset < 0) { - podIndex = -1; - } else { - podIndex = lastPodIndex++; - } - return { - index: podIndex ?? 0, - offset: Math.trunc(currentAdBreakTimeOffset), - }; -} - -export function calculateAdvertisingDetails(ad: Ad, podPosition: number): AdobeAdvertisingDetails { - return { - podPosition, - length: ad.duration ? Math.trunc(ad.duration) : 0, - name: 'NA', // TODO - playerName: 'THEOplayer', - }; -} - -export function calculateChapterDetails(cue: TextTrackCue): AdobeChapterDetails { - const id = Number(cue.id); - const index = isNaN(id) ? 0 : id; - return { - length: Math.trunc((cue.endTime - cue.startTime) / 1000), - offset: Math.trunc(cue.startTime / 1000), - index, - }; -} diff --git a/apps/e2e/android/app/build.gradle b/apps/e2e/android/app/build.gradle index 98920e79..b7ba82f1 100644 --- a/apps/e2e/android/app/build.gradle +++ b/apps/e2e/android/app/build.gradle @@ -112,12 +112,12 @@ def safeExtGet(prop, fallback) { } dependencies { - implementation project(path: ':react-native-theoplayer-analytics-adobe') - implementation project(path: ':react-native-theoplayer-analytics-adobe-edge') - implementation project(path: ':react-native-theoplayer-analytics-comscore') - implementation project(path: ':react-native-theoplayer-analytics-conviva') - implementation project(path: ':react-native-theoplayer-analytics-nielsen') - implementation project(path: ':react-native-theoplayer-yospace') + implementation project(path: ':react-native-theoplayer-analytics-adobe') + implementation project(path: ':react-native-theoplayer-analytics-adobe-edge') + implementation project(path: ':react-native-theoplayer-analytics-comscore') + implementation project(path: ':react-native-theoplayer-analytics-conviva') + implementation project(path: ':react-native-theoplayer-analytics-nielsen') + implementation project(path: ':react-native-theoplayer-yospace') // implementation project(path: ':react-native-theoplayer-analytics-adscript') // implementation project(path: ':react-native-theoplayer-analytics-gemius') diff --git a/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties b/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties index 2733ed5d..3ae1e2f1 100644 --- a/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/apps/e2e/ios/Podfile b/apps/e2e/ios/Podfile index aaa4fd6e..0c011f07 100755 --- a/apps/e2e/ios/Podfile +++ b/apps/e2e/ios/Podfile @@ -32,7 +32,6 @@ target 'ReactNativeTHEOplayer' do pod 'react-native-theoplayer-conviva', :path => '../../../conviva' pod 'react-native-theoplayer-nielsen', :path => '../../../nielsen' pod 'react-native-theoplayer-adobe', :path => '../../../adobe' - pod 'react-native-theoplayer-adobe-edge', :path => '../../../adobe-edge' google_cast_redirect @@ -49,7 +48,6 @@ target 'ReactNativeTHEOplayer-tvOS' do pod 'react-native-theoplayer-conviva', :path => '../../../conviva' pod 'react-native-theoplayer-nielsen', :path => '../../../nielsen' pod 'react-native-theoplayer-adobe', :path => '../../../adobe' - pod 'react-native-theoplayer-adobe-edge', :path => '../../../adobe-edge' end diff --git a/apps/e2e/src/tests/AdobeEdge.spec.ts b/apps/e2e/src/tests/AdobeEdge.spec.ts index 95d69daa..baa62c19 100644 --- a/apps/e2e/src/tests/AdobeEdge.spec.ts +++ b/apps/e2e/src/tests/AdobeEdge.spec.ts @@ -9,13 +9,37 @@ export default function (spec: TestScope) { testConnector( spec, (player: THEOplayer) => { - connector = new AdobeConnector(player, 'https://edge.adobedc.net/ee-pre-prd/va/v1', 'dataStreamId', undefined, true, undefined, false); + connector = new AdobeConnector(player, { + web: { + datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', + orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', + edgeBasePath: 'ee', + debugEnabled: true, + }, + mobile: { + environmentId: 'abcdef012345/abcdef012345/launch-abcdef012345-development', + debugEnabled: true, + }, + }); }, () => { - connector.stopAndStartNewSession([ - { name: 'title', value: 'test' }, - { name: 'custom1', value: 'value1' }, - ]); + connector.stopAndStartNewSession({ + friendlyName: 'New Session', + }); + connector.updateMetadata({ + custom1: 'value1', + custom2: 'value2', + }); + connector.setCustomIdentityMap({ + EMAIL: [ + { + id: 'user@example.com', + authenticatedState: 'authenticated', + primary: false, + }, + ], + }); + connector.setError('testError'); }, () => { connector.destroy(); diff --git a/apps/e2e/src/tests/AdobeEdgeNative.spec.ts b/apps/e2e/src/tests/AdobeEdgeNative.spec.ts deleted file mode 100644 index fcaa9b5d..00000000 --- a/apps/e2e/src/tests/AdobeEdgeNative.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TestScope } from 'cavy'; -import { AdobeConnector } from '@theoplayer/react-native-analytics-adobe-edge'; -import { testConnector } from './ConnectorUtils'; -import { THEOplayer } from 'react-native-theoplayer'; - -export default function (spec: TestScope) { - spec.describe(`Setup Adobe Edge Native connector`, function () { - let connector: AdobeConnector; - testConnector( - spec, - (player: THEOplayer) => { - connector = new AdobeConnector(player, 'https://edge.adobedc.net/ee-pre-prd/va/v1', 'dataStreamId', undefined, true, undefined, true); - }, - () => { - connector.stopAndStartNewSession([ - { name: 'title', value: 'test' }, - { name: 'custom1', value: 'value1' }, - ]); - }, - () => { - connector.destroy(); - }, - ); - }); -} diff --git a/apps/e2e/src/tests/index.ts b/apps/e2e/src/tests/index.ts index e2749b3c..56a5931e 100644 --- a/apps/e2e/src/tests/index.ts +++ b/apps/e2e/src/tests/index.ts @@ -1,14 +1,15 @@ import Adobe from './Adobe.spec'; import AdobeNative from './AdobeNative.spec'; import AdobeEdge from './AdobeEdge.spec'; -import AdobeEdgeNative from './AdobeEdgeNative.spec'; import Comscore from './Comscore.spec'; import Conviva from './Conviva.spec'; import Nielsen from './Nielsen.spec'; import Yospace from './Yospace.spec'; import { Platform } from 'react-native'; -const tests = [Adobe, AdobeNative, AdobeEdge, AdobeEdgeNative, Comscore, Conviva, Nielsen]; +const tests = Platform.OS === 'ios' ? [Adobe, AdobeNative, Comscore, Conviva, Nielsen] : + [Adobe, AdobeNative, AdobeEdge, Comscore, Conviva, Nielsen]; + if (Platform.OS === 'android') { tests.push(Yospace); } diff --git a/package-lock.json b/package-lock.json index 3d729ddf..994ca668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,8 +73,12 @@ "version": "0.7.0", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { + "@adobe/alloy": "^2.30.0", "openapi-fetch": "^0.9.8" }, + "devDependencies": { + "metro-react-native-babel-preset": "^0.77.0" + }, "peerDependencies": { "react": "*", "react-native": "*", @@ -145,7 +149,7 @@ }, "conviva": { "name": "@theoplayer/react-native-analytics-conviva", - "version": "1.11.0", + "version": "1.11.1", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { "@convivainc/conviva-js-coresdk": "^4.8.0", @@ -238,6 +242,73 @@ } } }, + "node_modules/@adobe/aep-rules-engine": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-3.1.1.tgz", + "integrity": "sha512-YeFDDSEM4wjwTNM4drKTIbYvfSdYS3DmeOgb1SxZfSXTcVg8vdxcd5blE2wKO42Qa4KUJK+ziK7ABvbHoO7T8Q==", + "license": "Apache-2.0", + "dependencies": { + "@vitest/coverage-v8": "^3.1.4" + } + }, + "node_modules/@adobe/alloy": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.30.0.tgz", + "integrity": "sha512-A2MFFUB2aurYdGt12q3Vov263Gu2KSvMO+CgWxufsK/W7dQIaJfUuu02qQrxtul/Ar7JK8FsIwNWR/ct9fjJjA==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/aep-rules-engine": "^3.1.1", + "@adobe/reactor-query-string": "^2.0.0", + "@babel/core": "^7.28.4", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/preset-env": "^7.28.3", + "@inquirer/prompts": "^7.8.4", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "commander": "^14.0.0", + "css.escape": "^1.5.1", + "js-cookie": "3.0.5", + "rollup": "^4.50.1", + "rollup-plugin-license": "^3.6.0", + "uuid": "^11.1.0" + }, + "bin": { + "alloyBuilder": "scripts/alloyBuilder.js" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.50.1" + } + }, + "node_modules/@adobe/alloy/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@adobe/reactor-query-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@adobe/reactor-query-string/-/reactor-query-string-2.0.0.tgz", + "integrity": "sha512-rGNnmKjpjA898mOHP5xU05geL50uwQDCxx6Ekh8C+l4Pem5OJIZJN/weqTgzVVxp9F+mRdPixFW5PqCEZrnTuA==", + "license": "Apache-2.0" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "license": "MIT", @@ -333,7 +404,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -365,7 +435,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -385,7 +454,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -393,7 +461,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -409,7 +476,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -417,7 +483,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", @@ -432,7 +497,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { "version": "1.22.10", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -449,6 +513,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "license": "MIT", @@ -458,7 +535,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -496,7 +572,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -514,7 +589,6 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -530,7 +604,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", @@ -546,7 +619,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -579,7 +651,6 @@ }, "node_modules/@babel/helper-wrap-function": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -616,7 +687,6 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -631,7 +701,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -645,7 +714,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -659,7 +727,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -675,7 +742,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -688,9 +754,156 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -742,6 +955,35 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz", + "integrity": "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.27.1", "dev": true, @@ -758,7 +1000,6 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -919,7 +1160,6 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -934,7 +1174,6 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -948,7 +1187,6 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -964,7 +1202,6 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -980,7 +1217,6 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -994,7 +1230,6 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1008,7 +1243,6 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -1023,7 +1257,6 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", @@ -1038,7 +1271,6 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1057,7 +1289,6 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1072,7 +1303,6 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1087,7 +1317,6 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1102,7 +1331,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1116,7 +1344,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1131,7 +1358,6 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1145,7 +1371,6 @@ }, "node_modules/@babel/plugin-transform-explicit-resource-management": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1160,7 +1385,6 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1174,7 +1398,6 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1203,7 +1426,6 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1218,7 +1440,6 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", @@ -1234,7 +1455,6 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1248,7 +1468,6 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1262,7 +1481,6 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1276,7 +1494,6 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1290,7 +1507,6 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1305,7 +1521,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1320,7 +1535,6 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1337,7 +1551,6 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1352,7 +1565,6 @@ }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1367,7 +1579,6 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1381,7 +1592,6 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1395,7 +1605,6 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1409,7 +1618,6 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", @@ -1427,7 +1635,6 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1442,7 +1649,6 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1456,7 +1662,6 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1471,7 +1676,6 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1485,7 +1689,6 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -1500,7 +1703,6 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -1516,7 +1718,6 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1574,12 +1775,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { + "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -1589,8 +1791,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1603,24 +1807,23 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { + "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.27.1", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "dev": true, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1632,27 +1835,25 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { + "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-spread": { + "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1661,16 +1862,73 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { "@babel/core": "^7.0.0-0" } @@ -1691,7 +1949,6 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1705,7 +1962,6 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1737,7 +1993,6 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1751,7 +2006,6 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1766,7 +2020,6 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1781,7 +2034,6 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1796,7 +2048,6 @@ }, "node_modules/@babel/preset-env": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.0", @@ -1879,7 +2130,6 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1903,7 +2153,6 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -2014,6 +2263,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "7.0.13", "dev": true, @@ -2409,13 +2667,26 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inquirer/external-editor": { + "node_modules/@inquirer/ansi": { "version": "1.0.2", - "dev": true, + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -2429,153 +2700,490 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "license": "ISC", + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "dev": true, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "dev": true, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "dev": true, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "dev": true, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/ttlcache": { - "version": "1.4.1", - "license": "ISC", + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "license": "ISC", + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@jest/create-cache-key-function": { - "version": "29.7.0", + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/environment": { - "version": "29.7.0", + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" @@ -2785,7 +3393,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2924,64 +3531,531 @@ "version": "0.79.6", "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/eslint-parser": "^7.25.1", - "@react-native/eslint-plugin": "0.79.6", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-ft-flow": "^2.0.1", - "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-react": "^7.30.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-native": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": ">=8", - "prettier": ">=2" - } - }, - "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { - "version": "8.10.2", - "dev": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", + "@react-native/eslint-plugin": "0.79.6", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { + "version": "8.10.2", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.79.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.79.6", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.79.6", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.79.6", + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/@react-native/eslint-plugin": { - "version": "0.79.6", - "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.79.6", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/js-polyfills": { - "version": "0.79.6", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/normalize-colors": { - "version": "0.79.6", - "license": "MIT" + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -3121,6 +4195,30 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "license": "MIT", @@ -3166,6 +4264,12 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.1", "dev": true, @@ -3379,6 +4483,207 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -3536,6 +4841,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-includes": { "version": "3.1.9", "dev": true, @@ -3657,6 +4971,42 @@ "version": "2.0.6", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "dev": true, @@ -3793,7 +5143,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.7", @@ -3806,7 +5155,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3814,7 +5162,6 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", @@ -3826,7 +5173,6 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" @@ -3842,6 +5188,16 @@ "hermes-parser": "0.25.1" } }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "license": "MIT", @@ -3922,7 +5278,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4002,6 +5357,16 @@ "version": "1.1.2", "license": "MIT" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "dev": true, @@ -4106,6 +5471,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "license": "MIT", @@ -4121,10 +5503,21 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "dev": true, + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "license": "Apache-2.0", @@ -4216,6 +5609,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "license": "ISC", @@ -4293,6 +5695,18 @@ "node": ">=18" } }, + "node_modules/commenting": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.1.0.tgz", + "integrity": "sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==", + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -4442,7 +5856,6 @@ }, "node_modules/core-js-compat": { "version": "3.45.1", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.25.3" @@ -4490,7 +5903,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4501,6 +5913,12 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "devOptional": true, @@ -4574,11 +5992,30 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "dev": true, @@ -4725,7 +6162,6 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -4902,6 +6338,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "peer": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "dev": true, @@ -4954,6 +6397,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "license": "MIT", @@ -5555,9 +7040,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -5604,6 +7094,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "license": "Apache-2.0" @@ -5792,7 +7292,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5842,7 +7341,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6154,7 +7652,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6174,6 +7671,12 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -6238,7 +7741,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.0", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6452,7 +7954,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6810,6 +8311,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "dev": true, @@ -6866,6 +8373,15 @@ "node": ">=8" } }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.2.1", "dev": true, @@ -7062,7 +8578,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7093,6 +8608,47 @@ "semver": "bin/semver.js" } }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "dev": true, @@ -7111,7 +8667,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7271,6 +8826,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7488,12 +9052,10 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -7603,6 +9165,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "license": "MIT", + "peer": true + }, "node_modules/lru-cache": { "version": "5.1.1", "license": "ISC", @@ -7615,6 +9184,41 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/makeerror": { "version": "1.0.12", "license": "BSD-3-Clause", @@ -7828,6 +9432,71 @@ "node": ">=18.18" } }, + "node_modules/metro-react-native-babel-preset": { + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.77.0.tgz", + "integrity": "sha512-HPPD+bTxADtoE4y/4t1txgTQ1LVR6imOBy7RMHUsqMVTbekoi8Ph5YI9vKX2VMPtVWeFt0w9YnCSLPa76GcXsA==", + "deprecated": "Use @react-native/babel-preset instead", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.18.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/metro-react-native-babel-preset/node_modules/react-refresh": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", + "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/metro-resolver": { "version": "0.82.5", "license": "MIT", @@ -8019,7 +9688,6 @@ }, "node_modules/minimatch": { "version": "9.0.5", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8049,6 +9717,15 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "dev": true, @@ -8061,10 +9738,38 @@ "version": "2.1.3", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/mux-embed": { "version": "4.27.0", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -8404,7 +10109,6 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { @@ -8415,6 +10119,18 @@ "quansync": "^0.2.7" } }, + "node_modules/package-name-regex": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/package-name-regex/-/package-name-regex-2.0.6.tgz", + "integrity": "sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/dword-design" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -8460,7 +10176,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8468,12 +10183,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -8488,12 +10201,10 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "dev": true, "license": "ISC" }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.1.2", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8507,6 +10218,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT", + "peer": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -8616,6 +10344,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -8775,6 +10532,15 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "license": "MIT", @@ -9432,12 +11198,10 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -9471,7 +11235,6 @@ }, "node_modules/regexpu-core": { "version": "6.3.1", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -9487,12 +11250,10 @@ }, "node_modules/regjsgen": { "version": "0.8.0", - "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -9503,7 +11264,6 @@ }, "node_modules/regjsparser/node_modules/jsesc": { "version": "3.0.2", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9631,6 +11391,86 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-license": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.6.0.tgz", + "integrity": "sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==", + "license": "MIT", + "dependencies": { + "commenting": "~1.1.0", + "fdir": "^6.4.3", + "lodash": "~4.17.21", + "magic-string": "~0.30.0", + "moment": "~2.30.1", + "package-name-regex": "~2.0.6", + "spdx-expression-validate": "~2.0.0", + "spdx-satisfies": "~5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/rollup-plugin-license/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -9673,7 +11513,6 @@ }, "node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, "license": "MIT" }, "node_modules/safe-push-apply": { @@ -9709,7 +11548,6 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -9783,6 +11621,15 @@ "node": ">=0.10.0" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "license": "MIT", @@ -9852,7 +11699,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9863,7 +11709,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9958,9 +11803,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "license": "ISC", + "peer": true + }, "node_modules/signal-exit": { "version": "4.1.0", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -10007,6 +11858,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "license": "BSD-3-Clause", @@ -10014,6 +11871,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "license": "MIT", @@ -10038,6 +11904,65 @@ "signal-exit": "^4.0.1" } }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-expression-validate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", + "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", + "license": "(MIT AND CC-BY-3.0)", + "dependencies": { + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", + "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "license": "BSD-3-Clause" @@ -10059,6 +11984,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "license": "MIT", + "peer": true + }, "node_modules/stackframe": { "version": "1.3.4", "license": "MIT" @@ -10087,6 +12019,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "dev": true, @@ -10146,7 +12084,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10159,12 +12096,10 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10296,7 +12231,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10335,6 +12269,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT", + "peer": true + }, "node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -10347,7 +12301,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10453,6 +12406,97 @@ "xtend": "~4.0.1" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "license": "MIT", + "peer": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT", + "peer": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "license": "BSD-3-Clause" @@ -10683,7 +12727,6 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10691,7 +12734,6 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -10703,7 +12745,6 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10711,7 +12752,6 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10788,6 +12828,234 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vlq": { "version": "1.0.1", "license": "MIT" @@ -10815,7 +13083,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10908,6 +13175,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "license": "MIT", + "peer": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -10935,7 +13219,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10951,12 +13234,10 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10964,7 +13245,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11058,7 +13338,7 @@ }, "node_modules/yaml": { "version": "2.8.1", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -11124,6 +13404,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/youbora-adapter-theoplayer2": { "version": "6.8.10", "license": "MIT",