This guide shows you how to create a custom receiver app that communicates between the sender app and receiver device and allows you to integrate the IMA SDK into your Cast app.
If you'd rather follow along with a finished IMA SDK receiver, download the sample app or view it on GitHub. You can also register a custom receiver to use our sample on GitHub with your cast device.
Our receiver sample is hosted on GitHub, so you can set up your custom receiver
for testing by using the following URL in your receiver:
https://googleads.github.io/googleads-ima-cast-dai/player.html
Set up the HTML
Create an HTML page for your custom receiver by putting
<!DOCTYPE html>
and</html>
tags into a file called player.html.Add a header section containing references to all of the libraries you need to use, including the Cast receiver library, the Cast Media Player library, and the IMA SDK (ima3_dai.js) tags:
<head> <title>Receiver Demo</title> <script type="text/javascript" src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script> <script type="text/javascript" src="//www.gstatic.com/cast/sdk/libs/mediaplayer/1.0.0/media_player.js"></script> <script type="text/javascript" src="player.js"></script> <script type="text/javascript" src="//imasdk.googleapis.com/js/sdkloader/ima3_dai.js"></script> </head>
Since your receiver is going to play video, add a body section containing a video player:
<body> <div id='splash' style='position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: white;'> <div style='text-align: center; position: absolute; top: 50%; left: 50%;'> <span>IMA SDK</span> </div> </div> <video id='mediaElement' autoplay='true' muted='true' style='width: 100%; height: 90%;overflow-y: hidden;'></video> <script> var player = new Player(document.getElementById('mediaElement')); player.start(); </script> </body>
This code creates a splash screen to show when the SDK isn't playing
and a video player. It also creates a new Player
object and passes
the video element to it, before telling it to start()
. This completes
your receiver setup. The next step is implementing the receiver's
functionality.
Create the player.js file
Create a new file called player.js and add the following to implement a sample video player:
var Player = function(mediaElement) { var namespace = 'urn:x-cast:com.google.ads.interactivemedia.dai.cast'; var self = this; this.castPlayer_ = null; this.startTime_ = 0; this.adIsPlaying_ = false; this.mediaElement_ = mediaElement; this.receiverManager_ = cast.receiver.CastReceiverManager.getInstance(); this.receiverManager_.onSenderConnected = function(event) { console.log('Sender Connected'); }; this.receiverManager_.onSenderDisconnected = this.onSenderDisconnected.bind(this); this.imaMessageBus_ = this.receiverManager_.getCastMessageBus(namespace); this.imaMessageBus_.onMessage = function(event) { console.log('Received message from sender: ' + event.data); var message = event.data.split(','); var method = message[0]; switch (method) { case 'getContentTime': var contentTime = self.getContentTime_(); self.broadcast_('contentTime,' + contentTime); break; default: self.broadcast_('Message not recognized'); break; } }; this.mediaManager_ = new cast.receiver.MediaManager(this.mediaElement_); this.mediaManager_.onLoad = this.onLoad.bind(this); this.mediaManager_.onSeek = this.onSeek.bind(this); this.initStreamManager_(); };
This describes a
Player
object for playing video, sending messages to the IMA SDK and communicating with any Cast senders. First it gets a reference to theCastReceiverManager
, which is responsible for controlling Cast receivers and registering event handlers for theonSenderConnected()
andonSenderDisconnected()
events.Next, the receiver creates a message bus with a specific namespace, which is used by the sender and receiver to send messages to each other. Specifically, some message cases are handled here for the sender telling the receiver to get the content time (explained later).
Finally, it creates a new Cast
MediaManager
which is responsible for handling media events. Since the DAI methods for loading and seeking are different from a stock media player, this code overrides theMediaManager.onLoad()
andMediaManager.onSeek()
methods.To be ready when the sender asks the receiver to load or seek the stream, add the
onLoad()
andonSeek()
functions to player.js:/** * Called on receipt of a LOAD message from the sender. * @param {!cast.receiver.MediaManager.Event} event The load event. */ Player.prototype.onLoad = function(event) { /* * imaRequestData contains: * for Live requests: * { * assetKey: <ASSET_KEY> * } * for VOD requests: * { * contentSourceId: <CMS_ID>, * videoID: <VIDEO_ID> * } * These can also be set as properties on this.streamRequest after * initializing with no constructor parameter. */ var imaRequestData = event.data.media.customData; this.startTime_ = imaRequestData.startTime; if (imaRequestData.assetKey) { this.streamRequest = new google.ima.dai.api.LiveStreamRequest(imaRequestData); } else if (imaRequestData.contentSourceId) { this.streamRequest = new google.ima.dai.api.VODStreamRequest(imaRequestData); } this.streamManager_.requestStream(this.streamRequest); document.getElementById('splash').style.display = 'none'; }; /** * Processes the SEEK event from the sender. * @param {!cast.receiver.MediaManager.Event} event The seek event. * @this {Player} */ Player.prototype.onSeek = function(event) { var currentTime = event.data.currentTime; this.seek_(currentTime); this.mediaManager_.broadcastStatus(true, event.data.requestId); };
The
onLoad()
function parses the event's custom data to create a stream request from theStreamManager
(see below), with different calls depending on whether the request is for a VOD stream or a live stream. This is set up to work with the custom data that's passed in from the sender sample. If you add or change the data there, you must also update this to make it work. TheonSeek()
performs a custom seek request on the stream in lieu of a normal seek command, discussed later.Add the StreamManager and StreamEvents to player.js:
/** * Initializes receiver stream manager and adds callbacks. * @private */ Player.prototype.initStreamManager_ = function() { var self = this; this.streamManager_ = new google.ima.dai.api.StreamManager(this.mediaElement_); var onStreamDataReceived = this.onStreamDataReceived.bind(this); this.streamManager_.addEventListener( google.ima.dai.api.StreamEvent.Type.LOADED, function(event) { var streamUrl = event.getStreamData().url; // Each element in subtitles array is an object with url and language // properties. Example of a subtitles array with 2 elements: // { // "url": "http://www.sis.com/1234/subtitles_en.ttml", // "language": "en" // }, { // "url": "http://www.sis.com/1234/subtitles_fr.ttml", // "language": "fr" // } self.subtitles = event.getStreamData().subtitles; onStreamDataReceived(streamUrl); }, false); this.streamManager_.addEventListener( google.ima.dai.api.StreamEvent.Type.ERROR, function(event) { var errorMessage = event.getStreamData().errorMessage; self.broadcast_(errorMessage); }, false); this.streamManager_.addEventListener( google.ima.dai.api.StreamEvent.Type.COMPLETE, function(event) { self.broadcast_('complete'); }, false); this.streamManager_.addEventListener( google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, function(event) { self.adIsPlaying_ = true; self.broadcast_('ad_break_started'); }, false); this.streamManager_.addEventListener( google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, function(event) { self.adIsPlaying_ = false; self.broadcast_('ad_break_ended'); }, false); }; /** * Loads stitched ads+content stream. * @param {!string} url of the stream. */ Player.prototype.onStreamDataReceived = function(url) { var self = this; var host = new cast.player.api.Host({ 'url': url, 'mediaElement': this.mediaElement_ }); this.broadcast_('onStreamDataReceived: ' + url); host.processMetadata = function(type, data, timestamp) { this.streamManager_.processMetadata(type, data, timestamp); }; var currentTime = this.startTime_ > 0 ? this.streamManager_ .streamTimeForContentTime(this.startTime_) : 0; this.broadcast_('start time: ' + currentTime); this.castPlayer_ = new cast.player.api.Player(host); this.castPlayer_.load( cast.player.api.CreateHlsStreamingProtocol(host), currentTime); if (this.subtitles[0] && this.subtitles[0].ttml) { this.castPlayer_.enableCaptions(true, 'ttml', this.subtitles[0].ttml); } };
To work with DAI, a player must pass ID3 events for live streams to the IMA SDKs as shown in the sample code.
The code shown above initializes the IMA
StreamManager
, which is responsible for requesting DAI streams from the SDK. TheinitStreamManager
method creates a newStreamManager
and registers several event listeners that handle different ad events.The
LOADED
event is fired when the stream manifest is available. It includes the stream's URL and subtitle info. The event handler then calls theonStreamDataReceived()
function, which loads the information into aPlayer
object created with the Cast SDK.The
AD_BREAK_STARTED
event is fired when an ad break starts. It is used in this example to disable seeking while an ad is playing. This is bookended by theAD_BREAK_ENDED
event, which is fired when an ad break ends, and where the example receiver re-enables seeking.The constructor for the
Player
implemented anonMessage()
function to handle messages coming from the sender. To implement the necessary message handling functions, add the following to player.js:/** * Gets content time for the stream. * @return {number} The content time. * @private */ Player.prototype.getContentTime_ = function() { return this.streamManager_ .contentTimeForStreamTime(this.mediaElement_.currentTime); }; /** * Sends messages to all connected sender apps. * @param {!string} message Message to be sent to senders. * @private */ Player.prototype.broadcast_ = function(message) { if (this.imaMessageBus_ && this.imaMessageBus_.broadcast) { this.imaMessageBus_.broadcast(message); } }; /** * Seeks player to location. * @param {number} time The time to seek to in seconds. * @private */ Player.prototype.seek_ = function(time) { if (this.adIsPlaying_) { return; } this.mediaElement_.currentTime = time; this.broadcast_('Seeking to: ' + time); };
This code defines some methods to handle messages from the sender.
getContentTime()
sends the content time (the progress in the stream minus the time spent playing ads) to the sender. The sender uses that time to resume the stream locally if it disconnects from the receiver. The seek function checks to see if an ad is playing before seeking.Add methods to start the stream's playback and handle the sender disconnecting from the receiver to player.js:
/** * Starts receiver manager which tracks playback of the stream. */ Player.prototype.start = function() { this.receiverManager_.start(); }; /** * Called when a sender disconnects from the app. * @param {cast.receiver.CastReceiverManager.SenderDisconnectedEvent} event */ Player.prototype.onSenderDisconnected = function(event) { console.log('onSenderDisconnected'); // When the last or only sender is connected to a receiver, // tapping Disconnect stops the app running on the receiver. if (this.receiverManager_.getSenders().length === 0 && event.reason === cast.receiver.system.DisconnectReason.REQUESTED_BY_SENDER) { this.receiverManager_.stop(); } };