Commit 2f4e73e9 authored by aiguha@chromium.org's avatar aiguha@chromium.org

The CastExtensionHandler handles interaction with the Cast Host Extension of

the chromoting host.
It only modifies webapp behavior if the "casting" capability is part of the
negotiated set between host and client.

It performs the following tasks:
1. Sends and receives extension messages to/from the host.
2. Initializes and uses the Google Cast Chrome Sender API library to interact
with nearby Cast Receivers, acting as a Sender App.
3. Acts as a message proxy between the Cast Host Extension and the Cast
Receiver, brokering their peer connection.

Review URL: https://codereview.chromium.org/464133003

Cr-Commit-Position: refs/heads/master@{#290140}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@290140 0039d316-1c4b-4281-b951-d872f2087c98
parent 23935931
......@@ -122,6 +122,10 @@
'remoting_webapp_js_gnubby_auth_files': [
'webapp/gnubby_auth_handler.js',
],
# cast extension handler JavaScript files.
'remoting_webapp_js_cast_extension_files': [
'webapp/cast_extension_handler.js',
],
# browser test JavaScript files.
'remoting_webapp_js_browser_test_files': [
'webapp/browser_test/browser_test.js',
......@@ -164,6 +168,7 @@
'<@(remoting_webapp_js_auth_google_files)',
'<@(remoting_webapp_js_client_files)',
'<@(remoting_webapp_js_gnubby_auth_files)',
'<@(remoting_webapp_js_cast_extension_files)',
'<@(remoting_webapp_js_host_files)',
'<@(remoting_webapp_js_logging_files)',
'<@(remoting_webapp_js_ui_files)',
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Description of this file.
* Class handling interaction with the cast extension session of the Chromoting
* host. It receives and sends extension messages from/to the host through
* the client session. It uses the Google Cast Chrome Sender API library to
* interact with nearby Cast receivers.
*
* Once it establishes connection with a Cast device (upon user choice), it
* creates a session, loads our registered receiver application and then becomes
* a message proxy between the host and cast device, helping negotiate
* their peer connection.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
* @param {!remoting.ClientSession} clientSession The client session to send
* cast extension messages to.
*/
remoting.CastExtensionHandler = function(clientSession) {
/** @private */
this.clientSession_ = clientSession;
/** @type {chrome.cast.Session} @private */
this.session_ = null;
/** @type {string} @private */
this.kCastNamespace_ = 'urn:x-cast:com.chromoting.cast.all';
/** @type {string} @private */
this.kApplicationId_ = "8A1211E3";
/** @type {Array.<Object>} @private */
this.messageQueue_ = [];
this.start_();
};
/**
* The id of the script node.
* @type {string}
* @private
*/
remoting.CastExtensionHandler.prototype.SCRIPT_NODE_ID_ = 'cast-script-node';
/**
* Attempts to load the Google Cast Chrome Sender API libary.
* @private
*/
remoting.CastExtensionHandler.prototype.start_ = function() {
var node = document.getElementById(this.SCRIPT_NODE_ID_);
if (node) {
console.error(
'Multiple calls to CastExtensionHandler.start_ not expected.');
return;
}
// Create a script node to load the Cast Sender API.
node = document.createElement('script');
node.id = this.SCRIPT_NODE_ID_;
node.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
node.type = 'text/javascript';
document.body.insertBefore(node, document.body.firstChild);
/** @type {remoting.CastExtensionHandler} */
var that = this;
var onLoad = function() {
window['__onGCastApiAvailable'] = that.onGCastApiAvailable.bind(that);
};
var onLoadError = function(event) {
console.error("Failed to load Chrome Cast Sender API.");
}
node.addEventListener('load', onLoad, false);
node.addEventListener('error', onLoadError, false);
};
/**
* Process Cast Extension Messages from the Chromoting host.
* @param {string} msgString The extension message's data.
*/
remoting.CastExtensionHandler.prototype.onMessage = function(msgString) {
var message = getJsonObjectFromString(msgString);
// Save messages to send after a session is established.
this.messageQueue_.push(message);
// Trigger the sending of pending messages, followed by the one just
// received.
if (this.session_) {
this.sendPendingMessages_();
}
};
/**
* Send cast-extension messages through the client session.
* @param {Object} response The JSON response to be sent to the host. The
* response object must contain the appropriate keys.
* @private
*/
remoting.CastExtensionHandler.prototype.sendMessageToHost_ =
function(response) {
this.clientSession_.sendCastExtensionMessage(response);
};
/**
* Send pending messages from the host to the receiver app.
* @private
*/
remoting.CastExtensionHandler.prototype.sendPendingMessages_ = function() {
var len = this.messageQueue_.length;
for(var i = 0; i<len; i++) {
this.session_.sendMessage(this.kCastNamespace_,
this.messageQueue_[i],
this.sendMessageSuccess.bind(this),
this.sendMessageFailure.bind(this));
}
this.messageQueue_ = [];
};
/**
* Event handler for '__onGCastApiAvailable' window event. This event is
* triggered if the Google Cast Chrome Sender API is available. We attempt to
* load this API in this.start(). If the API loaded successfully, we can proceed
* to initialize it and configure it to launch our Cast Receiver Application.
*
* @param {boolean} loaded True if the API loaded succesfully.
* @param {Object} errorInfo Info if the API load failed.
*/
remoting.CastExtensionHandler.prototype.onGCastApiAvailable =
function(loaded, errorInfo) {
if (loaded) {
this.initializeCastApi();
} else {
console.log(errorInfo);
}
};
/**
* Initialize the Cast API.
* @private
*/
remoting.CastExtensionHandler.prototype.initializeCastApi = function() {
var sessionRequest = new chrome.cast.SessionRequest(this.kApplicationId_);
var apiConfig =
new chrome.cast.ApiConfig(sessionRequest,
this.sessionListener.bind(this),
this.receiverListener.bind(this),
chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
chrome.cast.DefaultActionPolicy.CREATE_SESSION);
chrome.cast.initialize(
apiConfig, this.onInitSuccess.bind(this), this.onInitError.bind(this));
};
/**
* Callback for successful initialization of the Cast API.
*/
remoting.CastExtensionHandler.prototype.onInitSuccess = function() {
console.log("Initialization Successful.");
};
/**
* Callback for failed initialization of the Cast API.
*/
remoting.CastExtensionHandler.prototype.onInitError = function() {
console.error("Initialization Failed.");
};
/**
* Listener invoked when a session is created or connected by the SDK.
* Note: The requestSession method would not cause this callback to be invoked
* since it is passed its own listener.
* @param {chrome.cast.Session} session The resulting session.
*/
remoting.CastExtensionHandler.prototype.sessionListener = function(session) {
console.log('New Session:' + /** @type {string} */ (session.sessionId));
this.session_ = session;
if (this.session_.media.length != 0) {
// There should be no media associated with the session, since we never
// directly load media from the Sender application.
this.onMediaDiscovered('sessionListener', this.session_.media[0]);
}
this.session_.addMediaListener(
this.onMediaDiscovered.bind(this, 'addMediaListener'));
this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
this.session_.addMessageListener(this.kCastNamespace_,
this.chromotingMessageListener.bind(this));
this.session_.sendMessage(this.kCastNamespace_,
{subject : 'test', chromoting_data : 'Hello, Cast.'},
this.sendMessageSuccess.bind(this),
this.sendMessageFailure.bind(this));
this.sendPendingMessages_();
};
/**
* Listener invoked when a media session is created by another sender.
* @param {string} how How this callback was triggered.
* @param {chrome.cast.media.Media} media The media item discovered.
* @private
*/
remoting.CastExtensionHandler.prototype.onMediaDiscovered =
function(how, media) {
console.error("Unexpected media session discovered.");
};
/**
* Listener invoked when a cast extension message was sent to the cast device
* successfully.
* @private
*/
remoting.CastExtensionHandler.prototype.sendMessageSuccess = function() {
};
/**
* Listener invoked when a cast extension message failed to be sent to the cast
* device.
* @param {Object} error The error.
* @private
*/
remoting.CastExtensionHandler.prototype.sendMessageFailure = function(error) {
console.error('Failed to Send Message.', error);
};
/**
* Listener invoked when a cast extension message is received from the Cast
* device.
* @param {string} ns The namespace of the message received.
* @param {string} message The stringified JSON message received.
*/
remoting.CastExtensionHandler.prototype.chromotingMessageListener =
function(ns, message) {
if (ns === this.kCastNamespace_) {
try {
var messageObj = getJsonObjectFromString(message);
this.sendMessageToHost_(messageObj);
} catch (err) {
console.error('Failed to process message from Cast device.');
}
} else {
console.error("Unexpected message from Cast device.");
}
};
/**
* Listener invoked when there updates to the current session.
*
* @param {boolean} isAlive True if the session is still alive.
*/
remoting.CastExtensionHandler.prototype.sessionUpdateListener =
function(isAlive) {
var message = isAlive ? 'Session Updated' : 'Session Removed';
message += ': ' + this.session_.sessionId +'.';
console.log(message);
};
/**
* Listener invoked when the availability of a Cast receiver that supports
* the application in sessionRequest is known or changes.
*
* @param {chrome.cast.ReceiverAvailability} availability Receiver availability.
*/
remoting.CastExtensionHandler.prototype.receiverListener =
function(availability) {
if (availability === chrome.cast.ReceiverAvailability.AVAILABLE) {
console.log("Receiver(s) Found.");
} else {
console.error("No Receivers Available.");
}
};
/**
* Launches the associated receiver application by requesting that it be created
* on the Cast device. It uses the SessionRequest passed during initialization
* to determine what application to launch on the Cast device.
*
* Note: This method is intended to be used as a click listener for a custom
* cast button on the webpage. We currently use the default cast button in
* Chrome, so this method is unused.
*/
remoting.CastExtensionHandler.prototype.launchApp = function() {
chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this),
this.onLaunchError.bind(this));
};
/**
* Listener invoked when chrome.cast.requestSession completes successfully.
*
* @param {chrome.cast.Session} session The requested session.
*/
remoting.CastExtensionHandler.prototype.onRequestSessionSuccess =
function (session) {
this.session_ = session;
this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
if (this.session_.media.length != 0) {
this.onMediaDiscovered('onRequestSession', this.session_.media[0]);
}
this.session_.addMediaListener(
this.onMediaDiscovered.bind(this, 'addMediaListener'));
this.session_.addMessageListener(this.kCastNamespace_,
this.chromotingMessageListener.bind(this));
};
/**
* Listener invoked when chrome.cast.requestSession fails.
* @param {chrome.cast.Error} error The error code.
*/
remoting.CastExtensionHandler.prototype.onLaunchError = function(error) {
console.error("Error Casting to Receiver.", error);
};
/**
* Stops the running receiver application associated with the session.
* TODO(aiguha): When the user disconnects using the blue drop down bar,
* the client session should notify the CastExtensionHandler, which should
* call this method to close the session with the Cast device.
*/
remoting.CastExtensionHandler.prototype.stopApp = function() {
this.session_.stop(this.onStopAppSuccess.bind(this),
this.onStopAppError.bind(this));
};
/**
* Listener invoked when the receiver application is stopped successfully.
*/
remoting.CastExtensionHandler.prototype.onStopAppSuccess = function() {
};
/**
* Listener invoked when we fail to stop the receiver application.
*
* @param {chrome.cast.Error} error The error code.
*/
remoting.CastExtensionHandler.prototype.onStopAppError = function(error) {
console.error('Error Stopping App: ', error);
};
......@@ -67,6 +67,9 @@ remoting.ClientPlugin = function(container, onExtensionMessage) {
*/
this.updateMouseCursorImage = function(url, hotspotX, hotspotY) {};
/** @param {string} data Remote cast extension message. */
this.onCastExtensionHandler = function(data) {};
/** @type {remoting.MediaSourceRenderer} */
this.mediaSourceRenderer_ = null;
......@@ -263,6 +266,13 @@ remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
// Let the host know that we can use the video framerecording extension.
this.capabilities_.push(
remoting.ClientSession.Capability.VIDEO_RECORDER);
// Let the host know that we can support casting of the screen.
// TODO(aiguha): Add this capability based on a gyp/command-line flag,
// rather than by default.
this.capabilities_.push(
remoting.ClientSession.Capability.CAST);
} else if (this.pluginApiVersion_ >= 6) {
this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
} else {
......@@ -357,6 +367,9 @@ remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
case 'test-echo-reply':
console.log('Got echo reply: ' + extMsgData);
break;
case 'cast_message':
this.onCastExtensionHandler(extMsgData);
break;
default:
if (!this.onExtensionMessage_(extMsgType, extMsgData)) {
console.log('Unexpected message received: ' +
......
......@@ -22,6 +22,13 @@
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* True if Cast capability is supported.
*
* @type {boolean}
*/
remoting.enableCast = false;
/**
* @param {HTMLElement} container Container element for the client view.
* @param {string} hostDisplayName A human-readable name for the host.
......@@ -166,6 +173,9 @@ remoting.ClientSession = function(container, hostDisplayName, accessCode,
/** @type {remoting.GnubbyAuthHandler} @private */
this.gnubbyAuthHandler_ = null;
/** @type {remoting.CastExtensionHandler} @private */
this.castExtensionHandler_ = null;
if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
// Resize-to-client is not supported for IT2Me hosts.
this.resizeToClientButton_.hidden = true;
......@@ -366,7 +376,8 @@ remoting.ClientSession.Capability = {
// this.plugin_.notifyClientResolution().
SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
VIDEO_RECORDER: 'videoRecorder'
VIDEO_RECORDER: 'videoRecorder',
CAST: 'casting'
};
/**
......@@ -548,6 +559,8 @@ remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
this.plugin_.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this);
this.plugin_.onGnubbyAuthHandler = this.processGnubbyAuthMessage_.bind(this);
this.plugin_.updateMouseCursorImage = this.updateMouseCursorImage_.bind(this);
this.plugin_.onCastExtensionHandler =
this.processCastExtensionMessage_.bind(this);
this.initiateConnection_();
};
......@@ -1070,6 +1083,7 @@ remoting.ClientSession.prototype.setState_ = function(newState) {
this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
if (this.state_ == remoting.ClientSession.State.CONNECTED) {
this.createGnubbyAuthHandler_();
this.createCastExtensionHandler_();
}
this.raiseEvent(remoting.ClientSession.Events.stateChanged,
......@@ -1539,3 +1553,43 @@ remoting.ClientSession.prototype.getPluginPositionForTesting = function() {
left: parseFloat(style.marginLeft)
};
};
/**
* Send a Cast extension message to the host.
* @param {Object} data The cast message data.
*/
remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) {
if (!this.plugin_)
return;
this.plugin_.sendClientMessage('cast_message', JSON.stringify(data));
};
/**
* Process a remote Cast extension message from the host.
* @param {string} data Remote cast extension data message.
* @private
*/
remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) {
if (this.castExtensionHandler_) {
try {
this.castExtensionHandler_.onMessage(data);
} catch (err) {
console.error('Failed to process cast message: ',
/** @type {*} */ (err));
}
} else {
console.error('Received unexpected cast message');
}
};
/**
* Create a CastExtensionHandler and inform the host that cast extension
* is supported.
* @private
*/
remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
if (remoting.enableCast && this.mode_ == remoting.ClientSession.Mode.ME2ME) {
this.castExtensionHandler_ = new remoting.CastExtensionHandler(this);
}
};
......@@ -385,3 +385,126 @@ function ClientRect() {
/** @type {number} */
this.right = 0;
};
/** @type {Object} */
chrome.cast = {};
/** @constructor */
chrome.cast.AutoJoinPolicy = function() {};
/** @type {chrome.cast.AutoJoinPolicy} */
chrome.cast.AutoJoinPolicy.PAGE_SCOPED;
/** @type {chrome.cast.AutoJoinPolicy} */
chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED;
/** @type {chrome.cast.AutoJoinPolicy} */
chrome.cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
/** @constructor */
chrome.cast.DefaultActionPolicy = function() {};
/** @type {chrome.cast.DefaultActionPolicy} */
chrome.cast.DefaultActionPolicy.CAST_THIS_TAB;
/** @type {chrome.cast.DefaultActionPolicy} */
chrome.cast.DefaultActionPolicy.CREATE_SESSION;
/** @constructor */
chrome.cast.Error = function() {};
/** @constructor */
chrome.cast.ReceiverAvailability = function() {};
/** @type {chrome.cast.ReceiverAvailability} */
chrome.cast.ReceiverAvailability.AVAILABLE;
/** @type {chrome.cast.ReceiverAvailability} */
chrome.cast.ReceiverAvailability.UNAVAILABLE;
/** @type {Object} */
chrome.cast.media = {};
/** @constructor */
chrome.cast.media.Media = function() {
/** @type {number} */
this.mediaSessionId = 0;
};
/** @constructor */
chrome.cast.Session = function() {
/** @type {Array.<chrome.cast.media.Media>} */
this.media = [];
/** @type {string} */
this.sessionId = '';
};
/**
* @param {string} namespace
* @param {Object} message
* @param {function():void} successCallback
* @param {function(chrome.cast.Error):void} errorCallback
*/
chrome.cast.Session.prototype.sendMessage =
function(namespace, message, successCallback, errorCallback) {};
/**
* @param {function(chrome.cast.media.Media):void} listener
*/
chrome.cast.Session.prototype.addMediaListener = function(listener) {};
/**
* @param {function(boolean):void} listener
*/
chrome.cast.Session.prototype.addUpdateListener = function(listener) {};
/**
* @param {string} namespace
* @param {function(chrome.cast.media.Media):void} listener
*/
chrome.cast.Session.prototype.addMessageListener =
function(namespace, listener){};
/**
* @param {function():void} successCallback
* @param {function(chrome.cast.Error):void} errorCallback
*/
chrome.cast.Session.prototype.stop =
function(successCallback, errorCallback) {};
/**
* @constructor
* @param {string} applicationID
*/
chrome.cast.SessionRequest = function(applicationID) {};
/**
* @constructor
* @param {chrome.cast.SessionRequest} sessionRequest
* @param {function(chrome.cast.Session):void} sessionListener
* @param {function(chrome.cast.ReceiverAvailability):void} receiverListener
* @param {chrome.cast.AutoJoinPolicy=} opt_autoJoinPolicy
* @param {chrome.cast.DefaultActionPolicy=} opt_defaultActionPolicy
*/
chrome.cast.ApiConfig = function(sessionRequest,
sessionListener,
receiverListener,
opt_autoJoinPolicy,
opt_defaultActionPolicy) {};
/**
* @param {chrome.cast.ApiConfig} apiConfig
* @param {function():void} onInitSuccess
* @param {function(chrome.cast.Error):void} onInitError
*/
chrome.cast.initialize =
function(apiConfig, onInitSuccess, onInitError) {};
/**
* @param {function(chrome.cast.Session):void} successCallback
* @param {function(chrome.cast.Error):void} errorCallback
*/
chrome.cast.requestSession =
function(successCallback, errorCallback) {};
......@@ -43,7 +43,7 @@
"js": [ "cs_third_party_auth_trampoline.js" ]
}
],
"content_security_policy": "default-src 'self'; script-src 'self' {{ TALK_GADGET_HOST }}; style-src 'self' https://fonts.googleapis.com; img-src 'self' {{ TALK_GADGET_HOST }} data:; font-src *; connect-src 'self' {{ OAUTH2_ACCOUNTS_HOST }} {{ GOOGLE_API_HOSTS }} {{ TALK_GADGET_HOST }} https://relay.google.com",
"content_security_policy": "default-src 'self'; script-src 'self' {{ TALK_GADGET_HOST }} https://www.gstatic.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' {{ TALK_GADGET_HOST }} data:; font-src *; connect-src 'self' {{ OAUTH2_ACCOUNTS_HOST }} {{ GOOGLE_API_HOSTS }} {{ TALK_GADGET_HOST }} https://relay.google.com",
{% endif %}
"optional_permissions": [
"<all_urls>"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment