Warning: The DAI SDK for Cast SDK v2 has been officially deprecated, as of November 17th, 2021. All existing users are urged to migrate to the CAF DAI SDK.

HTML5 receiver app

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

  1. Create an HTML page for your custom receiver by putting <!DOCTYPE html> and </html> tags into a file called player.html.

  2. 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>
    
  3. 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

  1. 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 the CastReceiverManager, which is responsible for controlling Cast receivers and registering event handlers for the onSenderConnected() and onSenderDisconnected() 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 the MediaManager.onLoad() and MediaManager.onSeek() methods.

  2. To be ready when the sender asks the receiver to load or seek the stream, add the onLoad() and onSeek() 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 the StreamManager (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. The onSeek() performs a custom seek request on the stream in lieu of a normal seek command, discussed later.

  3. 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. The initStreamManager method creates a new StreamManager 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 the onStreamDataReceived() function, which loads the information into a Player 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 the AD_BREAK_ENDED event, which is fired when an ad break ends, and where the example receiver re-enables seeking.

  4. The constructor for the Player implemented an onMessage() 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.

  5. 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();
      }
    };