diff --git a/CHANGELOG.md b/CHANGELOG.md index 62374c693..8b26a2fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Extended event dispatching mechanism with broadcast functionality. + ## [3.2.1] - 23-12-07 ### Fixed diff --git a/android/src/main/java/com/theoplayer/ReactTHEOplayerPackage.kt b/android/src/main/java/com/theoplayer/ReactTHEOplayerPackage.kt index 9bc3b8ce8..ea0414955 100644 --- a/android/src/main/java/com/theoplayer/ReactTHEOplayerPackage.kt +++ b/android/src/main/java/com/theoplayer/ReactTHEOplayerPackage.kt @@ -8,6 +8,7 @@ import com.theoplayer.ads.AdsModule import com.theoplayer.cache.CacheModule import com.theoplayer.drm.ContentProtectionModule import com.theoplayer.cast.CastModule +import com.theoplayer.broadcast.EventBroadcastModule import com.theoplayer.player.PlayerModule class ReactTHEOplayerPackage : ReactPackage { @@ -17,7 +18,8 @@ class ReactTHEOplayerPackage : ReactPackage { AdsModule(reactContext), ContentProtectionModule(reactContext), CastModule(reactContext), - CacheModule(reactContext) + CacheModule(reactContext), + EventBroadcastModule(reactContext) ) } diff --git a/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt b/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt index ae0459921..35f6f5429 100644 --- a/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt +++ b/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt @@ -11,6 +11,7 @@ import com.theoplayer.android.api.ads.wrapper.AdsApiWrapper import com.theoplayer.android.api.cast.Cast import com.theoplayer.android.api.error.THEOplayerException import com.theoplayer.android.api.player.Player +import com.theoplayer.broadcast.EventBroadcastAdapter import com.theoplayer.presentation.PresentationManager import com.theoplayer.source.SourceAdapter @@ -22,7 +23,7 @@ class ReactTHEOplayerView(private val reactContext: ThemedReactContext) : private val eventEmitter: PlayerEventEmitter = PlayerEventEmitter(reactContext.reactApplicationContext, this) - + val broadcast = EventBroadcastAdapter(this) var presentationManager: PresentationManager? = null var playerContext: ReactTHEOplayerContext? = null private var isInitialized: Boolean = false diff --git a/android/src/main/java/com/theoplayer/ads/AdAdapter.kt b/android/src/main/java/com/theoplayer/ads/AdAdapter.kt index a60035853..df5118405 100644 --- a/android/src/main/java/com/theoplayer/ads/AdAdapter.kt +++ b/android/src/main/java/com/theoplayer/ads/AdAdapter.kt @@ -1,12 +1,17 @@ package com.theoplayer.ads import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableArray +import com.google.ads.interactivemedia.v3.api.AdPodInfo +import com.google.ads.interactivemedia.v3.api.UiElement import com.theoplayer.android.api.ads.Ad import com.theoplayer.android.api.ads.AdBreak import com.theoplayer.android.api.ads.CompanionAd import com.theoplayer.android.api.ads.GoogleImaAd +import com.theoplayer.android.api.ads.UniversalAdId +import com.theoplayer.android.api.event.ads.AdIntegrationKind import java.lang.Exception private const val PROP_AD_SYSTEM = "adSystem" @@ -42,11 +47,13 @@ private const val PROP_COMPANION_RESOURCEURI = "resourceURI" private const val PROP_UNIVERSAL_AD_ID_REGISTRY = "adIdRegistry" private const val PROP_UNIVERSAL_AD_ID_VALUE = "adIdValue" -object AdAdapter { - fun fromAd(ad: Ad): WritableMap { - return fromAd(ad, true) - } +private const val INVALID_DOUBLE = -1.0 +private const val INVALID_INT = -1 +object AdAdapter { + /** + * Convert a list of native Ads to a ReactNative Ads. + */ fun fromAds(ads: List): WritableArray { val payload = Arguments.createArray() for (ad in ads) { @@ -55,6 +62,16 @@ object AdAdapter { return payload } + /** + * Convert a native Ad to a ReactNative Ad. + */ + fun fromAd(ad: Ad): WritableMap { + return fromAd(ad, true) + } + + /** + * Convert a native Ad to a ReactNative Ad, optionally include its AdBreak. + */ private fun fromAd(ad: Ad, includeAdBreak: Boolean): WritableMap { val adPayload = Arguments.createMap() adPayload.putString( @@ -113,6 +130,9 @@ object AdAdapter { return adPayload } + /** + * Convert a native AdBreak to a ReactNative AdBreak. + */ fun fromAdBreak(adbreak: AdBreak?): WritableMap { val adbreakPayload = Arguments.createMap() if (adbreak == null) { @@ -158,4 +178,275 @@ object AdAdapter { } return companionsPayload } + + /** + * Convert a ReactNative Ad to a native Ad. + */ + fun parseAd(ad: ReadableMap?): GoogleImaAd? { + if (ad == null) { + return null + } + return object: GoogleImaAd { + override fun getId(): String { + return ad.getString(PROP_AD_ID) ?: "" + } + + override fun getCompanions(): List { + return emptyList() + } + + override fun getType(): String? { + return ad.getString(PROP_AD_TYPE) + } + + override fun getAdBreak(): AdBreak? { + return parseAdBreak(ad.getMap(PROP_AD_BREAK)) + } + + override fun getSkipOffset(): Int { + return if (ad.hasKey(PROP_AD_SKIPOFFSET)) ad.getInt(PROP_AD_SKIPOFFSET) else 0 + } + + override fun getIntegration(): AdIntegrationKind { + return AdIntegrationKind.from(ad.getString(PROP_AD_INTEGRATION)) + } + + override fun getImaAd(): com.google.ads.interactivemedia.v3.api.Ad { + return parseImaAd(ad) + } + + override fun getAdSystem(): String { + return ad.getString(PROP_AD_SYSTEM) ?: "" + } + + override fun getCreativeId(): String? { + return ad.getString(PROP_AD_CREATIVE_ID) + } + + override fun getWrapperAdIds(): List { + return emptyList() + } + + override fun getWrapperAdSystems(): List { + return emptyList() + } + + override fun getWrapperCreativeIds(): List { + return emptyList() + } + + override fun getVastMediaBitrate(): Int { + return if (ad.hasKey(PROP_AD_BITRATE)) ad.getInt(PROP_AD_BITRATE) else 0 + } + + override fun getUniversalAdIds(): List { + return emptyList() + } + + override fun getTraffickingParameters(): String { + return ad.getString(PROP_AD_TRAFFICKING_PARAMETERS) ?: "" + } + } + } + + fun parseAdBreak(adBreak: ReadableMap?): AdBreak? { + if (adBreak == null) { + return null + } + return object: AdBreak { + override fun getAds(): List { + return emptyList() + } + + override fun getMaxDuration(): Int { + return if (adBreak.hasKey(PROP_ADBREAK_MAXDURATION)) + (1e-3 * adBreak.getInt(PROP_ADBREAK_MAXDURATION)).toInt() + else + INVALID_INT + } + + override fun getMaxRemainingDuration(): Double { + return if (adBreak.hasKey(PROP_ADBREAK_MAXREMAININGDURATION)) + 1e-3 * adBreak.getDouble(PROP_ADBREAK_MAXREMAININGDURATION) + else + INVALID_DOUBLE + } + + override fun getTimeOffset(): Int { + return if (adBreak.hasKey(PROP_ADBREAK_TIMEOFFSET)) + (1e-3 * adBreak.getInt(PROP_ADBREAK_TIMEOFFSET)).toInt() + else + 0 + } + + override fun getIntegration(): AdIntegrationKind { + return AdIntegrationKind.from(adBreak.getString(PROP_ADBREAK_INTEGRATION)) + } + } + } + + fun parseImaAd(ad: ReadableMap?): com.google.ads.interactivemedia.v3.api.Ad { + return object: com.google.ads.interactivemedia.v3.api.Ad { + override fun getDuration(): Double { + return ad?.run { + if (hasKey(PROP_AD_DURATION)) 1e-3 * getDouble(PROP_AD_DURATION) else INVALID_DOUBLE + } ?: INVALID_DOUBLE + } + + override fun getSkipTimeOffset(): Double { + return ad?.run { + if (hasKey(PROP_AD_SKIPOFFSET)) 1e-3 * getDouble(PROP_AD_SKIPOFFSET) else INVALID_DOUBLE + } ?: INVALID_DOUBLE + } + + override fun getHeight(): Int { + return ad?.run { + if (hasKey(PROP_AD_HEIGHT)) getInt(PROP_AD_HEIGHT) else INVALID_INT + } ?: INVALID_INT + } + + override fun getVastMediaBitrate(): Int { + return ad?.run { + if (hasKey(PROP_AD_BITRATE)) getInt(PROP_AD_BITRATE) else INVALID_INT + } ?: INVALID_INT + } + + override fun getVastMediaHeight(): Int { + return ad?.run { + if (hasKey(PROP_AD_HEIGHT)) getInt(PROP_AD_HEIGHT) else INVALID_INT + } ?: INVALID_INT + } + + override fun getVastMediaWidth(): Int { + return ad?.run { + if (hasKey(PROP_AD_WIDTH)) getInt(PROP_AD_WIDTH) else INVALID_INT + } ?: INVALID_INT + } + + override fun getWidth(): Int { + return ad?.run { + if (hasKey(PROP_AD_WIDTH)) getInt(PROP_AD_WIDTH) else INVALID_INT + } ?: INVALID_INT + } + + override fun getAdPodInfo(): AdPodInfo { + return object: AdPodInfo { + override fun getMaxDuration(): Double { + return INVALID_DOUBLE + } + + override fun getTimeOffset(): Double { + return INVALID_DOUBLE + } + + override fun getAdPosition(): Int { + return INVALID_INT + } + + override fun getPodIndex(): Int { + return INVALID_INT + } + + override fun getTotalAds(): Int { + return INVALID_INT + } + + override fun isBumper(): Boolean { + return false + } + } + } + + override fun getAdId(): String { + return ad?.getString(PROP_AD_ID) ?: "" + } + + override fun getAdSystem(): String { + return ad?.getString(PROP_AD_SYSTEM) ?: "" + } + + override fun getAdvertiserName(): String { + return "" + } + + override fun getContentType(): String { + return ad?.getString(PROP_AD_CONTENT_TYPE) ?: "" + } + + override fun getCreativeAdId(): String { + return "" + } + + override fun getCreativeId(): String { + return ad?.getString(PROP_AD_CREATIVE_ID) ?: "" + } + + override fun getDealId(): String { + return "" + } + + override fun getDescription(): String { + return "" + } + + override fun getSurveyUrl(): String { + return "" + } + + override fun getTitle(): String { + return ad?.getString(PROP_AD_TITLE) ?: "" + } + + override fun getTraffickingParameters(): String { + return ad?.getString(PROP_AD_TRAFFICKING_PARAMETERS) ?: "" + } + + @Deprecated("Deprecated in Java") + override fun getUniversalAdIdRegistry(): String { + return "" + } + + @Deprecated("Deprecated in Java") + override fun getUniversalAdIdValue(): String { + return ad?.getString(PROP_UNIVERSAL_AD_ID_VALUE) ?: "" + } + + override fun getCompanionAds(): List { + return emptyList() + } + + override fun getUiElements(): Set { + return emptySet() + } + + override fun isLinear(): Boolean { + // Only linear ads are supported currently + return true + } + + override fun isSkippable(): Boolean { + return false + } + + override fun isUiDisabled(): Boolean { + return false + } + + override fun getUniversalAdIds(): Array { + return emptyArray() + } + + override fun getAdWrapperCreativeIds(): Array { + return emptyArray() + } + + override fun getAdWrapperIds(): Array { + return emptyArray() + } + + override fun getAdWrapperSystems(): Array { + return emptyArray() + } + } + } } diff --git a/android/src/main/java/com/theoplayer/ads/AdEventAdapter.kt b/android/src/main/java/com/theoplayer/ads/AdEventAdapter.kt index 5bb70b278..20dc2e8a6 100644 --- a/android/src/main/java/com/theoplayer/ads/AdEventAdapter.kt +++ b/android/src/main/java/com/theoplayer/ads/AdEventAdapter.kt @@ -1,10 +1,13 @@ package com.theoplayer.ads import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableMap import com.theoplayer.android.api.ads.wrapper.AdsApiWrapper import com.facebook.react.bridge.WritableMap import com.theoplayer.android.api.ads.Ad import com.theoplayer.android.api.ads.AdBreak +import com.theoplayer.android.api.ads.GoogleImaAd +import com.theoplayer.android.api.ads.ima.GoogleImaAdEvent import com.theoplayer.android.api.ads.ima.GoogleImaAdEventType import com.theoplayer.android.api.ads.wrapper.AdEventListener import com.theoplayer.android.api.event.EventType @@ -13,6 +16,8 @@ import java.util.* private const val EVENT_PROP_AD = "ad" private const val EVENT_PROP_TYPE = "type" +private const val EVENT_PROP_SUBTYPE = "subType" + private val ALL_AD_EVENTS = arrayOf( GoogleImaAdEventType.LOADED, GoogleImaAdEventType.AD_BREAK_STARTED, @@ -64,21 +69,64 @@ class AdEventAdapter(private val adsApi: AdsApiWrapper, eventEmitter: AdEventEmi } } - private fun mapAdType(eventType: EventType<*>): String { - return when (eventType as GoogleImaAdEventType) { - GoogleImaAdEventType.LOADED -> "adloaded" - GoogleImaAdEventType.STARTED -> "adbegin" - GoogleImaAdEventType.FIRST_QUARTILE -> "adfirstquartile" - GoogleImaAdEventType.MIDPOINT -> "admidpoint" - GoogleImaAdEventType.THIRD_QUARTILE -> "adthirdquartile" - GoogleImaAdEventType.COMPLETED -> "adend" - GoogleImaAdEventType.SKIPPED -> "adskip" - GoogleImaAdEventType.AD_ERROR -> "aderror" - GoogleImaAdEventType.AD_BUFFERING -> "adbuffering" - GoogleImaAdEventType.AD_BREAK_STARTED -> "adbreakbegin" - GoogleImaAdEventType.AD_BREAK_ENDED -> "adbreakend" - GoogleImaAdEventType.AD_BREAK_FETCH_ERROR -> "aderror" - else -> eventType.getName().lowercase(Locale.getDefault()) + companion object { + + /** + * Create a native GoogleImaAdEvent from a ReactNative AdEvent. + */ + fun parseEvent(event: ReadableMap?): GoogleImaAdEvent? { + val eventType = mapAdType(event?.getString(EVENT_PROP_SUBTYPE)) + return if (event != null && eventType != null) { + object : GoogleImaAdEvent { + override val ad: GoogleImaAd? + get() = AdAdapter.parseAd(event.getMap(EVENT_PROP_AD)) + override val adData: Map + get() = mapOf() + override fun getDate(): Date { + return Date() + } + override fun getType(): EventType { + return eventType + } + } + } else { + null + } + } + + private fun mapAdType(eventType: String?): EventType? { + return when (eventType) { + "adloaded" -> GoogleImaAdEventType.LOADED + "adbegin" -> GoogleImaAdEventType.STARTED + "adfirstquartile" -> GoogleImaAdEventType.FIRST_QUARTILE + "admidpoint" -> GoogleImaAdEventType.MIDPOINT + "adthirdquartile" -> GoogleImaAdEventType.THIRD_QUARTILE + "adend" -> GoogleImaAdEventType.COMPLETED + "adskip" -> GoogleImaAdEventType.SKIPPED + "aderror" -> GoogleImaAdEventType.AD_ERROR + "adbuffering" -> GoogleImaAdEventType.AD_BUFFERING + "adbreakbegin" -> GoogleImaAdEventType.AD_BREAK_STARTED + "adbreakend" -> GoogleImaAdEventType.AD_BREAK_ENDED + else -> null /*unknown*/ + } + } + + private fun mapAdType(eventType: EventType<*>): String { + return when (eventType as GoogleImaAdEventType) { + GoogleImaAdEventType.LOADED -> "adloaded" + GoogleImaAdEventType.STARTED -> "adbegin" + GoogleImaAdEventType.FIRST_QUARTILE -> "adfirstquartile" + GoogleImaAdEventType.MIDPOINT -> "admidpoint" + GoogleImaAdEventType.THIRD_QUARTILE -> "adthirdquartile" + GoogleImaAdEventType.COMPLETED -> "adend" + GoogleImaAdEventType.SKIPPED -> "adskip" + GoogleImaAdEventType.AD_ERROR -> "aderror" + GoogleImaAdEventType.AD_BUFFERING -> "adbuffering" + GoogleImaAdEventType.AD_BREAK_STARTED -> "adbreakbegin" + GoogleImaAdEventType.AD_BREAK_ENDED -> "adbreakend" + GoogleImaAdEventType.AD_BREAK_FETCH_ERROR -> "aderror" + else -> eventType.getName().lowercase(Locale.getDefault()) + } } } diff --git a/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt b/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt new file mode 100644 index 000000000..91b1e3222 --- /dev/null +++ b/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt @@ -0,0 +1,27 @@ +package com.theoplayer.broadcast + +import com.theoplayer.android.api.event.EventDispatcher +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.EventType +import com.theoplayer.android.api.event.Event + +open class DefaultEventDispatcher: EventDispatcher> { + private val _listeners = mutableMapOf, MutableList>>() + + override fun > addEventListener(eventType: EventType, listener: EventListener) { + if (!_listeners.contains(eventType)) { + _listeners[eventType] = mutableListOf() + } + _listeners[eventType]?.add(listener) + } + + override fun > removeEventListener(eventType: EventType, listener: EventListener) { + _listeners[eventType]?.remove(listener) + } + + fun dispatchEvent(event: Event<*>) { + _listeners[event.type]?.forEach { listener -> + (listener as EventListener>).handleEvent(event) + } + } +} diff --git a/android/src/main/java/com/theoplayer/broadcast/EventAdapter.kt b/android/src/main/java/com/theoplayer/broadcast/EventAdapter.kt new file mode 100644 index 000000000..0fa666df7 --- /dev/null +++ b/android/src/main/java/com/theoplayer/broadcast/EventAdapter.kt @@ -0,0 +1,23 @@ +package com.theoplayer.broadcast + +import android.util.Log +import com.facebook.react.bridge.ReadableMap +import com.theoplayer.ads.AdEventAdapter +import com.theoplayer.android.api.event.Event + +private const val EVENT_PROP_TYPE = "type" +private const val PROP_AD_EVENT = "adevent" + +private const val TAG = "EventAdapter" + +object EventAdapter { + fun parseEvent(event: ReadableMap?): Event<*>? { + return when (val eventType = event?.getString(EVENT_PROP_TYPE)) { + PROP_AD_EVENT -> AdEventAdapter.parseEvent(event) + else -> { + Log.w(TAG, "Forwarding events of type $eventType not supported yet.") + null + } /*not supported yet*/ + } + } +} diff --git a/android/src/main/java/com/theoplayer/broadcast/EventBroadcastAdapter.kt b/android/src/main/java/com/theoplayer/broadcast/EventBroadcastAdapter.kt new file mode 100644 index 000000000..e6008310d --- /dev/null +++ b/android/src/main/java/com/theoplayer/broadcast/EventBroadcastAdapter.kt @@ -0,0 +1,15 @@ +package com.theoplayer.broadcast + +import com.facebook.react.bridge.ReadableMap +import com.theoplayer.ReactTHEOplayerView + +class EventBroadcastAdapter(private val view: ReactTHEOplayerView): DefaultEventDispatcher() { + /** + * Convert a react-native event to a native event and broadcast it. + */ + fun broadcastEvent(event: ReadableMap) { + EventAdapter.parseEvent(event)?.also { + view.broadcast.dispatchEvent(it) + } + } +} diff --git a/android/src/main/java/com/theoplayer/broadcast/EventBroadcastModule.kt b/android/src/main/java/com/theoplayer/broadcast/EventBroadcastModule.kt new file mode 100644 index 000000000..2ffa6c39d --- /dev/null +++ b/android/src/main/java/com/theoplayer/broadcast/EventBroadcastModule.kt @@ -0,0 +1,36 @@ +@file:Suppress("unused") +package com.theoplayer.broadcast + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.theoplayer.ReactTHEOplayerView +import com.theoplayer.util.ViewResolver + +private const val TAG = "EventBroadcastModule" + +@ReactModule(name = TAG) +class EventBroadcastModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { + + private val viewResolver: ViewResolver + + init { + viewResolver = ViewResolver(context) + } + + override fun getName(): String { + return TAG + } + + /** + * Receive a react-native broadcast event and route it to the player's broadcast adapter. + */ + @ReactMethod + fun broadcastEvent(tag: Int, event: ReadableMap) { + viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> + view?.broadcast?.broadcastEvent(event) + } + } +} diff --git a/ios/THEOplayerRCTBridge.m b/ios/THEOplayerRCTBridge.m index f5d351b01..a43a1d233 100644 --- a/ios/THEOplayerRCTBridge.m +++ b/ios/THEOplayerRCTBridge.m @@ -239,3 +239,13 @@ @interface RCT_EXTERN_REMAP_MODULE(CacheModule, THEOplayerRCTCacheAPI, RCTEventE drmConfig:(NSDictionary)drmConfig) @end + +// ---------------------------------------------------------------------------- +// Broadcast Module +// ---------------------------------------------------------------------------- +@interface RCT_EXTERN_REMAP_MODULE(EventBroadcastModule, THEOplayerRCTEventBroadcastAPI, NSObject) + +RCT_EXTERN_METHOD(broadcastEvent:(nonnull NSNumber *)node + event:(NSDictionary)event) + +@end diff --git a/ios/THEOplayerRCTTypeUtils.swift b/ios/THEOplayerRCTTypeUtils.swift index 834b3d965..fda669ad6 100644 --- a/ios/THEOplayerRCTTypeUtils.swift +++ b/ios/THEOplayerRCTTypeUtils.swift @@ -98,6 +98,23 @@ class THEOplayerRCTTypeUtils { return TextTrackStyleEdgeStyle.none } } + + class func adIntegrationKind(_ integration: String) -> AdIntegrationKind { + switch integration { + case "theo": + return AdIntegrationKind.theo + case "freewheel": + return AdIntegrationKind.freewheel + case "google-ima": + return AdIntegrationKind.google_ima + case "google-dai": + return AdIntegrationKind.google_dai + case "": + return AdIntegrationKind.defaultKind + default: + return AdIntegrationKind.defaultKind + } + } #if os(iOS) class func cacheStatusToString(_ status: CacheStatus) -> String { diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 75206c3d3..e801849b6 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -8,6 +8,7 @@ public class THEOplayerRCTView: UIView { // MARK: Members public private(set) var player: THEOplayer? public private(set) var mainEventHandler: THEOplayerRCTMainEventHandler + public private(set) var broadcastEventHandler: THEOplayerRCTBroadcastEventHandler var textTrackEventHandler: THEOplayerRCTTextTrackEventHandler var mediaTrackEventHandler: THEOplayerRCTMediaTrackEventHandler var adEventHandler: THEOplayerRCTAdsEventHandler @@ -44,6 +45,7 @@ public class THEOplayerRCTView: UIView { init() { // create event handlers to maintain event props self.mainEventHandler = THEOplayerRCTMainEventHandler() + self.broadcastEventHandler = THEOplayerRCTBroadcastEventHandler() self.textTrackEventHandler = THEOplayerRCTTextTrackEventHandler() self.mediaTrackEventHandler = THEOplayerRCTMediaTrackEventHandler() self.adEventHandler = THEOplayerRCTAdsEventHandler() diff --git a/ios/ads/THEOplayerRCTAdAggregator.swift b/ios/ads/THEOplayerRCTAdAdapter.swift similarity index 53% rename from ios/ads/THEOplayerRCTAdAggregator.swift rename to ios/ads/THEOplayerRCTAdAdapter.swift index 68187b166..c734b530d 100644 --- a/ios/ads/THEOplayerRCTAdAggregator.swift +++ b/ios/ads/THEOplayerRCTAdAdapter.swift @@ -1,4 +1,4 @@ -// THEOplayerRCTAdAggregator.swift +// THEOplayerRCTAdAdapter.swift import Foundation import THEOplayerSDK @@ -38,10 +38,9 @@ let PROP_COMPANION_WIDTH: String = "width" let PROP_COMPANION_HEIGHT: String = "height" let PROP_COMPANION_RESOURCE_URI: String = "resourceURI" -class THEOplayerRCTAdAggregator { +class THEOplayerRCTAdAdapter { -#if (GOOGLE_IMA || GOOGLE_DAI) || canImport(THEOplayerGoogleIMAIntegration) - class func aggregateAd(ad: Ad, processAdBreak: Bool = true) -> [String:Any] { + class func fromAd(ad: Ad, processAdBreak: Bool = true) -> [String:Any] { var adData: [String:Any] = [:] adData[PROP_AD_INTEGRATION] = ad.integration._rawValue adData[PROP_AD_TYPE] = ad.type @@ -56,11 +55,11 @@ class THEOplayerRCTAdAggregator { } if processAdBreak, let adBreak = ad.adBreak { - adData[PROP_AD_BREAK] = THEOplayerRCTAdAggregator.aggregateAdBreak(adBreak: adBreak) + adData[PROP_AD_BREAK] = THEOplayerRCTAdAdapter.fromAdBreak(adBreak: adBreak) } #if os(iOS) - adData[PROP_AD_COMPANIONS] = THEOplayerRCTAdAggregator.aggregateCompanionAds(companionAds: ad.companions) + adData[PROP_AD_COMPANIONS] = THEOplayerRCTAdAdapter.fromCompanionAds(companionAds: ad.companions) #endif adData[PROP_AD_UNIVERSAL_AD_IDS] = [] @@ -91,7 +90,7 @@ class THEOplayerRCTAdAggregator { } let traffickingParametersString = googleImaAd.traffickingParameters adData[PROP_GOOGLE_AD_TRAFFICKING_PARAMETERS_STRING] = traffickingParametersString - if let traffickingParameters = THEOplayerRCTAdAggregator.aggregateTraffickingParameters(traffickingParametersString: traffickingParametersString) { + if let traffickingParameters = THEOplayerRCTAdAdapter.fromTraffickingParameters(traffickingParametersString: traffickingParametersString) { adData[PROP_GOOGLE_AD_TRAFFICKING_PARAMETERS] = traffickingParameters } adData[PROP_GOOGLE_AD_BITRATE] = googleImaAd.vastMediaBitrate @@ -102,14 +101,7 @@ class THEOplayerRCTAdAggregator { adData[PROP_GOOGLE_AD_HEIGHT] = height } if !googleImaAd.universalAdIds.isEmpty { - var adIdList: [[String:Any]] = [] - for adId in googleImaAd.universalAdIds { - var adIdData: [String:Any] = [:] - adIdData[PROP_GOOGLE_AD_ID_REGISTRY] = adId.adIdRegistry - adIdData[PROP_GOOGLE_AD_ID_VALUE] = adId.adIdValue - adIdList.append(adIdData) - } - adData[PROP_AD_UNIVERSAL_AD_IDS] = adIdList + adData[PROP_AD_UNIVERSAL_AD_IDS] = THEOplayerRCTAdAdapter.fromUniversalAdIds(universalAdIds: googleImaAd.universalAdIds) } adData[PROP_GOOGLE_AD_WRAPPER_AD_IDS] = googleImaAd.wrapperAdIds adData[PROP_GOOGLE_AD_WRAPPER_AD_SYSTEMS] = googleImaAd.wrapperAdSystems @@ -119,7 +111,64 @@ class THEOplayerRCTAdAggregator { return adData } - class func aggregateAdBreak(adBreak: AdBreak) -> [String:Any] { + private class func fromUniversalAdIds(universalAdIds: [THEOplayerSDK.UniversalAdId]?) -> [[String:Any]] { + guard let universalAdIds = universalAdIds else { + return [] + } + + var adIdList: [[String:Any]] = [] + for adId in universalAdIds { + var adIdData: [String:Any] = [:] + adIdData[PROP_GOOGLE_AD_ID_REGISTRY] = adId.adIdRegistry + adIdData[PROP_GOOGLE_AD_ID_VALUE] = adId.adIdValue + adIdList.append(adIdData) + } + + return adIdList + } + + class func toAd(adData: [String:Any]?) -> NativeAd? { + guard let adData = adData else { + return nil + } + + return NativeLinearGoogleImaAd(adBreak: THEOplayerRCTAdAdapter.toAdBreak(adBreakData: adData[PROP_AD_BREAK] as? [String:Any]), + companions: THEOplayerRCTAdAdapter.toCompanionAds(companiondAdsData: adData[PROP_AD_COMPANIONS] as? [[String : Any]]), + type: (adData[PROP_AD_TYPE] as? String) ?? "", + id: adData[PROP_AD_ID] as? String, + skipOffset: adData[PROP_AD_SKIP_OFFSET] as? Int, + resourceURI: adData[PROP_AD_RESOURCE_URI] as? String, + width: adData[PROP_GOOGLE_AD_WIDTH] as? Int, + height: adData[PROP_GOOGLE_AD_HEIGHT] as? Int, + integration: THEOplayerRCTTypeUtils.adIntegrationKind((adData[PROP_AD_INTEGRATION] as? String) ?? ""), + duration: lround(Double((adData[PROP_AD_DURATION] as? Int) ?? 0) * 0.001), // msec -> sec + mediaFiles: [], // TODO + adSystem: adData[PROP_GOOGLE_AD_AD_SYSTEM] as? String, + creativeId: adData[PROP_GOOGLE_AD_CREATIVE_ID] as? String, + wrapperAdIds: (adData[PROP_GOOGLE_AD_WRAPPER_AD_IDS] as? [String]) ?? [], + wrapperAdSystems: (adData[PROP_GOOGLE_AD_WRAPPER_AD_SYSTEMS] as? [String]) ?? [], + wrapperCreativeIds: (adData[PROP_GOOGLE_AD_WRAPPER_CREATIVE_IDS] as? [String] ?? []), + vastMediaBitrate: (adData[PROP_GOOGLE_AD_BITRATE] as? Int) ?? 0, + universalAdIds: THEOplayerRCTAdAdapter.toUniversalAdIds(universalAdIdsData: adData[PROP_AD_UNIVERSAL_AD_IDS] as? [[String:Any]]), + traffickingParameters: "") // TODO + } + + private class func toUniversalAdIds(universalAdIdsData: [[String:Any]]?) -> [THEOplayerSDK.UniversalAdId] { + guard let universalAdIdsData = universalAdIdsData else { + return [] + } + + var adIdList: [THEOplayerSDK.UniversalAdId] = [] + + for adIdData in universalAdIdsData { + adIdList.append(NativeUniversalAdId(adIdValue: (adIdData[PROP_GOOGLE_AD_ID_VALUE] as? String) ?? "", + adIdRegistry: (adIdData[PROP_GOOGLE_AD_ID_REGISTRY] as? String) ?? "")) + } + + return adIdList + } + + class func fromAdBreak(adBreak: AdBreak) -> [String:Any] { var adBreakData: [String:Any] = [:] adBreakData[PROP_ADBREAK_MAX_DURATION] = adBreak.maxDuration * 1000 // sec -> msec adBreakData[PROP_ADBREAK_TIME_OFFSET] = adBreak.timeOffset * 1000 // sec -> msec @@ -128,7 +177,7 @@ class THEOplayerRCTAdAggregator { if !adBreak.ads.isEmpty { var adList: [[String:Any]] = [] for ad in adBreak.ads { - adList.append(THEOplayerRCTAdAggregator.aggregateAd(ad: ad, processAdBreak: false)) + adList.append(THEOplayerRCTAdAdapter.fromAd(ad: ad, processAdBreak: false)) } adBreakData[PROP_ADBREAK_ADS] = adList if adList.count > 0, @@ -139,7 +188,27 @@ class THEOplayerRCTAdAggregator { return adBreakData } - class private func aggregateCompanionAds(companionAds: [CompanionAd?]) -> [[String:Any]] { + class func toAdBreak(adBreakData: [String:Any]?) -> NativeAdBreak? { + guard let adBreakData = adBreakData else { + return nil + } + + var ads: [NativeAd] = [] + if let adsData = adBreakData[PROP_ADBREAK_ADS] as? [[String:Any]] { + for adData in adsData { + if let ad = THEOplayerRCTAdAdapter.toAd(adData: adData) { + ads.append(ad) + } + } + } + + return NativeAdBreak(ads: ads, + maxDuration: lround(Double((adBreakData[PROP_ADBREAK_MAX_DURATION] as? Int) ?? 0) * 0.001), // msec -> sec, + maxRemainingDuration: Double((adBreakData[PROP_ADBREAK_MAX_REMAINING_DURATION] as? Int) ?? 0) * 0.001, // msec -> sec, + timeOffset: lround(Double((adBreakData[PROP_ADBREAK_TIME_OFFSET] as? Int) ?? 0) * 0.001)) // msec -> sec, + } + + class private func fromCompanionAds(companionAds: [CompanionAd?]) -> [[String:Any]] { var companionAdsData: [[String:Any]] = [] for cAd in companionAds { if let companionAd = cAd { @@ -156,12 +225,15 @@ class THEOplayerRCTAdAggregator { return companionAdsData } - class private func aggregateTraffickingParameters(traffickingParametersString: String) -> [String:Any]? { + class func toCompanionAds(companiondAdsData: [[String:Any]]?) -> [CompanionAd?] { + return [] + } + + class private func fromTraffickingParameters(traffickingParametersString: String) -> [String:Any]? { if let data = traffickingParametersString.data(using: .utf8) { return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any] } return nil } -#endif } diff --git a/ios/ads/THEOplayerRCTAdEventAdapter.swift b/ios/ads/THEOplayerRCTAdEventAdapter.swift new file mode 100644 index 000000000..6bf0fa9ff --- /dev/null +++ b/ios/ads/THEOplayerRCTAdEventAdapter.swift @@ -0,0 +1,27 @@ +// THEOplayerRCTAdEventAdapter.swift + +import Foundation +import THEOplayerSDK + +let ADEVENT_PROP_SUBTYPE: String = "subType" +let ADEVENT_PROP_AD: String = "ad" + +class THEOplayerRCTAdEventAdapter { + class func toAdEvent(eventData: NSDictionary) -> THEOplayerSDK.EventProtocol? { + if let subtype = eventData[ADEVENT_PROP_SUBTYPE] as? String { + let adData = eventData[ADEVENT_PROP_AD] as? [String:Any] + switch (subtype) { + case "adloaded": return THEOplayerSDK.AdLoadedEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "adbreakbegin": return THEOplayerSDK.AdBreakBeginEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAdBreak(adBreakData: adData)) + case "adbegin": return THEOplayerSDK.AdBeginEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "adfirstquartile": return THEOplayerSDK.AdFirstQuartileEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "admidpoint": return THEOplayerSDK.AdMidpointEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "adthirdquartile": return THEOplayerSDK.AdThirdQuartileEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "adend": return THEOplayerSDK.AdEndEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAd(adData: adData)) + case "adbreakend": return THEOplayerSDK.AdBreakEndEvent(date: Date(), ad: THEOplayerRCTAdAdapter.toAdBreak(adBreakData: adData)) + default: return nil + } + } + return nil + } +} diff --git a/ios/ads/THEOplayerRCTAdsAPI.swift b/ios/ads/THEOplayerRCTAdsAPI.swift index ab2270524..02aea724a 100644 --- a/ios/ads/THEOplayerRCTAdsAPI.swift +++ b/ios/ads/THEOplayerRCTAdsAPI.swift @@ -67,7 +67,7 @@ class THEOplayerRCTAdsAPI: NSObject, RCTBridgeModule { let theView = self.bridge.uiManager.view(forReactTag: node) as! THEOplayerRCTView if let ads = theView.ads(), let currentAdBreak = ads.currentAdBreak { - resolve(THEOplayerRCTAdAggregator.aggregateAdBreak(adBreak:currentAdBreak)) + resolve(THEOplayerRCTAdAdapter.fromAdBreak(adBreak:currentAdBreak)) } else { reject(ERROR_CODE_ADS_ACCESS_FAILURE, ERROR_MESSAGE_ADS_ACCESS_FAILURE, nil) if DEBUG_ADS_API { PrintUtils.printLog(logText: "[NATIVE] Could not retrieve current adbreak (ads module unavailable).") } @@ -83,7 +83,7 @@ class THEOplayerRCTAdsAPI: NSObject, RCTBridgeModule { let currentAdsArray = ads.currentAds var currentAds: [[String:Any]] = [] for ad in currentAdsArray { - currentAds.append(THEOplayerRCTAdAggregator.aggregateAd(ad: ad)) + currentAds.append(THEOplayerRCTAdAdapter.fromAd(ad: ad)) } resolve(currentAds) } else { @@ -101,7 +101,7 @@ class THEOplayerRCTAdsAPI: NSObject, RCTBridgeModule { let currentAdBreaksArray = ads.scheduledAdBreaks var currentAdBreaks: [[String:Any]] = [] for adbreak in currentAdBreaksArray { - currentAdBreaks.append(THEOplayerRCTAdAggregator.aggregateAdBreak(adBreak: adbreak)) + currentAdBreaks.append(THEOplayerRCTAdAdapter.fromAdBreak(adBreak: adbreak)) } resolve(currentAdBreaks) } else { diff --git a/ios/ads/THEOplayerRCTAdsEventHandler.swift b/ios/ads/THEOplayerRCTAdsEventHandler.swift index e6eb47a9f..4c5b0e35d 100644 --- a/ios/ads/THEOplayerRCTAdsEventHandler.swift +++ b/ios/ads/THEOplayerRCTAdsEventHandler.swift @@ -62,7 +62,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_BEGIN, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -75,7 +75,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_END, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -88,7 +88,7 @@ class THEOplayerRCTAdsEventHandler { let adBreak = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_ADBREAK_BEGIN, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAdBreak(adBreak: adBreak) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAdBreak(adBreak: adBreak) ]) } } @@ -101,7 +101,7 @@ class THEOplayerRCTAdsEventHandler { let adBreak = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_ADBREAK_END, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAdBreak(adBreak: adBreak) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAdBreak(adBreak: adBreak) ]) } } @@ -114,7 +114,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_ERROR, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -127,7 +127,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_FIRST_QUARTILE, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -140,7 +140,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_MIDPOINT, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -153,7 +153,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_THIRD_QUARTILE, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } @@ -166,7 +166,7 @@ class THEOplayerRCTAdsEventHandler { let ad = event.ad { forwardedAdEvent([ AD_EVENT_PROP_TYPE: EVENT_TYPE_AD_LOADED, - AD_EVENT_PROP_AD: THEOplayerRCTAdAggregator.aggregateAd(ad: ad) + AD_EVENT_PROP_AD: THEOplayerRCTAdAdapter.fromAd(ad: ad) ]) } } diff --git a/ios/ads/THEOplayerRCTAdsNative.swift b/ios/ads/THEOplayerRCTAdsNative.swift new file mode 100644 index 000000000..4709522c5 --- /dev/null +++ b/ios/ads/THEOplayerRCTAdsNative.swift @@ -0,0 +1,159 @@ +// THEOplayerRCTAdsNative.swift + +import Foundation +import THEOplayerSDK + +class NativeAd: THEOplayerSDK.Ad { + /** A reference to the `AdBreak` of which the ad is a part of.*/ + var adBreak: AdBreak? = nil + /** An array of `CompanionAd`s associated to the ad, if available within the same Creatives element.*/ + var companions: [THEOplayerSDK.CompanionAd?] = [] + /** Either 'linear' or 'nonlinear', depending on the concrete implementer.*/ + var type: String = "" + /** The identifier of the creative, provided in the VAST-file.*/ + var id: String? = nil + /** When the Ad can be skipped, in seconds.*/ + var skipOffset: Int? = nil + /**The URI of the the ad content.*/ + var resourceURI: String? = nil + /** The width of the advertisement, in pixels.*/ + var width: Int? = nil + /** The height of the advertisement, in pixels.*/ + var height: Int? = nil + /** The kind of the ad integration.*/ + var integration: THEOplayerSDK.AdIntegrationKind = THEOplayerSDK.AdIntegrationKind.defaultKind + + init(adBreak: AdBreak? = nil, companions: [THEOplayerSDK.CompanionAd?], type: String, id: String? = nil, skipOffset: Int? = nil, resourceURI: String? = nil, width: Int? = nil, height: Int? = nil, integration: THEOplayerSDK.AdIntegrationKind) { + self.adBreak = adBreak + self.companions = companions + self.type = type + self.id = id + self.skipOffset = skipOffset + self.resourceURI = resourceURI + self.width = width + self.height = height + self.integration = integration + } +} + +class NativeLinearAd: NativeAd, THEOplayerSDK.LinearAd { + /** The duration of the LinearAd, as provided by the VAST file, in seconds.*/ + var duration: Int? = 0 + /** An array of mediafiles, which provides some meta data retrieved from the VAST file.*/ + var mediaFiles: [THEOplayerSDK.MediaFile] = [] + + init(adBreak: AdBreak? = nil, companions: [THEOplayerSDK.CompanionAd?], type: String, id: String? = nil, skipOffset: Int? = nil, resourceURI: String? = nil, width: Int? = nil, height: Int? = nil, integration: THEOplayerSDK.AdIntegrationKind, duration: Int? = 0, mediaFiles: [THEOplayerSDK.MediaFile] = []) { + + self.duration = duration + self.mediaFiles = mediaFiles + + super.init(adBreak:adBreak, + companions: companions, + type: type, + id: id, + skipOffset: skipOffset, + resourceURI: resourceURI, + width: width, + height: height, + integration: integration) + } +} + +class NativeLinearGoogleImaAd: NativeLinearAd, THEOplayerSDK.GoogleImaAd { + /** The source ad server information included in the ad response.*/ + var adSystem: String? = nil + /** The identifier of the selected creative for the ad.*/ + var creativeId: String? = nil + /** The list of wrapper ad identifiers as specified in the VAST response.*/ + var wrapperAdIds: [String] = [] + /** The list of wrapper ad systems as specified in the VAST response.*/ + var wrapperAdSystems: [String] = [] + /** The list of wrapper creative identifiers.*/ + var wrapperCreativeIds: [String] = [] + /** The bitrate of the currently playing creative as listed in the VAST response.*/ + var vastMediaBitrate: Int = 0 + /** The list of universal ad ID information of the selected creative for the ad.*/ + var universalAdIds: [UniversalAdId] = [] + /** The String representing custom trafficking parameters from the VAST response.*/ + var traffickingParameters: String = "" + + init(adBreak: AdBreak? = nil, companions: [THEOplayerSDK.CompanionAd?], type: String, id: String? = nil, skipOffset: Int? = nil, resourceURI: String? = nil, width: Int? = nil, height: Int? = nil, integration: THEOplayerSDK.AdIntegrationKind, duration: Int? = 0, mediaFiles: [THEOplayerSDK.MediaFile] = [], adSystem: String? = nil, creativeId: String? = nil, wrapperAdIds: [String], wrapperAdSystems: [String], wrapperCreativeIds: [String], vastMediaBitrate: Int, universalAdIds: [UniversalAdId], traffickingParameters: String) { + self.adSystem = adSystem + self.creativeId = creativeId + self.wrapperAdIds = wrapperAdIds + self.wrapperAdSystems = wrapperAdSystems + self.wrapperCreativeIds = wrapperCreativeIds + self.vastMediaBitrate = vastMediaBitrate + self.universalAdIds = universalAdIds + self.traffickingParameters = traffickingParameters + + super.init(adBreak: adBreak, + companions: companions, + type: type, + id: id, + skipOffset: skipOffset, + resourceURI: resourceURI, + width: width, + height: height, + integration:integration, + duration: duration, + mediaFiles: mediaFiles) + } +} + +class NativeAdBreak: THEOplayerSDK.AdBreak { + /** An array of all the ads that are available in the current AdBreak.*/ + var ads: [Ad] = [] + /**Indicates the duration of the ad break, in seconds.*/ + var maxDuration: Int = -1 + /** Indicates the remaining duration of the ad break, in seconds.*/ + var maxRemainingDuration: Double = -1 + /** The time offset at which point the content will be paused to play the ad break, in seconds.*/ + var timeOffset: Int = 0 + + init(ads: [Ad], maxDuration: Int, maxRemainingDuration: Double, timeOffset: Int) { + self.ads = ads + self.maxDuration = maxDuration + self.maxRemainingDuration = maxRemainingDuration + self.timeOffset = timeOffset + } +} + +class NativeCompanionAd: THEOplayerSDK.CompanionAd { +/** An identifier of the element in which the companion ad should be appended, if available.*/ + var adSlotId: String? = nil + /** An alternative description for the companion ad.*/ + var altText: String? = nil + /** The website of the advertisement.*/ + var clickThrough: String? = nil + /** The height of the companion ad, in pixels.*/ + var height: Int? = nil + /** The URI of the ad content.*/ + var resourceURI: String? = nil + /** The width of the companion ad, in pixels.*/ + var width: Int? = nil + /** The type of the companion ad.*/ + var type: String = "" + + init(adSlotId: String? = nil, altText: String? = nil, clickThrough: String? = nil, height: Int? = nil, resourceURI: String? = nil, width: Int? = nil, type: String) { + self.adSlotId = adSlotId + self.altText = altText + self.clickThrough = clickThrough + self.height = height + self.resourceURI = resourceURI + self.width = width + self.type = type + } +} + +class NativeUniversalAdId: THEOplayerSDK.UniversalAdId { + /** The Universal Ad identifier of the selected creative for the ad.*/ + var adIdValue: String = "" + /** The registry associated with cataloging the UniversalAdId of the selected creative for the ad.*/ + var adIdRegistry: String = "" + + init(adIdValue: String, adIdRegistry: String) { + self.adIdValue = adIdValue + self.adIdRegistry = adIdRegistry + } +} diff --git a/ios/eventBroadcasting/THEOplayerRCTBroadcastEventHandler.swift b/ios/eventBroadcasting/THEOplayerRCTBroadcastEventHandler.swift new file mode 100644 index 000000000..df1ce9015 --- /dev/null +++ b/ios/eventBroadcasting/THEOplayerRCTBroadcastEventHandler.swift @@ -0,0 +1,99 @@ +// +// THEOplayerRCTBroadcastEventHandler.swift +// + +import Foundation +import THEOplayerSDK + +let EVENT_PROP_TYPE: String = "type" + +public class THEOplayerRCTBroadcastEventHandler: DefaultEventDispatcher { + public func broadcastEvent(eventData: NSDictionary) { + if let nativeEvent = self.toEvent(eventData: eventData) { + self.dispatchEvent(event: nativeEvent) + } + } + + private func toEvent(eventData: NSDictionary) -> THEOplayerSDK.EventProtocol? { + if let type = eventData[EVENT_PROP_TYPE] as? String { + switch (type) { + case "adevent": return THEOplayerRCTAdEventAdapter.toAdEvent(eventData: eventData) + // more cases to be added on demand... + default: return nil + } + } + + return nil + } +} + +public class DefaultEventDispatcher: NSObject, THEOplayerSDK.EventDispatcherProtocol, THEOplayerSDK.DispatchDispatch { + private var eventListeners = [String: [EventListenerWrapper]]() + + public func addEventListener(type: THEOplayerSDK.EventType, listener: @escaping (_ : E) -> ()) -> THEOplayerSDK.EventListener { + let eventListener = DefaultEventListenerWrapper(target: self, listener: listener) + if self.eventListeners[type.name] != nil { + self.eventListeners[type.name]?.append(eventListener) + } else { + self.eventListeners[type.name] = [eventListener] + } + return eventListener + } + + public func removeEventListener(type: THEOplayerSDK.EventType, listener: THEOplayerSDK.EventListener) { + guard let eventListener = listener as? EventListenerWrapper else { return } + self.eventListeners[type.name]?.removeAll { $0 === eventListener } + } + + public func dispatchEvent(event: THEOplayerSDK.EventProtocol) { + if let listeners = self.eventListeners[event.type] { + for listener in listeners { + listener.invoke(event: event) + } + } + } + + public func removeEventListeners() { + for listeners in self.eventListeners.values { + for listener in listeners { + listener.destroy() + } + } + self.eventListeners = [:] + } + + func contains(type: THEOplayerSDK.EventType, listener: THEOplayerSDK.EventListener) -> Bool { + guard let listeners: [EventListenerWrapper] = self.eventListeners[type.name] else { + return false + } + + return listeners.first { $0 === listener } != nil + } +} + +class DefaultEventListenerWrapper: EventListenerWrapper, THEOplayerSDK.EventListener { + weak private var target: T? + var listener: ((T) -> (E) -> ())? + + init(target: T, listener: @escaping (E) -> ()) { + self.target = target + func wrappedListener(_:T) -> (E) -> () { return listener } + self.listener = wrappedListener + } + + func invoke(event: THEOplayerSDK.EventProtocol) { + if let target = self.target { + listener?(target)(event as! E) + } + } + + func destroy() { + self.listener = nil + } +} + +protocol EventListenerWrapper: AnyObject { + func invoke(event: THEOplayerSDK.EventProtocol) + func destroy() +} + diff --git a/ios/eventBroadcasting/THEOplayerRCTEventBroadcastAPI.swift b/ios/eventBroadcasting/THEOplayerRCTEventBroadcastAPI.swift new file mode 100644 index 000000000..01c99758b --- /dev/null +++ b/ios/eventBroadcasting/THEOplayerRCTEventBroadcastAPI.swift @@ -0,0 +1,33 @@ +// +// THEOplayerRCTPlayerAPI.swift +// + +import Foundation +import THEOplayerSDK + +protocol EventReceiver { + func onReceivedEvent() +} + +@objc(THEOplayerRCTEventBroadcastAPI) +class THEOplayerRCTBroadcastAPI: NSObject, RCTBridgeModule { + @objc var bridge: RCTBridge! + + + static func moduleName() -> String! { + return "EventBroadcastModule" + } + + static func requiresMainQueueSetup() -> Bool { + return false + } + + @objc(broadcastEvent:event:) + func broadcastEvent(_ node: NSNumber, event: NSDictionary) -> Void { + DispatchQueue.main.async { + if let theView = self.bridge.uiManager.view(forReactTag: node) as? THEOplayerRCTView { + theView.broadcastEventHandler.broadcastEvent(eventData: event) + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 2948391e6..fe72c579e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "react-native": "^0.72.5", "react-native-builder-bob": "^0.18.3", "release-it": "^16.2.1", - "theoplayer": "^6.1.0", + "theoplayer": "^6.5.0", "typescript": "^4.9.5" }, "peerDependencies": { @@ -16305,9 +16305,9 @@ "dev": true }, "node_modules/theoplayer": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-6.1.0.tgz", - "integrity": "sha512-/G9ri0YvJRI7JafFVVyn/bY/Mx15D4v0ewOFQJBcN7TvBgAQhE02Kx/ik/0CccrbpnQGYMdGGr0lHI1TXH5D6Q==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-6.5.0.tgz", + "integrity": "sha512-7GtkSj2Z/IwtyuvuN6ot76veIAK0zEmR6nnrgnJI5OrvG4uNbI6dgr4iip3ySKC0lUsMSnospmQAXzEeeIi0SA==", "dev": true }, "node_modules/throat": { diff --git a/package.json b/package.json index 8d5413fdc..5f1d20764 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "react-native": "^0.72.5", "react-native-builder-bob": "^0.18.3", "release-it": "^16.2.1", - "theoplayer": "^6.1.0", + "theoplayer": "^6.5.0", "typescript": "^4.9.5" }, "peerDependencies": { diff --git a/react-native-theoplayer.podspec b/react-native-theoplayer.podspec index 24205bb26..c6315747c 100644 --- a/react-native-theoplayer.podspec +++ b/react-native-theoplayer.podspec @@ -20,7 +20,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "12.0", :tvos => "12.0" } s.source = { :git => "https://www.theoplayer.com/.git", :tag => "#{s.version}" } - s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift' + s.source_files = 'ios/*.{h,m,swift}', 'ios/ads/*.swift', 'ios/casting/*.swift', 'ios/contentprotection/*.swift', 'ios/pip/*.swift', 'ios/backgroundAudio/*.swift', 'ios/cache/*.swift', 'ios/eventBroadcasting/*.swift' s.resources = ['ios/*.css'] # ReactNative Dependency diff --git a/src/api/barrel.ts b/src/api/barrel.ts index c3df6cdcc..cf27be114 100644 --- a/src/api/barrel.ts +++ b/src/api/barrel.ts @@ -1,5 +1,7 @@ export * from './abr/barrel'; export * from './ads/barrel'; +export * from './backgroundAudio/barrel'; +export * from './broadcast/barrel'; export * from './cache/barrel'; export * from './cast/barrel'; export * from './pip/barrel'; diff --git a/src/api/broadcast/EventBroadcastAPI.ts b/src/api/broadcast/EventBroadcastAPI.ts new file mode 100644 index 000000000..4b4126b38 --- /dev/null +++ b/src/api/broadcast/EventBroadcastAPI.ts @@ -0,0 +1,13 @@ +import type { Event } from '../event/Event'; + +/** + * The EventBroadcastAPI allows dispatching extra player events to both ReactNative and native listeners. + * + * @internal + */ +export interface EventBroadcastAPI { + /** + * Broadcast an event. + */ + broadcastEvent(event: Event) : void; +} diff --git a/src/api/broadcast/barrel.ts b/src/api/broadcast/barrel.ts new file mode 100644 index 000000000..56a237940 --- /dev/null +++ b/src/api/broadcast/barrel.ts @@ -0,0 +1 @@ +export * from './EventBroadcastAPI'; diff --git a/src/api/player/THEOplayer.ts b/src/api/player/THEOplayer.ts index a59bcde90..1eabc84bc 100644 --- a/src/api/player/THEOplayer.ts +++ b/src/api/player/THEOplayer.ts @@ -12,6 +12,7 @@ import type { PresentationMode } from '../presentation/PresentationMode'; import type { PiPConfiguration } from '../pip/PiPConfiguration'; import type { BackgroundAudioConfiguration } from '../backgroundAudio/BackgroundAudioConfiguration'; import type { PlayerVersion } from './PlayerVersion'; +import type { EventBroadcastAPI } from "../broadcast/EventBroadcastAPI"; export type PreloadType = 'none' | 'metadata' | 'auto' | ''; @@ -212,4 +213,9 @@ export interface THEOplayer extends EventDispatcher { * Native player handle. */ readonly nativeHandle: NativeHandleType; + + /** + * Event Broadcast API. + */ + readonly broadcast: EventBroadcastAPI; } diff --git a/src/internal/adapter/THEOplayerAdapter.ts b/src/internal/adapter/THEOplayerAdapter.ts index 5566d41e9..fe31c47b4 100644 --- a/src/internal/adapter/THEOplayerAdapter.ts +++ b/src/internal/adapter/THEOplayerAdapter.ts @@ -3,7 +3,7 @@ import type { ABRConfiguration, AdsAPI, CastAPI, - DurationChangeEvent, + DurationChangeEvent, EventBroadcastAPI, LoadedMetadataEvent, MediaTrack, MediaTrackEvent, @@ -26,6 +26,7 @@ import { addTextTrackCue, addTrack, AspectRatio, + BackgroundAudioConfiguration, findMediaTrackByUid, findTextTrackByUid, MediaTrackEventType, @@ -46,8 +47,8 @@ import { THEOplayerNativeCastAdapter } from './cast/THEOplayerNativeCastAdapter' import { AbrAdapter } from './abr/AbrAdapter'; import { NativeModules, Platform } from 'react-native'; import { TextTrackStyleAdapter } from './track/TextTrackStyleAdapter'; -import type { BackgroundAudioConfiguration } from 'src/api/backgroundAudio/BackgroundAudioConfiguration'; import type { NativePlayerState } from './NativePlayerState'; +import { EventBroadcastAdapter } from "./broadcast/EventBroadcastAdapter"; const defaultPlayerState: NativePlayerState = { source: undefined, @@ -82,6 +83,7 @@ export class THEOplayerAdapter extends DefaultEventDispatcher im private readonly _castAdapter: THEOplayerNativeCastAdapter; private readonly _abrAdapter: AbrAdapter; private readonly _textTrackStyleAdapter: TextTrackStyleAdapter; + private _externalEventRouter: EventBroadcastAPI | undefined = undefined; private _playerVersion!: PlayerVersion; constructor(view: THEOplayerView, initialState: NativePlayerState = defaultPlayerState) { @@ -518,10 +520,16 @@ export class THEOplayerAdapter extends DefaultEventDispatcher im return this._playerVersion; } + // @internal get nativeHandle(): NativeHandleType { return this._view.nativeHandle; } + // @internal + get broadcast(): EventBroadcastAPI { + return this._externalEventRouter ?? (this._externalEventRouter = new EventBroadcastAdapter(this)); + } + initializeFromNativePlayer_(version: PlayerVersion, state: NativePlayerState | undefined) { this._playerVersion = version; if (state) { diff --git a/src/internal/adapter/THEOplayerWebAdapter.ts b/src/internal/adapter/THEOplayerWebAdapter.ts index 4d75b2464..a4fb372d3 100644 --- a/src/internal/adapter/THEOplayerWebAdapter.ts +++ b/src/internal/adapter/THEOplayerWebAdapter.ts @@ -12,7 +12,7 @@ import type { TextTrackStyle, THEOplayer, } from 'react-native-theoplayer'; -import { AspectRatio, PlayerEventType, PresentationMode } from 'react-native-theoplayer'; +import { AspectRatio, EventBroadcastAPI, PlayerEventType, PresentationMode } from 'react-native-theoplayer'; import { THEOplayerWebAdsAdapter } from './ads/THEOplayerWebAdsAdapter'; import { THEOplayerWebCastAdapter } from './cast/THEOplayerWebCastAdapter'; import { ChromelessPlayer as NativeChromelessPlayer, SourceDescription as NativeSourceDescription, version as nativeVersion } from 'theoplayer'; @@ -25,6 +25,7 @@ import type { BackgroundAudioConfiguration } from 'src/api/backgroundAudio/Backg import { WebPresentationModeManager } from './web/WebPresentationModeManager'; import { WebMediaSession } from './web/WebMediaSession'; import { BaseEvent } from './event/BaseEvent'; +import { EventBroadcastAdapter } from "./broadcast/EventBroadcastAdapter"; const defaultBackgroundAudioConfiguration: BackgroundAudioConfiguration = { enabled: false, @@ -44,6 +45,7 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher private _targetVideoQuality: number | number[] | undefined = undefined; private _backgroundAudioConfiguration: BackgroundAudioConfiguration = defaultBackgroundAudioConfiguration; private _pipConfiguration: PiPConfiguration = defaultPipConfiguration; + private _externalEventRouter: EventBroadcastAPI | undefined = undefined; constructor(player: NativeChromelessPlayer, config?: PlayerConfiguration) { super(); @@ -318,10 +320,6 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher this._player = undefined; } - get nativeHandle(): NativeHandleType { - return this._player; - } - private readonly onVisibilityChange = () => { if (!this._player) { return; @@ -335,4 +333,12 @@ export class THEOplayerWebAdapter extends DefaultEventDispatcher // Apply media session controls this._mediaSession?.updateActionHandlers(); }; + + get nativeHandle(): NativeHandleType { + return this._player; + } + + get broadcast(): EventBroadcastAPI { + return this._externalEventRouter ?? (this._externalEventRouter = new EventBroadcastAdapter(this)); + } } diff --git a/src/internal/adapter/broadcast/EventBroadcastAdapter.ts b/src/internal/adapter/broadcast/EventBroadcastAdapter.ts new file mode 100644 index 000000000..5dcad953c --- /dev/null +++ b/src/internal/adapter/broadcast/EventBroadcastAdapter.ts @@ -0,0 +1,20 @@ +import type { EventBroadcastAPI, PlayerEventMap, THEOplayer } from 'react-native-theoplayer'; +import { NativeModules } from 'react-native'; +import type { THEOplayerAdapter } from '../THEOplayerAdapter'; +import type { StringKeyOf } from '../../../api/event/EventDispatcher'; + +export class EventBroadcastAdapter implements EventBroadcastAPI { + constructor(private _player: THEOplayer) {} + + broadcastEvent>(event: PlayerEventMap[K]): void { + // Broadcast ReactNative event. + (this._player as THEOplayerAdapter).dispatchEvent(event); + + try { + // Broadcast native event. + NativeModules.EventBroadcastModule.broadcastEvent(this._player.nativeHandle, Object.freeze(event)); + } catch (e) { + console.warn(`EventBroadcastModule not available: ${e}`); + } + } +} diff --git a/src/internal/adapter/broadcast/EventBroadcastAdapter.web.ts b/src/internal/adapter/broadcast/EventBroadcastAdapter.web.ts new file mode 100644 index 000000000..933789983 --- /dev/null +++ b/src/internal/adapter/broadcast/EventBroadcastAdapter.web.ts @@ -0,0 +1,44 @@ +import type { EventBroadcastAPI } from "react-native-theoplayer"; +import type { THEOplayer } from "react-native-theoplayer"; +import type { THEOplayerWebAdapter } from "../THEOplayerWebAdapter"; +import type { StringKeyOf } from "../../../api/event/EventDispatcher"; +import type { PlayerEventMap } from "react-native-theoplayer"; +import type { Event as WebEvent, EventMap } from 'theoplayer'; +import { AdEvent, PlayerEventType } from "react-native-theoplayer"; +import { DefaultWebEventDispatcher } from "./web/DefaultWebEventDispatcher"; + +export class EventBroadcastAdapter extends DefaultWebEventDispatcher> implements EventBroadcastAPI { + + constructor(private _player: THEOplayer) { + super(); + } + + broadcastEvent>(event: PlayerEventMap[K]): void { + // Broadcast ReactNative event. + (this._player as THEOplayerWebAdapter).dispatchEvent(event); + + // Broadcast native event. + const nativeEvent = toNativeEvent(event); + if (nativeEvent) { + this.dispatchEvent(nativeEvent); + } + } +} + +function toNativeEvent>(event: PlayerEventMap[K]): WebEvent | undefined { + switch (event.type) { + case PlayerEventType.AD_EVENT: return toNativeAdEvent(event); + default: { + console.warn(`EventBroadcastAdapter: native event of type ${event?.type}} not supported`) + return undefined + } + } +} + +function toNativeAdEvent(event: AdEvent): WebEvent | undefined { + return { + type: event.subType, + ad: event.ad, + date: event.date + } as WebEvent; +} diff --git a/src/internal/adapter/broadcast/web/DefaultWebEventDispatcher.ts b/src/internal/adapter/broadcast/web/DefaultWebEventDispatcher.ts new file mode 100644 index 000000000..aaccf239c --- /dev/null +++ b/src/internal/adapter/broadcast/web/DefaultWebEventDispatcher.ts @@ -0,0 +1,66 @@ +import type { EventDispatcher, EventMap, StringKeyOf, EventListener } from "theoplayer"; + +export function arrayRemoveElement(array: T[], element: T): boolean { + const index = array.indexOf(element); + if (index === -1) { + return false; + } + arrayRemoveAt(array, index); + return true; +} + +export function arrayRemoveAt(array: T[], index: number): void { + array.splice(index, 1); +} + +export class DefaultWebEventDispatcher>> implements EventDispatcher { + readonly _eventListeners: Map, EventListener]>[]> = new Map(); + + addEventListener>(types: K | K[], listener: EventListener): void { + if (typeof types === 'string') { + this.addSingleEventListener_(types, listener); + } else { + types.forEach(type => { + this.addSingleEventListener_(type, listener); + }) + } + } + + private addSingleEventListener_>(type: K, listener: EventListener): void { + if (!this._eventListeners.has(type)) { + // @ts-ignore + this._eventListeners.set(type, [listener]); + } else { + // @ts-ignore + this._eventListeners.get(type)?.push(listener); + } + } + + clearEventListeners(): void { + this._eventListeners.clear(); + } + + dispatchEvent = >(event: TMap[K]): void => { + const listeners = (this._eventListeners.get(event.type as K) ?? []).slice(); + for (const listener of listeners) { + listener.call(this, event); + } + }; + + removeEventListener>(types: K | K[], listener: EventListener): void { + if (typeof types === 'string') { + this.removeSingleEventListener(types, listener); + } else { + types.forEach(type => { + this.removeSingleEventListener(type, listener); + }); + } + } + + removeSingleEventListener>(type: K, listener: EventListener): void { + const listeners = this._eventListeners.get(type); + if (listeners) { + arrayRemoveElement(listeners, listener); + } + } +}