author | Randall Barker <rbarker@mozilla.com> |
Fri, 19 Sep 2014 13:50:00 +0200 | |
changeset 206470 | 4f539eec6a3edd6568f3ea685f6018aa23f34c16 |
parent 206469 | 6e195997b6848f3a0d4173030b518db3b3c29d20 |
child 206471 | 6ebbcb6f3b0347fe1e53d7a0cdefc3d744e23284 |
push id | 27528 |
push user | ryanvm@gmail.com |
push date | Mon, 22 Sep 2014 19:27:54 +0000 |
treeherder | mozilla-central@d8688cafc752 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | wesj, mfinkle |
bugs | 1048425 |
milestone | 35.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -278,16 +278,23 @@ pref("browser.search.noCurrentEngine", t #ifdef MOZ_OFFICIAL_BRANDING // {moz:official} expands to "official" pref("browser.search.official", true); #endif // Control media casting feature pref("browser.casting.enabled", true); +#ifdef RELEASE_BUILD +pref("browser.mirroring.enabled", false); +pref("browser.mirroring.enabled.roku", false); +#else +pref("browser.mirroring.enabled", true); +pref("browser.mirroring.enabled.roku", true); +#endif // Enable sparse localization by setting a few package locale overrides pref("chrome.override_package.global", "browser"); pref("chrome.override_package.mozapps", "browser"); pref("chrome.override_package.passwordmgr", "browser"); // enable xul error pages pref("browser.xul.error_pages.enabled", true);
--- a/mobile/android/chrome/content/CastingApps.js +++ b/mobile/android/chrome/content/CastingApps.js @@ -11,16 +11,17 @@ XPCOMUtils.defineLazyModuleGetter(this, // JSM files, but we left them here to allow for better lazy JSM loading. var rokuDevice = { id: "roku:ecp", target: "roku:ecp", factory: function(aService) { Cu.import("resource://gre/modules/RokuApp.jsm"); return new RokuApp(aService); }, + mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"), types: ["video/mp4"], extensions: ["mp4"] }; var fireflyDevice = { id: "firefly:dial", target: "urn:dial-multiscreen-org:service:dial:1", filters: { @@ -47,17 +48,17 @@ var mediaPlayerDevice = { }; var CastingApps = { _castMenuId: -1, mirrorStartMenuId: -1, mirrorStopMenuId: -1, init: function ca_init() { - if (!this.isEnabled()) { + if (!this.isCastingEnabled()) { return; } // Register targets SimpleServiceDiscovery.registerDevice(rokuDevice); SimpleServiceDiscovery.registerDevice(fireflyDevice); SimpleServiceDiscovery.registerDevice(mediaPlayerDevice); @@ -94,50 +95,58 @@ var CastingApps = { Services.obs.removeObserver(this, "Casting:Stop"); Services.obs.removeObserver(this, "Casting:Mirror"); Services.obs.removeObserver(this, "ssdp-service-found"); Services.obs.removeObserver(this, "ssdp-service-lost"); NativeWindow.contextmenus.remove(this._castMenuId); }, + _mirrorStarted: function(stopMirrorCallback) { + this.stopMirrorCallback = stopMirrorCallback; + NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false }); + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true }); + }, + serviceAdded: function(aService) { - if (aService.mirror && this.mirrorStartMenuId == -1) { + if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) { this.mirrorStartMenuId = NativeWindow.menu.add({ name: Strings.browser.GetStringFromName("casting.mirrorTab"), callback: function() { - function callbackFunc(aService) { + let callbackFunc = function(aService) { let app = SimpleServiceDiscovery.findAppForService(aService); - if (app) - app.mirror(function() { - }); - } + if (app) { + app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this)); + } + }.bind(this); - function filterFunc(aService) { - return aService.mirror == true; - } - this.prompt(callbackFunc, filterFunc); + this.prompt(callbackFunc, aService => aService.mirror); }.bind(this), parent: NativeWindow.menu.toolsMenuID }); this.mirrorStopMenuId = NativeWindow.menu.add({ name: Strings.browser.GetStringFromName("casting.mirrorTabStop"), callback: function() { if (this.tabMirror) { this.tabMirror.stop(); this.tabMirror = null; + } else if (this.stopMirrorCallback) { + this.stopMirrorCallback(); + this.stopMirrorCallback = null; } NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true }); NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); }.bind(this), parent: NativeWindow.menu.toolsMenuID }); } - NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + if (this.mirrorStartMenuId != -1) { + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false }); + } }, serviceLost: function(aService) { if (aService.mirror && this.mirrorStartMenuId != -1) { let haveMirror = false; SimpleServiceDiscovery.services.forEach(function(service) { if (service.mirror) { haveMirror = true; @@ -145,20 +154,24 @@ var CastingApps = { }); if (!haveMirror) { NativeWindow.menu.remove(this.mirrorStartMenuId); this.mirrorStartMenuId = -1; } } }, - isEnabled: function isEnabled() { + isCastingEnabled: function isCastingEnabled() { return Services.prefs.getBoolPref("browser.casting.enabled"); }, + isMirroringEnabled: function isMirroringEnabled() { + return Services.prefs.getBoolPref("browser.mirroring.enabled"); + }, + observe: function (aSubject, aTopic, aData) { switch (aTopic) { case "Casting:Play": if (this.session && this.session.remoteMedia.status == "paused") { this.session.remoteMedia.play(); } break; case "Casting:Pause":
--- a/mobile/android/modules/RokuApp.jsm +++ b/mobile/android/modules/RokuApp.jsm @@ -6,16 +6,20 @@ "use strict"; this.EXPORTED_SYMBOLS = ["RokuApp"]; const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); +const WEBRTC_PLAYER_NAME = "WebRTC Player"; +const MIRROR_PORT = 8011; +const JSON_MESSAGE_TERMINATOR = "\r\n"; + function log(msg) { //Services.console.logStringMessage(msg); } const PROTOCOL_VERSION = 1; /* RokuApp is a wrapper for interacting with a Roku channel. * The basic interactions all use a REST API. @@ -24,36 +28,39 @@ const PROTOCOL_VERSION = 1; function RokuApp(service) { this.service = service; this.resourceURL = this.service.location; #ifdef RELEASE_BUILD this.app = "Firefox"; #else this.app = "Firefox Nightly"; #endif - this.appID = -1; + this.mediaAppID = -1; + this.mirrorAppID = -1; } RokuApp.prototype = { status: function status(callback) { // We have no way to know if the app is running, so just return "unknown" - // but we use this call to fetch the appID for the given app name + // but we use this call to fetch the mediaAppID for the given app name let url = this.resourceURL + "query/apps"; let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("GET", url, true); xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; xhr.overrideMimeType("text/xml"); xhr.addEventListener("load", (function() { if (xhr.status == 200) { let doc = xhr.responseXML; let apps = doc.querySelectorAll("app"); for (let app of apps) { if (app.textContent == this.app) { - this.appID = app.id; + this.mediaAppID = app.id; + } else if (app.textContent == WEBRTC_PLAYER_NAME) { + this.mirrorAppID = app.id } } } // Since ECP has no way of telling us if an app is running, we always return "unknown" if (callback) { callback({ state: "unknown" }); } @@ -64,33 +71,33 @@ RokuApp.prototype = { callback({ state: "unknown" }); } }).bind(this), false); xhr.send(null); }, start: function start(callback) { - // We need to make sure we have cached the appID - if (this.appID == -1) { + // We need to make sure we have cached the mediaAppID + if (this.mediaAppID == -1) { this.status(function() { - // If we found the appID, use it to make a new start call - if (this.appID != -1) { + // If we found the mediaAppID, use it to make a new start call + if (this.mediaAppID != -1) { this.start(callback); } else { // We failed to start the app, so let the caller know callback(false); } }.bind(this)); return; } // Start a given app with any extra query data. Each app uses it's own data scheme. // NOTE: Roku will also pass "source=external-control" as a param - let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION); + let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION); let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); xhr.open("POST", url, true); xhr.overrideMimeType("text/plain"); xhr.addEventListener("load", (function() { if (callback) { callback(xhr.status === 200); } @@ -124,25 +131,63 @@ RokuApp.prototype = { callback(false); } }).bind(this), false); xhr.send(null); }, remoteMedia: function remoteMedia(callback, listener) { - if (this.appID != -1) { + if (this.mediaAppID != -1) { if (callback) { callback(new RemoteMedia(this.resourceURL, listener)); } } else { if (callback) { callback(); } } + }, + + mirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // The status function may not have been called yet if mirrorAppID is -1 + this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback)); + } else { + this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback); + } + }, + + _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) { + if (this.mirrorAppID == -1) { + // TODO: Inform user to install Roku WebRTC Player Channel. + log("RokuApp: Failed to find Mirror App ID."); + } else { + let url = this.resourceURL + "launch/" + this.mirrorAppID; + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + xhr.open("POST", url, true); + xhr.overrideMimeType("text/plain"); + + xhr.addEventListener("load", (function() { + // 204 seems to be returned if the channel is already running + if ((xhr.status == 200) || (xhr.status == 204)) { + this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback); + } + }).bind(this), false); + + xhr.addEventListener("error", function() { + log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME); + }, false); + + xhr.send(null); + } + + if (callback) { + callback(); + } } } /* RemoteMedia provides a wrapper for using TCP socket to control Roku apps. * The server implementation must be built into the Roku receiver app. */ function RemoteMedia(url, listener) { this._url = url; @@ -220,16 +265,158 @@ RemoteMedia.prototype = { // TODO: add position support this._sendMsg({ type: "PLAY" }); }, pause: function pause() { this._sendMsg({ type: "STOP" }); }, - load: function load(aData) { - this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster }); + load: function load(data) { + this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster }); }, get status() { return this._status; } } + +function RemoteMirror(url, win, viewport, mirrorStartedCallback) { + this._serverURI = Services.io.newURI(url , null, null); + this._window = win; + this._iceCandidates = []; + this.mirrorStarted = mirrorStartedCallback; + + // This code insures the generated tab mirror is not wider than 800 nor taller than 600 + // Better dimensions should be chosen after the Roku Channel is working. + let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + let cWidth = Math.max(viewport.cssWidth, viewport.width); + let cHeight = Math.max(viewport.cssHeight, viewport.height); + + const MAX_WIDTH = 800; + const MAX_HEIGHT = 600; + + let tWidth = 0; + let tHeight = 0; + + if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) { + tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight); + tWidth = MAX_WIDTH; + } else { + tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth); + tHeight = MAX_HEIGHT; + } + + let constraints = { + video: { + mediaSource: "browser", + browserWindow: windowId, + scrollWithPage: true, + advanced: [ + { + width: { min: tWidth, max: tWidth }, + height: { min: tHeight, max: tHeight } + }, + { aspectRatio: cWidth / cHeight } + ] + } + }; + + this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {}); +} + +RemoteMirror.prototype = { + _sendOffer: function(offer) { + if (!this._baseSocket) { + this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket); + } + this._jsonOffer = JSON.stringify(offer); + this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" }); + this._socket.onopen = this._onSocketOpen.bind(this); + this._socket.ondata = this._onSocketData.bind(this); + this._socket.onerror = this._onSocketError.bind(this); + }, + + _onReceiveGUMStream: function(stream) { + this._pc = new this._window.mozRTCPeerConnection; + this._pc.addStream(stream); + this._pc.onicecandidate = (evt => { + // Usually the last candidate is null, expected? + if (!evt.candidate) { + return; + } + let jsonCandidate = JSON.stringify(evt.candidate); + this._iceCandidates.push(jsonCandidate); + this._sendIceCandidates(); + }); + + this._pc.createOffer(offer => { + this._pc.setLocalDescription( + new this._window.mozRTCSessionDescription(offer), + () => this._sendOffer(offer), + () => log("RemoteMirror: Failed to set local description.")); + }, + () => log("RemoteMirror: Failed to create offer.")); + }, + + _stopMirror: function() { + if (this._socket) { + this._socket.close(); + this._socket = null; + } + if (this._pc) { + this._pc.close(); + this._pc = null; + } + this._jsonOffer = null; + this._iceCandidates = []; + }, + + _onSocketData: function(response) { + if (response.type == "data") { + response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => { + if (data) { + let parsedData = JSON.parse(data); + if (parsedData.type == "answer") { + this._pc.setRemoteDescription( + new this._window.mozRTCSessionDescription(parsedData), + () => this.mirrorStarted(this._stopMirror.bind(this)), + () => log("RemoteMirror: Failed to set remote description.")); + } else { + this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData)) + } + } else { + log("RemoteMirror: data is null"); + } + }); + } else if (response.type == "error") { + log("RemoteMirror: Got socket error."); + this._stopMirror(); + } else { + log("RemoteMirror: Got unhandled socket event: " + response.type); + } + }, + + _onSocketError: function(err) { + log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA")); + this._stopMirror(); + }, + + _onSocketOpen: function() { + this._open = true; + if (this._jsonOffer) { + let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR; + this._socket.send(jsonOffer, jsonOffer.length); + this._jsonOffer = null; + this._sendIceCandidates(); + } + }, + + _sendIceCandidates: function() { + if (this._socket && this._open) { + this._iceCandidates.forEach(value => { + value = value + JSON_MESSAGE_TERMINATOR; + this._socket.send(value, value.length); + }); + this._iceCandidates = []; + } + } +};
--- a/mobile/android/modules/SimpleServiceDiscovery.jsm +++ b/mobile/android/modules/SimpleServiceDiscovery.jsm @@ -404,16 +404,20 @@ var SimpleServiceDiscovery = { _addService: function(service) { // Filter out services that do not match the device filter if (!this._filterService(service)) { return; } // Only add and notify if we don't already know about this service if (!this._services.has(service.uuid)) { + let device = this._devices.get(service.target); + if (device && device.mirror) { + service.mirror = true; + } this._services.set(service.uuid, service); Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid); } // Make sure we remember this service is not stale this._services.get(service.uuid).lastPing = this._searchTimestamp; } }