How to add standard media controls to Google Cast app?
Asked Answered
A

2

8

I'm developing Google Cast custom receiver app using WebTorrent (https://webtorrent.io, https://github.com/feross/webtorrent) and Google Cast sender app using JavaScript (Chrome) SDK.

The idea of my app is sending torrent id (magnet URI like magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d or HTTP/HTTPS URL to a *.torrent file like https://webtorrent.io/torrents/sintel.torrent) from Google Cast sender to Google Cast receiver, and using WebTorrent in Google Cast receiver to display the media (video or audio) from the torrent.

Note that torrent id is not a direct URL to the media file.

Now I'm using Google Cast namespace and messageBus to send and receive the torrent id.

WebTorrent API provides 2 ways to display the media:

Here is the code of my receiver:

<html>
  <head>
    <script src="https://www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
    <script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>
  </head>
  <body>
    <video autoplay id='media' />
    <script>
      window.mediaElement = document.getElementById('media');
      window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);
      window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
      window.messageBus = window.castReceiverManager.getCastMessageBus('urn:x-cast:com.google.cast.sample.helloworld');
      window.messageBus.onMessage = function(event) {
        displayVideo(event.data);
        // Inform all senders on the CastMessageBus of the incoming message event
        // sender message listener will be invoked
        window.messageBus.send(event.senderId, event.data);
      };
      function displayVideo(torrentId) {
        var client = new WebTorrent();
        client.add(torrentId, function (torrent) {
          var file = torrent.files[0];
          file.renderTo('video');
        });
      }
      window.castReceiverManager.start();
    </script>
  </body>
</html>

Here is the code of my sender:

<!--
Copyright (C) 2014 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<style type="text/css">
html, body, #wrapper {
   height:100%;
   width: 100%;
   margin: 0;
   padding: 0;
   border: 0;
}
#wrapper td {
   vertical-align: middle;
   text-align: center;
}
input {
  font-family: "Arial", Arial, sans-serif;
  font-size: 40px;
  font-weight: bold;
}
.border {
    border: 2px solid #cccccc;
    border-radius: 5px;
}
.border:focus { 
    outline: none;
    border-color: #8ecaed;
    box-shadow: 0 0 5px #8ecaed;
}
</style>
<script type="text/javascript" src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"></script>
<script type="text/javascript">
var applicationID = 'F5304A3D';
var namespace = 'urn:x-cast:com.google.cast.sample.helloworld';
var session = null;

/**
 * Call initialization for Cast
 */
if (!chrome.cast || !chrome.cast.isAvailable) {
  setTimeout(initializeCastApi, 1000);
}

/**
 * initialization
 */
function initializeCastApi() {
  var sessionRequest = new chrome.cast.SessionRequest(applicationID);
  var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
    sessionListener,
    receiverListener);

  chrome.cast.initialize(apiConfig, onInitSuccess, onError);
};

/**
 * initialization success callback
 */
function onInitSuccess() {
  appendMessage("onInitSuccess");
}

/**
 * initialization error callback
 */
function onError(message) {
  appendMessage("onError: "+JSON.stringify(message));
}

/**
 * generic success callback
 */
function onSuccess(message) {
  appendMessage("onSuccess: "+message);
}

/**
 * callback on success for stopping app
 */
function onStopAppSuccess() {
  appendMessage('onStopAppSuccess');
}

/**
 * session listener during initialization
 */
function sessionListener(e) {
  appendMessage('New session ID:' + e.sessionId);
  session = e;
  session.addUpdateListener(sessionUpdateListener);
  session.addMessageListener(namespace, receiverMessage);
}

/**
 * listener for session updates
 */
function sessionUpdateListener(isAlive) {
  var message = isAlive ? 'Session Updated' : 'Session Removed';
  message += ': ' + session.sessionId;
  appendMessage(message);
  if (!isAlive) {
    session = null;
  }
};

/**
 * utility function to log messages from the receiver
 * @param {string} namespace The namespace of the message
 * @param {string} message A message string
 */
function receiverMessage(namespace, message) {
  appendMessage("receiverMessage: "+namespace+", "+message);
};

/**
 * receiver listener during initialization
 */
function receiverListener(e) {
  if( e === 'available' ) {
    appendMessage("receiver found");
  }
  else {
    appendMessage("receiver list empty");
  }
}

/**
 * stop app/session
 */
function stopApp() {
  session.stop(onStopAppSuccess, onError);
}

/**
 * send a message to the receiver using the custom namespace
 * receiver CastMessageBus message handler will be invoked
 * @param {string} message A message string
 */
function sendMessage(message) {
  if (session!=null) {
    session.sendMessage(namespace, message, onSuccess.bind(this, "Message sent: " + message), onError);
  }
  else {
    chrome.cast.requestSession(function(e) {
        session = e;
        session.sendMessage(namespace, message, onSuccess.bind(this, "Message sent: " + message), onError);
      }, onError);
  }
}

/**
 * append message to debug message window
 * @param {string} message A message string
 */
function appendMessage(message) {
  console.log(message);
  var dw = document.getElementById("debugmessage");
  dw.innerHTML += '\n' + JSON.stringify(message);
};

/**
 * utility function to handle text typed in by user in the input field
 */
function update() {
  sendMessage(document.getElementById("input").value);
}

/**
 * handler for the transcribed text from the speech input
 * @param {string} words A transcibed speech string
 */
function transcribe(words) {
  sendMessage(words);
}
</script>
</head>
<body>
  <table id="wrapper">
    <tr>
        <td>
            <form method="get" action="JavaScript:update();">
                <input id="input" class="border" type="text" size="30" onwebkitspeechchange="transcribe(this.value)" x-webkit-speech/>
            </form>
        </td>
    </tr>
  </table>

  <!-- Debbugging output -->
  <div style="margin:10px; visibility:hidden;">
    <textarea rows="20" cols="70" id="debugmessage">
    </textarea>
  </div>

<script type="text/javascript">
  document.getElementById("input").focus();
</script>
</body>
</html>

The problem: The receiver handles torrent id from sender and video plays as expected. But official Google Cast app or official Google Cast extension for Chrome doesn't show standard media controls for playing video to pause, stop, seek, etc.

This is what I have (this is a screenshot of standard built-in modal dialog for Google Cast in the latest version of Google Chrome):

Screenshot of standard built-in modal dialog for Google Cast in the latest version of Google Chrome

This is what I want to achieve (this is a screenshot of standard built-in modal dialog for Google Cast in the latest version of Google Chrome):

Screenshot of standard built-in modal dialog for Google Cast in the latest version of Google Chrome

Adding

window.mediaElement = document.getElementById('media');
window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);

for

<video autoplay id='media' />

element don't help.

Should I add something to sender and/or receiver to add standard media controls for <video autoplay id='media' /> on all senders?

Maybe there is another way to send and receive torrent id without using Google Cast namespace and messageBus?

UPD

Looks like I've found the root of my problem...

How to enable default media controls for existing playing video in receiver?

For example, the receiver app already have playing video:

<video autoplay id='media'
src='https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
/>

How to enable default media controls - working buttons "Play/Pause", working progress bar (on all senders like official Google Cast extension for Chrome) for this playing video?

Looks like adding the following code not help:

window.mediaElement = document.getElementById('media');
window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);
window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
window.castReceiverManager.start();

Here is the full source code of receiver:

<html>
<head>
<script src="https://www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script>
</head>
<body>
<video autoplay id='media'
src='https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
/>
<script>
window.mediaElement = document.getElementById('media');
window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);
window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();
window.castReceiverManager.start();
</script>
</body>
</html>

UPD2:

Looks like it is possible to use any text string (the torrent id in my case) instead of media URL in chrome.cast.media.MediaInfo and use the media namespace instead of using custom namespace and custom message bus (i.e. without using https://developers.google.com/cast/docs/reference/receiver/cast.receiver.CastReceiverManager#getCastMessageBus and https://developers.google.com/cast/docs/reference/receiver/cast.receiver.CastMessageBus and https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session#sendMessage):

function cast() {
  url = 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d';
  chrome.cast.requestSession(function(session) {
    var mediaInfo = new chrome.cast.media.MediaInfo(url);
    //mediaInfo.contentType = 'video/mp4';
    //mediaInfo.contentType = 'audio/mpeg';
    //mediaInfo.contentType = 'image/jpeg';
    var request = new chrome.cast.media.LoadRequest(mediaInfo);
    request.autoplay = true;
    session.loadMedia(request, function() {}, onError);
  }, onError);
}

But how to handle it on the receiver in this case?

Antung answered 26/4, 2016 at 11:7 Comment(0)
C
1

There is actually an existing Google Cast UX Guidelines which states that the sender application must provide a top-level Cast button. There are three ways to support a Cast button which were fully discussed in Android Sender App Development

  • Using the MediaRouter ActionBar provider: android.support.v7.app.MediaRouteActionProvider
  • Using the MediaRouter Cast button: android.support.v7.app.MediaRouteButton
  • Developing a custom UI with the MediaRouter API’s and MediaRouter.Callback

To show standard media controls once the media is playing, the sender application can control the media playback using the RemoteMediaPlayer instance. Steps and examples can be found in the documentation.

For a comprehensive listing of all classes, methods and events in the Google Cast Android SDK, see the Google Cast Android API Reference.

Cubbyhole answered 27/4, 2016 at 15:12 Comment(2)
Thanks for your reply, but this question about developing a JavaScript (Chrome) Google Cast sender (developers.google.com/cast/docs/chrome_sender), not an Android sender.Antung
Apologies for sending documents related to Android instead of JavasScript. Anyways, thank you for editing your post and tagging JavaScript. :)Cubbyhole
H
0

I realize it's been 3 years, but what jumps out at me is that you are missing the "controls" attribute on your video tag! Without the attribute, a player will render the video but provide no ui for controlling playback....

Syntax is the same as it is for autoplay: the controls attribute is standalone and takes no value. It is all or nothing - a standard set of controls with default styling, or none at all... if you were building for the browser, you might choose to omit the attribute and make your own controls to match the look and feel of the page. But based on the screenshots you shared, there's no need (and it might not work in a chromecast receiver environment)

The correct html is below, and that should be all you need :)

https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' />

Please let us know if and how you ended up fixing your problem (and send me a DM... I recently started integrating webtorrent into a streaming video platform I'm creating and so far, so good, but the documentation is pretty bad and I have a few questions. Thanks!)

Habitual answered 26/4, 2016 at 11:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.