diff --git a/android/app/src/main/java/tv/broadpeak/simid/app/PlayerActivity.kt b/android/app/src/main/java/tv/broadpeak/simid/app/PlayerActivity.kt index 6168744..8e7cbc2 100644 --- a/android/app/src/main/java/tv/broadpeak/simid/app/PlayerActivity.kt +++ b/android/app/src/main/java/tv/broadpeak/simid/app/PlayerActivity.kt @@ -26,6 +26,7 @@ import androidx.media3.ui.PlayerView import androidx.core.net.toUri import androidx.media3.common.util.UnstableApi import androidx.media3.ui.PlayerNotificationManager +import tv.broadpeak.simid.controller.CreativeData import tv.broadpeak.simid.controller.MediaState import tv.broadpeak.simid.controller.Dimensions import tv.broadpeak.smartlib.SmartLib @@ -141,8 +142,9 @@ class PlayerActivity : AppCompatActivity() { val b = intent.extras val creativeUrl = b?.getString("creativeUrl") ?: return val creativeAdParams = b.getString("creativeAdParams") ?: "" + val creativeClickThruUrl = b.getString("creativeClickThruUrl") ?: "" val creativeDuration = b.getInt("creativeDuration") - loadSimid("input-creative", creativeUrl, creativeAdParams, creativeDuration.toFloat(), true) + loadSimid("input-creative", creativeUrl, creativeAdParams, creativeClickThruUrl, creativeDuration.toFloat(), true) } private fun initSmartLib(url: String) { @@ -171,7 +173,8 @@ class PlayerActivity : AppCompatActivity() { runOnUiThread { val iframeResource = adData.nonLinearIframeResources[0].url val adParameters = adData.nonLinearIframeResources[0].parameters - loadSimid(adData.adId, iframeResource, adParameters, (adData.duration.toFloat() / 1000.0F)) + val clickThruUrl = adData.clickURL + loadSimid(adData.adId, iframeResource, adParameters, clickThruUrl, (adData.duration.toFloat() / 1000.0F)) } } } @@ -216,7 +219,7 @@ class PlayerActivity : AppCompatActivity() { } } - private fun loadSimid(adId: String, creativeUri: String, adParameters: String, duration: Float, autoStart: Boolean = false) { + private fun loadSimid(adId: String, creativeUri: String, adParameters: String, clickThruUrl: String, duration: Float, autoStart: Boolean = false) { if (playerContainer == null) { return @@ -227,7 +230,8 @@ class PlayerActivity : AppCompatActivity() { Log.d(TAG, "Load SIMID: ${playerDimensions.toString()} $creativeUri $duration") - val simidController = SimidController(this, applicationContext, playerDimensions, playerDimensions, creativeUri, adParameters, duration) + val creativeData = CreativeData(adParameters, clickThruUrl) + val simidController = SimidController(this, applicationContext, playerDimensions, playerDimensions, creativeUri, creativeData, duration) simidController.let { controller -> controller.onAddSimid { webView -> addSimidWebView(adId, webView) } @@ -237,7 +241,7 @@ class PlayerActivity : AppCompatActivity() { controller.onGetMediaState { getMediaState() } controller.onPauseMedia { pauseMedia() } controller.onPlayMedia { playMedia() } - controller.onOpenClickthrough { uri -> openClickthrough(uri) } + controller.onOpenPage { uri -> openPage(uri) } controller.onComplete { skipped -> completeAd(adId, skipped) } controller.simidControllerApi(bpkSimidController!!) @@ -359,8 +363,8 @@ class PlayerActivity : AppCompatActivity() { return true } - private fun openClickthrough(uri: String) { - Log.d(TAG, "Open clickthrough: $uri") + private fun openPage(uri: String) { + Log.d(TAG, "Open page: $uri") val intent = Intent(Intent.ACTION_VIEW, uri.toUri()) startActivity(intent) diff --git a/android/app/src/main/java/tv/broadpeak/simid/app/SimidController.kt b/android/app/src/main/java/tv/broadpeak/simid/app/SimidController.kt index 588dc2c..aab9454 100644 --- a/android/app/src/main/java/tv/broadpeak/simid/app/SimidController.kt +++ b/android/app/src/main/java/tv/broadpeak/simid/app/SimidController.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.graphics.Rect import tv.broadpeak.simid.controller.Dimensions +import tv.broadpeak.simid.controller.CreativeData import tv.broadpeak.smartlib.ad.simid.GenericSimidControllerApi class SimidController( @@ -12,11 +13,11 @@ class SimidController( private val mainPlayerDimensions: Dimensions, private val creativeDimensions: Dimensions, private val creativeUri: String, - private val adParameters: String = "", + private val creativeData: CreativeData, private val adDuration: Float = 0.0F, private val adSkippable: Boolean = false, private val mediaStatePollingInterval: Long = MEDIA_TIMEUPDATE_INTERVAL_MS -) : tv.broadpeak.simid.controller.SimidController(activity, context, mainPlayerDimensions, creativeDimensions, creativeUri, adParameters, adDuration, adSkippable, mediaStatePollingInterval) { +) : tv.broadpeak.simid.controller.SimidController(activity, context, mainPlayerDimensions, creativeDimensions, creativeUri, creativeData, adDuration, adSkippable, mediaStatePollingInterval) { private var simidControllerApi: GenericSimidControllerApi? = null diff --git a/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidController.kt b/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidController.kt index 38c6307..36d725f 100644 --- a/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidController.kt +++ b/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidController.kt @@ -24,7 +24,7 @@ import kotlinx.serialization.json.encodeToJsonElement * @param playerDimensions the main player dimensions * @param creativeDimensions the initial creative dimensions the application/player will set * @param creativeUri The creative URI - * @param adParameters the creative ad parameters + * @param creativeData the creative data (ad parameters, clickThruUrl) * @param adDuration the display duration of the creative (0 by default, meaning no requested duration) * @param adSkippable true if the linear ad is skippable (false by default) * @param mediaTimeupdateInterval the interval in ms to send media timeupdate message to the creative (250ms by default, -1 to disable) @@ -35,7 +35,7 @@ public open class SimidController ( private var playerDimensions: Dimensions, private var creativeDimensions: Dimensions, private val creativeUri: String, - private val adParameters: String = "", + private val creativeData: CreativeData, private val adDuration: Float = 0.0F, private val adSkippable: Boolean = false, private val mediaTimeupdateInterval: Long = MEDIA_TIMEUPDATE_INTERVAL_MS @@ -65,7 +65,7 @@ public open class SimidController ( private var onShowSimid: ((Boolean) -> Unit)? = null private var onResizeSimid: ((Dimensions) -> Boolean)? = null private var onResizePlayer: ((Dimensions) -> Unit)? = null - private var onOpenClickthrough: ((String) -> Unit)? = null + private var onOpenPage: ((String) -> Unit)? = null private var onComplete: ((Boolean) -> Unit)? = null private val mainScope = MainScope() @@ -103,8 +103,8 @@ public open class SimidController ( this.onResizePlayer = cb } - fun onOpenClickthrough(cb: (String) -> Unit) { - this.onOpenClickthrough = cb + fun onOpenPage(cb: (String) -> Unit) { + this.onOpenPage = cb } fun onComplete(cb: (Boolean) -> Unit) { @@ -186,6 +186,7 @@ public open class SimidController ( this.addMessageListener(CreativeMessage.REQUEST_STOP, ::onCreativeRequestStop) this.addMessageListener(CreativeMessage.EXPAND_NONLINEAR, ::onCreativeExpandNonlinear) this.addMessageListener(CreativeMessage.COLLAPSE_NONLINEAR, ::onCreativeCollapseNonlinear) + this.addMessageListener(CreativeMessage.CLICK_THRU, ::onCreativeClickThru) this.addMessageListener(CreativeMessage.REQUEST_NAVIGATION, ::onCreativeRequestNavigation) } @@ -289,17 +290,21 @@ public open class SimidController ( stopAd(StopCode.CREATIVE_INITIATED) } - private fun onCreativeRequestNavigation(message: Message) { - if (onOpenClickthrough == null) { - rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, "Navigation not supported by the player") + private fun onCreativeClickThru(message: Message) { + val args: CreativeClickThruMessageArgs = json.decodeFromJsonElement(message.args!!) + + // Open landing page only when playerHandles is true + if (!(args.playerHandles ?: false)) { + this.resolveMessage(message) return } + + val uri = args.uri ?: args.url // url deprecated in favor of uri + this.onOpenUri(message, args.url) + } + private fun onCreativeRequestNavigation(message: Message) { val args: CreativeRequestNavigationMessageArgs = json.decodeFromJsonElement(message.args!!) - // Spec §4.4.12.1: resolve before opening the URI so the creative receives - // the message prior to the app being backgrounded. - resolveMessage(message) - onPauseMedia?.invoke() - onOpenClickthrough?.invoke(args.uri) + this.onOpenUri(message, args.uri) } //endregion CREATIVE MESSAGE HANDLERS @@ -392,15 +397,15 @@ public open class SimidController ( null, // This should be filled in on mobile false, // player.isDeviceMuted, 1.0F, // player.volume, - if (onOpenClickthrough != null) NavigationSupport.PLAYER_HANDLES else NavigationSupport.AD_HANDLES, + if (this.onOpenPage != null) NavigationSupport.PLAYER_HANDLES else NavigationSupport.AD_HANDLES, null, // CloseButtonSupport.AD_HANDLES, adDuration ) // Escape characters to avoid JSON parsing failure in Creative - val adParams = adParameters.replace("\"", "\\\"") + val adParams = this.creativeData.adParameters.replace("\"", "\\\"") - val creativeData = CreativeData(adParams) + val creativeData = CreativeData(adParams, this.creativeData.clickThruUrl) val args = PlayerInitMessageArgs(environmentData, creativeData) try { @@ -533,9 +538,26 @@ public open class SimidController ( stopAd(StopCode.NON_LINEAR_DURATION_COMPLETE) } } + //endregion MAIN VIDEO STATE - private fun dimensions(rect: Rect): Dimensions { - return Dimensions(rect.top, rect.left, rect.width(), rect.height()) + // region CLICK THROUGH + private fun onOpenUri(message: Message, uri: String?) { + if (uri == null) { + this.rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, "Invalid URI") + return + } + + if (this.onOpenPage == null) { + this.rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, "Navigation not supported by the player") + return + } + + // Spec §4.4.12.1: resolve before opening the window so the creative receives + // the message prior to the app being backgrounded. + this.resolveMessage(message) + + this.onPauseMedia?.invoke() + this.onOpenPage?.invoke(uri) } - //endregion MAIN VIDEO STATE + // endregion CLICK THROUGH } diff --git a/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidMessages.kt b/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidMessages.kt index 69d295d..fd54243 100644 --- a/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidMessages.kt +++ b/android/controller/src/main/java/tv/broadpeak/simid/controller/SimidMessages.kt @@ -323,7 +323,8 @@ data class CreativeClickThruMessageArgs( val x: Int?, val y: Int?, val playerHandles: Boolean?, - val url: String? + val uri: String?, + val url: String? // deprecated in favor of uri ) @Serializable diff --git a/ios/SimidController/SimidController.swift b/ios/SimidController/SimidController.swift index 332381c..287ae4c 100644 --- a/ios/SimidController/SimidController.swift +++ b/ios/SimidController/SimidController.swift @@ -12,7 +12,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation private var playerDimensions: Dimensions private var creativeDimensions: Dimensions private let creativeUri: String - private let adParameters: String + private let creativeData: CreativeData private let adDuration: Double private let adSkippable: Bool private let mediaTimeupdateInterval: UInt64 @@ -31,7 +31,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation private var onShowSimid: ((Bool) -> Void)? private var onResizeSimid: ((Dimensions) -> Bool)? private var onResizePlayer: ((Dimensions) -> Void)? - private var onOpenClickthrough: ((String) -> Void)? + private var onOpenPage: ((String) -> Void)? private var onComplete: ((Bool) -> Void)? /** @@ -39,7 +39,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation * @param playerDimensions the main player dimensions * @param creativeDimensions the initial creative dimensions the application/player will set * @param creativeUri The creative URI - * @param adParameters the creative ad parameters + * @param creativeData the creative data (ad parameters, clickThruUrl) * @param adDuration the display duration of the creative (0 by default, meaning no requested duration) * @param adSkippable true if the linear ad is skippable (false by default) * @param mediaTimeupdateInterval the interval in ms to send media timeupdate message to the creative (250ms by default, -1 to disable) @@ -48,7 +48,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation playerDimensions: Dimensions, creativeDimensions: Dimensions, creativeUri: String, - adParameters: String = "", + creativeData: CreativeData, adDuration: Double = 0, adSkippable: Bool = false, mediaTimeupdateInterval: UInt64 = MEDIA_TIMEUPDATE_INTERVAL_MS @@ -56,7 +56,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation self.playerDimensions = playerDimensions self.creativeDimensions = creativeDimensions self.creativeUri = creativeUri - self.adParameters = adParameters + self.creativeData = creativeData self.adDuration = adDuration self.adSkippable = adSkippable self.mediaTimeupdateInterval = mediaTimeupdateInterval @@ -73,7 +73,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation public func onShowSimid(_ cb: @escaping (Bool) -> Void) { self.onShowSimid = cb } public func onResizeSimid(_ cb: @escaping (Dimensions) -> Bool) { self.onResizeSimid = cb } public func onResizePlayer(_ cb: @escaping (Dimensions) -> Void) { self.onResizePlayer = cb } - public func onOpenClickthrough(_ cb: @escaping (String) -> Void) { self.onOpenClickthrough = cb } + public func onOpenPage(_ cb: @escaping (String) -> Void) { self.onOpenPage = cb } public func onComplete(_ cb: @escaping (Bool) -> Void) { self.onComplete = cb } public func getVersion() -> String { @@ -159,6 +159,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation addMessageListener(CreativeMessage.REQUEST_STOP) { [weak self] message in self?.onCreativeRequestStop(message) } addMessageListener(CreativeMessage.EXPAND_NONLINEAR) { [weak self] message in self?.onCreativeExpandNonlinear(message) } addMessageListener(CreativeMessage.COLLAPSE_NONLINEAR) { [weak self] message in self?.onCreativeCollapseNonlinear(message) } + addMessageListener(CreativeMessage.CLICK_THRU) { [weak self] message in self?.onCreativeClickThru(message) } addMessageListener(CreativeMessage.REQUEST_NAVIGATION) { [weak self] message in self?.onCreativeRequestNavigation(message) } } @@ -219,7 +220,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation self.creativeDimensions = creativeDim // If creative successfully resized then resize the main player/ - self.onResizePlayer?(mediaDim) + onResizePlayer(mediaDim) self.resolveMessage(message) @@ -256,16 +257,28 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation _ = self.onPlayMedia?() (self.onResizeSimid?(creativeDimensions) ?? false) ? self.resolveMessage(message) : rejectMessage(message, errorCode: PlayerErrorCode.UNSPECIFIED, errorMessage: "Unable to collapse nonlinear ad") } + + private func onCreativeClickThru(_ message: Message) { + guard let args = message.args as? CreativeClickThruMessageArgs else { + self.rejectMessage(message) + return + } + + // Open landing page only when playerHandles is true + if args.playerHandles != true { + self.resolveMessage(message) + return + } + + let uri = args.uri ?? args.url // url deprecated in favor of uri + self.onOpenUri(message: message, uri: uri) + } private func onCreativeRequestNavigation(_ message: Message) { guard let args = message.args as? CreativeRequestNavigationMessageArgs else { - self.rejectMessage(message) return } - - self.resolveMessage(message) - _ = self.onPauseMedia?() - self.onOpenClickthrough?(args.uri) + self.onOpenUri(message: message, uri: args.uri) } // MARK: - WEBVIEW MANAGEMENT @@ -337,7 +350,7 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation deviceId: nil, muted: false, volume: 1.0, - navigationSupport: onOpenClickthrough != nil + navigationSupport: onOpenPage != nil ? NavigationSupport.PLAYER_HANDLES : NavigationSupport.AD_HANDLES, closeButtonSupport: nil, @@ -345,19 +358,19 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation ) // Escape characters to avoid JSON parsing failure in Creative - let adParams = adParameters.replacingOccurrences( + let adParams = self.creativeData.adParameters.replacingOccurrences( of: "\"", with: "\\\"" ) - let creative = CreativeData( + let creativeData = CreativeData( adParameters: adParams, - clickThruUrl: "" + clickThruUrl: self.creativeData.clickThruUrl ) let args = PlayerInitMessageArgs( environmentData: env, - creativeData: creative + creativeData: creativeData ) Task { @@ -463,4 +476,24 @@ open class SimidController: SimidComponent, WKScriptMessageHandler, WKNavigation stopAd(reason: StopCode.NON_LINEAR_DURATION_COMPLETE) } } + + // MARK: - Click through + private func onOpenUri(message: Message, uri: String?) { + guard uri != nil else { + self.rejectMessage(message, errorCode:PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, errorMessage:"Invalid URI") + return + } + + guard self.onOpenPage != nil else { + self.rejectMessage(message, errorCode:PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, errorMessage: "Navigation not supported by the player") + return + } + + // Spec §4.4.12.1: resolve before opening the window so the creative receives + // the message prior to the app being backgrounded. + self.resolveMessage(message) + + _ = self.onPauseMedia?() + self.onOpenPage?(uri!) + } } diff --git a/ios/SimidController/SimidMessage.swift b/ios/SimidController/SimidMessage.swift index f4cae0a..3d833d1 100644 --- a/ios/SimidController/SimidMessage.swift +++ b/ios/SimidController/SimidMessage.swift @@ -374,9 +374,17 @@ struct PlayerFatalErrorMessageArgs: MessageArgs { // MARK: - Creative / Environment -struct CreativeData: Codable { +public struct CreativeData: Codable { let adParameters: String let clickThruUrl: String + + public init( + adParameters: String, + clickThruUrl: String + ) { + self.adParameters = adParameters + self.clickThruUrl = clickThruUrl + } } struct EnvironmentData: Codable { @@ -420,7 +428,8 @@ struct CreativeClickThruMessageArgs: MessageArgs { let x: Int? let y: Int? let playerHandles: Bool? - let url: String? + let uri: String? + let url: String? // deprecated in favor of uri } struct CreativeFatalErrorMessageArgs: MessageArgs { diff --git a/ios/SimidDemoApp/AssetsView.swift b/ios/SimidDemoApp/AssetsView.swift index 420cf90..2bee62f 100644 --- a/ios/SimidDemoApp/AssetsView.swift +++ b/ios/SimidDemoApp/AssetsView.swift @@ -7,6 +7,7 @@ struct Asset: Identifiable, Hashable { let url: URL let simidURL: URL? let adParameters: String? + let clickThruUrl: String? } struct AssetsView: View { @@ -14,14 +15,16 @@ struct AssetsView: View { let assets: [Asset] = [ Asset(title: "Big Buck Bunny", url: URL(string: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8")!, - simidURL: URL(string: "https://interactiveadvertisingbureau.github.io/SIMID/examples/creatives/banner_nonlinear.html"), - adParameters: "{\"bannerText\":\"Click here to draw!\",\"webUrl\":\"https://quickdraw.withgoogle.com/\"}" + simidURL: URL(string: "http://192.168.10.2:8080/Broadpeak/simid/adserver/creatives/creative-test.html"), + adParameters: "{\"bannerText\":\"Click here to draw!\",\"webUrl\":\"https://quickdraw.withgoogle.com/\"}", + clickThruUrl: "" ), Asset(title: "BPK.IO", url: URL(string: "https://dcv5s0ei7csoc.cloudfront.net/2ab56412b1163ee103b9ed7065a20563/AVOD/Meridian_1920x1080_30fps_SDR/conditioned/stream.m3u8?midfreq=40&coll=cooldrink&adid=crea&max_ads=1&nldur=20&vdur=600&vod=true")!, simidURL: nil, - adParameters: nil + adParameters: nil, + clickThruUrl: "" ) ] diff --git a/ios/SimidDemoApp/GenericSimidController.swift b/ios/SimidDemoApp/GenericSimidController.swift index 6d46a29..8fd200e 100644 --- a/ios/SimidDemoApp/GenericSimidController.swift +++ b/ios/SimidDemoApp/GenericSimidController.swift @@ -9,7 +9,7 @@ class GenericSimidController: SimidController { playerDimensions: Dimensions, creativeDimensions: Dimensions, creativeUri: String, - adParameters: String = "", + creativeData: CreativeData, adDuration: Double = 0, adSkippable: Bool = false, mediaTimeupdateInterval: UInt64 = SimidController.MEDIA_TIMEUPDATE_INTERVAL_MS @@ -17,7 +17,7 @@ class GenericSimidController: SimidController { super.init(playerDimensions: playerDimensions, creativeDimensions: creativeDimensions, creativeUri: creativeUri, - adParameters: adParameters, + creativeData: creativeData, adDuration: adDuration, adSkippable: adSkippable, mediaTimeupdateInterval: mediaTimeupdateInterval diff --git a/ios/SimidDemoApp/PlayerViewController.swift b/ios/SimidDemoApp/PlayerViewController.swift index 9de22a7..1c8ea15 100644 --- a/ios/SimidDemoApp/PlayerViewController.swift +++ b/ios/SimidDemoApp/PlayerViewController.swift @@ -54,6 +54,7 @@ final class PlayerViewController: UIViewController, AdEventsListener { adId: "simid-ad", creativeUri: (asset?.simidURL)!.absoluteString, adParameters: (asset?.adParameters)!, + clickThruUrl: (asset?.clickThruUrl)!, duration: 10.0 ) } @@ -164,6 +165,7 @@ final class PlayerViewController: UIViewController, AdEventsListener { adId: adData.adId, creativeUri: iframeResource.url, adParameters: iframeResource.parameters, + clickThruUrl: adData.clickURL, duration: Double(adData.duration) / 1000.0 ) } @@ -197,6 +199,7 @@ final class PlayerViewController: UIViewController, AdEventsListener { adId: String, creativeUri: String, adParameters: String, + clickThruUrl: String, duration: Double, autoStart: Bool = true ) { @@ -209,12 +212,17 @@ final class PlayerViewController: UIViewController, AdEventsListener { width: Int(bounds.width), height: Int(bounds.height) ) + + let creativeData = CreativeData( + adParameters: adParameters, + clickThruUrl: clickThruUrl + ) let controller = GenericSimidController( playerDimensions: dims, creativeDimensions: dims, creativeUri: creativeUri, - adParameters: adParameters, + creativeData: creativeData, adDuration: duration ) @@ -257,8 +265,8 @@ final class PlayerViewController: UIViewController, AdEventsListener { return true } - controller.onOpenClickthrough { url in - if let u = URL(string: url) { + controller.onOpenPage { uri in + if let u = URL(string: uri) { UIApplication.shared.open(u) } } diff --git a/web/app/src/App.ts b/web/app/src/App.ts index 2a5a05d..6047521 100644 --- a/web/app/src/App.ts +++ b/web/app/src/App.ts @@ -76,8 +76,9 @@ export default class App { private async startCreative() { const url = this.creativeEditUrl.value const adParams = this.creativeEditAdParams.value + const clickThruUrl = '' const duration = parseInt(this.creativeEditDuration.value) - this.player.loadSimid('input-creative', url, adParams, duration, true) + this.player.loadSimid('input-creative', url, adParams, clickThruUrl, duration, true) } private setResizeObserver() { diff --git a/web/app/src/Player.ts b/web/app/src/Player.ts index 4c8fa83..3a0c1d2 100644 --- a/web/app/src/Player.ts +++ b/web/app/src/Player.ts @@ -1,4 +1,4 @@ -import { MediaState } from '@broadpeak-tv/simid-controller' +import { CreativeData, MediaState } from '@broadpeak-tv/simid-controller' import SimidController from './SimidController' import { SmartLib } from '@broadpeak/smartlib' import '@broadpeak/smartlib-ad' @@ -66,13 +66,17 @@ export default class Player { await this.player.unload() } - public loadSimid(adId: string, creativeUri: string, adParameters: string, duration: number, autoStart = false) { + public loadSimid(adId: string, creativeUri: string, adParameters: string, clickThruUrl: string, duration: number, autoStart = false) { // Consider player container dimensions as initial creative dimensions const playerRect: DOMRect = this.getElementDimensions(this.playerContainer) console.log(`[Player] Load SIMID - uri:${creativeUri} duration:${duration}`) - const simidController = new SimidController(playerRect, playerRect, creativeUri, adParameters, duration, false) + const creativeData: CreativeData = { + adParameters, + clickThruUrl + } + const simidController = new SimidController(playerRect, playerRect, creativeUri, creativeData, duration, false, -1) simidController.onGetMediaState = () => this.getMediaState() simidController.onAddSimid = (iframe: HTMLIFrameElement) => this.addSimidIframe(adId, iframe) @@ -81,7 +85,7 @@ export default class Player { simidController.onResizePlayer = (dimensions: DOMRect) => this.resizePlayer(dimensions) simidController.onPauseMedia = () => this.pauseMedia() simidController.onPlayMedia = () => this.playMedia() - simidController.onOpenClickthrough = (uri: string) => this.openClickthrough(uri) + simidController.onOpenPage = (uri: string) => this.openPage(uri) simidController.onComplete = (skipped: boolean) => this.completeAd(adId, skipped) simidController.simidControllerApi = this.bpkSimidController @@ -118,9 +122,9 @@ export default class Player { console.log('[Player] onPrepareAd:', adData) this.adDatas.set(adData.adId, adData) if (adData.nonLinearIframeResources && adData.nonLinearIframeResources.length) { - const iframeResources = adData.nonLinearIframeResources[0] + const iframeResource = adData.nonLinearIframeResources[0] const duration = adData.duration ? (adData.duration / 1000) : 0 - this.loadSimid(adData.adId, iframeResources.url, iframeResources.parameters, duration) + this.loadSimid(adData.adId, iframeResource.url, iframeResource.parameters, adData.clickURL, duration) } }, onAdBegin: (adData: any, adBreakData: any) => { @@ -205,8 +209,8 @@ export default class Player { return true } - private openClickthrough(uri: string) { - console.log('[Player] Open clicktrough:', uri) + private openPage(uri: string) { + console.log('[Player] Open page:', uri) window.open(uri, '_blank') } diff --git a/web/controller/src/SimidController.ts b/web/controller/src/SimidController.ts index 696ef36..1470746 100644 --- a/web/controller/src/SimidController.ts +++ b/web/controller/src/SimidController.ts @@ -21,6 +21,7 @@ import { MediaTimeUpdateMessageArgs, NavigationSupport, PlayerResizeMessageArgs, + CreativeClickThruMessageArgs, } from './SimidMessages' import { SimidComponent } from "./SimidComponent" @@ -44,8 +45,8 @@ export class SimidController extends SimidComponent { // The creative URI private _creativeUri: string - // The ad parameters - private _adParameters: string + // The creative data (ad parameters, clickThruUrl) + private _creativeData: CreativeData | undefined // A reference to the iframe holding the SIMID creative private _simidIframe: HTMLIFrameElement @@ -76,7 +77,7 @@ export class SimidController extends SimidComponent { private _onShowSimid: ((boolean) => void) | undefined private _onResizeSimid: ((DOMRect) => boolean) | undefined private _onResizePlayer: ((DOMRect) => void) | undefined - private _onOpenClickthrough: ((uri: string) => void) | undefined + private _onOpenPage: ((uri: string) => void) | undefined private _onComplete: ((boolean) => void) | undefined private _timerMediaState: number | undefined @@ -92,7 +93,7 @@ export class SimidController extends SimidComponent { * @param playerDimensions the main player dimensions * @param creativeDimensions the initial creative dimensions the application/player will set * @param creativeUri The creative URI - * @param adParameters the creative ad parameters + * @param creativeData the creative data (ad parameters, clickThruUrl) * @param adDuration the display duration of the creative (0 by default, meaning no requested duration) * @param adSkippable true if the linear ad is skippable (false by default) * @param mediaTimeupdateInterval the interval in ms to send media timeupdate message to the creative (250ms by default, -1 to disable) @@ -101,7 +102,7 @@ export class SimidController extends SimidComponent { playerDimensions: DOMRect, creativeDimensions: DOMRect, creativeUri: string, - adParameters = '', + creativeData: CreativeData | undefined = undefined, adDuration = 0, adSkippable = false, mediaTimeupdateInterval = MEDIA_TIMEUPDATE_INTERVAL_MS) { @@ -112,7 +113,7 @@ export class SimidController extends SimidComponent { this._creativeDimensions = creativeDimensions as Dimensions this._creativeUri = creativeUri - this._adParameters = adParameters + this._creativeData = creativeData this._adSkippable = adSkippable this._isStopping = false @@ -188,8 +189,8 @@ export class SimidController extends SimidComponent { * Used in mobile app environments where the player manages external URL navigation. * The player must open the URI and the callback is invoked after resolve is sent to the creative. */ - public set onOpenClickthrough(cb: (uri: string) => void) { - this._onOpenClickthrough = cb + public set onOpenPage(cb: (uri: string) => void) { + this._onOpenPage = cb } /** @@ -268,6 +269,7 @@ export class SimidController extends SimidComponent { this.addMessageListener(CreativeMessage.REQUEST_STOP, (message: Message) => this.onCreativeRequestStop(message)) this.addMessageListener(CreativeMessage.EXPAND_NONLINEAR, (message: Message) => this.onCreativeExpandNonlinear(message)) this.addMessageListener(CreativeMessage.COLLAPSE_NONLINEAR, (message: Message) => this.onCreativeCollapseNonlinear(message)) + this.addMessageListener(CreativeMessage.CLICK_THRU, (message: Message) => this.onCreativeClickThru(message)) this.addMessageListener(CreativeMessage.REQUEST_NAVIGATION, (message: Message) => this.onCreativeRequestNavigation(message)) } @@ -381,18 +383,22 @@ export class SimidController extends SimidComponent { this._stopAd(StopCode.CREATIVE_INITIATED) } - protected onCreativeRequestNavigation(message: Message) { - if (!this._onOpenClickthrough) { - this.rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, 'Navigation not supported by the player') + protected onCreativeClickThru(message: Message) { + const args = message.args as CreativeClickThruMessageArgs + + // Open landing page only when playerHandles is true + if (!args.playerHandles) { + this.resolveMessage(message) return } - const args = message.args as CreativeRequestNavigationMessageArgs - // Spec §4.4.12.1: resolve before opening the window so the creative receives - // the message prior to the app being backgrounded. - this.resolveMessage(message) - this._onPauseMedia() - this._onOpenClickthrough(args.uri) + const uri = args.uri || args.url // url deprecated in favor of uri + this._onOpenUri(message, uri) + } + + protected onCreativeRequestNavigation(message: Message) { + const args = message.args as CreativeRequestNavigationMessageArgs + this._onOpenUri(message, args.uri) } // #endregion CREATIVE MESSAGE HANDLERS // #endregion PROTECTED METHODS @@ -417,22 +423,13 @@ export class SimidController extends SimidComponent { deviceId: '', // This should be filled in on mobile muted: mediaState ? mediaState.muted : false, volume: mediaState ? mediaState.volume : 1, - navigationSupport: this._onOpenClickthrough ? NavigationSupport.PLAYER_HANDLES : NavigationSupport.AD_HANDLES, + navigationSupport: this._onOpenPage ? NavigationSupport.PLAYER_HANDLES : NavigationSupport.AD_HANDLES, nonlinearDuration: this._adDuration, } - const creativeData: CreativeData = { - adParameters: this._adParameters, - clickThruUrl: '', - // These values should be populated from the VAST response - // adId: '', - // creativeId : '', - // adServingId: '', - } - const args: PlayerInitMessageArgs = { environmentData : environmentData, - creativeData: creativeData, + creativeData: this._creativeData, } try { @@ -588,5 +585,27 @@ export class SimidController extends SimidComponent { } // #endregion MAIN VIDEO STATE + // #region CLICK THROUGH + private _onOpenUri(message: Message, uri?: string) { + if (!uri) { + this.rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, 'Invalid URI') + return + } + + if (!this._onOpenPage) { + this.rejectMessage(message, PlayerErrorCode.NAVIGATION_NOT_SUPPORTED, 'Navigation not supported by the player') + return + } + + // Spec §4.4.12.1: resolve before opening the window so the creative receives + // the message prior to the app being backgrounded. + this.resolveMessage(message) + + this._onPauseMedia() + this._onOpenPage(uri) + + } + // #endregion CLICK THROUGH + // #endregion PRIVATE METHODS } \ No newline at end of file diff --git a/web/controller/src/SimidMessages.ts b/web/controller/src/SimidMessages.ts index a2b5db9..4bd3cc4 100644 --- a/web/controller/src/SimidMessages.ts +++ b/web/controller/src/SimidMessages.ts @@ -289,7 +289,8 @@ export type CreativeClickThruMessageArgs = { x?: number y?: number playerHandles?: boolean - url?: string + uri?: string + url?: string // deprecated in favor of uri } export type CreativeExpandNonLinearResolveMessageArgs = {