diff options
Diffstat (limited to 'mobile/android/chrome/content/CastingApps.js')
-rw-r--r-- | mobile/android/chrome/content/CastingApps.js | 850 |
1 files changed, 850 insertions, 0 deletions
diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js new file mode 100644 index 000000000..76773c4d8 --- /dev/null +++ b/mobile/android/chrome/content/CastingApps.js @@ -0,0 +1,850 @@ +// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "PageActions", + "resource://gre/modules/PageActions.jsm"); + +// Define service devices. We should consider moving these to their respective +// 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); + }, + types: ["video/mp4"], + extensions: ["mp4"] +}; + +var mediaPlayerDevice = { + id: "media:router", + target: "media:router", + factory: function(aService) { + Cu.import("resource://gre/modules/MediaPlayerApp.jsm"); + return new MediaPlayerApp(aService); + }, + types: ["video/mp4", "video/webm", "application/x-mpegurl"], + extensions: ["mp4", "webm", "m3u", "m3u8"], + init: function() { + Services.obs.addObserver(this, "MediaPlayer:Added", false); + Services.obs.addObserver(this, "MediaPlayer:Changed", false); + Services.obs.addObserver(this, "MediaPlayer:Removed", false); + }, + observe: function(subject, topic, data) { + if (topic === "MediaPlayer:Added") { + let service = this.toService(JSON.parse(data)); + SimpleServiceDiscovery.addService(service); + } else if (topic === "MediaPlayer:Changed") { + let service = this.toService(JSON.parse(data)); + SimpleServiceDiscovery.updateService(service); + } else if (topic === "MediaPlayer:Removed") { + SimpleServiceDiscovery.removeService(data); + } + }, + toService: function(display) { + // Convert the native data into something matching what is created in _processService() + return { + location: display.location, + target: "media:router", + friendlyName: display.friendlyName, + uuid: display.uuid, + manufacturer: display.manufacturer, + modelName: display.modelName, + mirror: display.mirror + }; + } +}; + +var fxOSTVDevice = { + id: "app://fling-player.gaiamobile.org", + target: "app://fling-player.gaiamobile.org/index.html", + factory: function(aService) { + Cu.import("resource://gre/modules/PresentationApp.jsm"); + let request = new window.PresentationRequest(this.target); + return new PresentationApp(aService, request); + }, + init: function() { + Services.obs.addObserver(this, "presentation-device-change", false); + SimpleServiceDiscovery.addExternalDiscovery(this); + }, + observe: function(subject, topic, data) { + let device = subject.QueryInterface(Ci.nsIPresentationDevice); + let service = this.toService(device); + switch (data) { + case "add": + SimpleServiceDiscovery.addService(service); + break; + case "update": + SimpleServiceDiscovery.updateService(service); + break; + case "remove": + if(SimpleServiceDiscovery.findServiceForID(device.id)) { + SimpleServiceDiscovery.removeService(device.id); + } + break; + } + }, + toService: function(device) { + return { + location: device.id, + target: fxOSTVDevice.target, + friendlyName: device.name, + uuid: device.id, + manufacturer: "Firefox OS TV", + modelName: "Firefox OS TV", + }; + }, + startDiscovery: function() { + window.navigator.mozPresentationDeviceInfo.forceDiscovery(); + + // need to update the lastPing time for known device. + window.navigator.mozPresentationDeviceInfo.getAll() + .then(function(devices) { + for (let device of devices) { + let service = fxOSTVDevice.toService(device); + SimpleServiceDiscovery.addService(service); + } + }); + }, + stopDiscovery: function() { + // do nothing + }, + types: ["video/mp4", "video/webm"], + extensions: ["mp4", "webm"], +}; + +var CastingApps = { + _castMenuId: -1, + mirrorStartMenuId: -1, + mirrorStopMenuId: -1, + _blocked: null, + _bound: null, + _interval: 120 * 1000, // 120 seconds + + init: function ca_init() { + if (!this.isCastingEnabled()) { + return; + } + + // Register targets + SimpleServiceDiscovery.registerDevice(rokuDevice); + + // MediaPlayerDevice will notify us any time the native device list changes. + mediaPlayerDevice.init(); + SimpleServiceDiscovery.registerDevice(mediaPlayerDevice); + + // Presentation Device will notify us any time the available device list changes. + if (window.PresentationRequest) { + fxOSTVDevice.init(); + SimpleServiceDiscovery.registerDevice(fxOSTVDevice); + } + + // Search for devices continuously + SimpleServiceDiscovery.search(this._interval); + + this._castMenuId = NativeWindow.contextmenus.add( + Strings.browser.GetStringFromName("contextmenu.sendToDevice"), + this.filterCast, + this.handleContextMenu.bind(this) + ); + + Services.obs.addObserver(this, "Casting:Play", false); + Services.obs.addObserver(this, "Casting:Pause", false); + Services.obs.addObserver(this, "Casting:Stop", false); + Services.obs.addObserver(this, "Casting:Mirror", false); + Services.obs.addObserver(this, "ssdp-service-found", false); + Services.obs.addObserver(this, "ssdp-service-lost", false); + Services.obs.addObserver(this, "application-background", false); + Services.obs.addObserver(this, "application-foreground", false); + + BrowserApp.deck.addEventListener("TabSelect", this, true); + BrowserApp.deck.addEventListener("pageshow", this, true); + BrowserApp.deck.addEventListener("playing", this, true); + BrowserApp.deck.addEventListener("ended", this, true); + BrowserApp.deck.addEventListener("MozAutoplayMediaBlocked", this, true); + // Note that the XBL binding is untrusted + BrowserApp.deck.addEventListener("MozNoControlsVideoBindingAttached", this, true, true); + }, + + _mirrorStarted: function(stopMirrorCallback) { + this.stopMirrorCallback = stopMirrorCallback; + NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false }); + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true }); + }, + + serviceAdded: function(aService) { + if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) { + this.mirrorStartMenuId = NativeWindow.menu.add({ + name: Strings.browser.GetStringFromName("casting.mirrorTab"), + callback: function() { + let callbackFunc = function(aService) { + let app = SimpleServiceDiscovery.findAppForService(aService); + if (app) { + app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this), window.BrowserApp.selectedBrowser.contentWindow); + } + }.bind(this); + + 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), + }); + } + 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; + } + }); + if (!haveMirror) { + NativeWindow.menu.remove(this.mirrorStartMenuId); + this.mirrorStartMenuId = -1; + } + } + }, + + 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": + if (this.session && this.session.remoteMedia.status == "started") { + this.session.remoteMedia.pause(); + } + break; + case "Casting:Stop": + if (this.session) { + this.closeExternal(); + } + break; + case "Casting:Mirror": + { + Cu.import("resource://gre/modules/TabMirror.jsm"); + this.tabMirror = new TabMirror(aData, window); + NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false }); + NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true }); + } + break; + case "ssdp-service-found": + this.serviceAdded(SimpleServiceDiscovery.findServiceForID(aData)); + break; + case "ssdp-service-lost": + this.serviceLost(SimpleServiceDiscovery.findServiceForID(aData)); + break; + case "application-background": + // Turn off polling while in the background + this._interval = SimpleServiceDiscovery.search(0); + SimpleServiceDiscovery.stopSearch(); + break; + case "application-foreground": + // Turn polling on when app comes back to foreground + SimpleServiceDiscovery.search(this._interval); + break; + } + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "TabSelect": { + let tab = BrowserApp.getTabForBrowser(aEvent.target); + this._updatePageActionForTab(tab, aEvent); + break; + } + case "pageshow": { + let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView); + this._updatePageActionForTab(tab, aEvent); + break; + } + case "playing": + case "ended": { + let video = aEvent.target; + if (video instanceof HTMLVideoElement) { + // If playing, send the <video>, but if ended we send nothing to shutdown the pageaction + this._updatePageActionForVideo(aEvent.type === "playing" ? video : null); + } + break; + } + case "MozAutoplayMediaBlocked": { + if (this._bound && this._bound.has(aEvent.target)) { + aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo")); + } else { + if (!this._blocked) { + this._blocked = new WeakMap; + } + this._blocked.set(aEvent.target, true); + } + break; + } + case "MozNoControlsVideoBindingAttached": { + if (!this._bound) { + this._bound = new WeakMap; + } + this._bound.set(aEvent.target, true); + if (this._blocked && this._blocked.has(aEvent.target)) { + this._blocked.delete(aEvent.target); + aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo")); + } + break; + } + } + }, + + _sendEventToVideo: function _sendEventToVideo(aElement, aData) { + let event = aElement.ownerDocument.createEvent("CustomEvent"); + event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(aData)); + aElement.dispatchEvent(event); + }, + + handleVideoBindingAttached: function handleVideoBindingAttached(aTab, aEvent) { + // Let's figure out if we have everything needed to cast a video. The binding + // defaults to |false| so we only need to send an event if |true|. + let video = aEvent.target; + if (!(video instanceof HTMLVideoElement)) { + return; + } + + if (SimpleServiceDiscovery.services.length == 0) { + return; + } + + this.getVideo(video, 0, 0, (aBundle) => { + // Let the binding know casting is allowed + if (aBundle) { + this._sendEventToVideo(aBundle.element, { allow: true }); + } + }); + }, + + handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) { + // The binding wants to start a casting session + let video = aEvent.target; + if (!(video instanceof HTMLVideoElement)) { + return; + } + + // Close an existing session first. closeExternal has checks for an exsting + // session and handles remote and video binding shutdown. + this.closeExternal(); + + // Start the new session + UITelemetry.addEvent("cast.1", "button", null); + this.openExternal(video, 0, 0); + }, + + makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + allowableExtension: function(aURI, aExtensions) { + return (aURI instanceof Ci.nsIURL) && aExtensions.indexOf(aURI.fileExtension) != -1; + }, + + allowableMimeType: function(aType, aTypes) { + return aTypes.indexOf(aType) != -1; + }, + + // This method will look at the aElement (or try to find a video at aX, aY) that has + // a castable source. If found, aCallback will be called with a JSON meta bundle. If + // no castable source was found, aCallback is called with null. + getVideo: function(aElement, aX, aY, aCallback) { + let extensions = SimpleServiceDiscovery.getSupportedExtensions(); + let types = SimpleServiceDiscovery.getSupportedMimeTypes(); + + // Fast path: Is the given element a video element? + if (aElement instanceof HTMLVideoElement) { + // If we found a video element, no need to look further, even if no + // castable video source is found. + this._getVideo(aElement, types, extensions, aCallback); + return; + } + + // Maybe this is an overlay, with the video element under it. + // Use the (x, y) location to guess at a <video> element. + + // The context menu system will keep walking up the DOM giving us a chance + // to find an element we match. When it hits <html> things can go BOOM. + try { + let elements = aElement.ownerDocument.querySelectorAll("video"); + for (let element of elements) { + // Look for a video element contained in the overlay bounds + let rect = element.getBoundingClientRect(); + if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) { + // Once we find a <video> under the overlay, we check it and exit. + this._getVideo(element, types, extensions, aCallback); + return; + } + } + } catch(e) {} + }, + + _getContentTypeForURI: function(aURI, aElement, aCallback) { + let channel; + try { + let secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; + if (aElement.crossOrigin) { + secFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_DATA_INHERITS; + if (aElement.crossOrigin === "use-credentials") { + secFlags |= Ci.nsILoadInfo.SEC_COOKIES_INCLUDE; + } + } + channel = NetUtil.newChannel({ + uri: aURI, + loadingNode: aElement, + securityFlags: secFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_VIDEO + }); + } catch(e) { + aCallback(null); + return; + } + + let listener = { + onStartRequest: function(request, context) { + switch (channel.responseStatus) { + case 301: + case 302: + case 303: + request.cancel(0); + let location = channel.getResponseHeader("Location"); + CastingApps._getContentTypeForURI(CastingApps.makeURI(location), aElement, aCallback); + break; + default: + aCallback(channel.contentType); + request.cancel(0); + break; + } + }, + onStopRequest: function(request, context, statusCode) {}, + onDataAvailable: function(request, context, stream, offset, count) {} + }; + + if (channel) { + channel.asyncOpen2(listener); + } else { + aCallback(null); + } + }, + + // Because this method uses a callback, make sure we return ASAP if we know + // we have a castable video source. + _getVideo: function(aElement, aTypes, aExtensions, aCallback) { + // Keep a list of URIs we need for an async mimetype check + let asyncURIs = []; + + // Grab the poster attribute from the <video> + let posterURL = aElement.poster; + + // First, look to see if the <video> has a src attribute + let sourceURL = aElement.src; + + // If empty, try the currentSrc + if (!sourceURL) { + sourceURL = aElement.currentSrc; + } + + if (sourceURL) { + // Use the file extension to guess the mime type + let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI)); + if (this.allowableExtension(sourceURI, aExtensions)) { + aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI}); + return; + } + + if (aElement.type) { + // Fast sync check + if (this.allowableMimeType(aElement.type, aTypes)) { + aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aElement.type }); + return; + } + } + + // Delay the async check until we sync scan all possible URIs + asyncURIs.push(sourceURI); + } + + // Next, look to see if there is a <source> child element that meets + // our needs + let sourceNodes = aElement.getElementsByTagName("source"); + for (let sourceNode of sourceNodes) { + let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI)); + + // Using the type attribute is our ideal way to guess the mime type. Otherwise, + // fallback to using the file extension to guess the mime type + if (this.allowableExtension(sourceURI, aExtensions)) { + aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type }); + return; + } + + if (sourceNode.type) { + // Fast sync check + if (this.allowableMimeType(sourceNode.type, aTypes)) { + aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type }); + return; + } + } + + // Delay the async check until we sync scan all possible URIs + asyncURIs.push(sourceURI); + } + + // Helper method that walks the array of possible URIs, fetching the mimetype as we go. + // As soon as we find a good sourceURL, avoid firing the callback any further + var _getContentTypeForURIs = (aURIs) => { + // Do an async fetch to figure out the mimetype of the source video + let sourceURI = aURIs.pop(); + this._getContentTypeForURI(sourceURI, aElement, (aType) => { + if (this.allowableMimeType(aType, aTypes)) { + // We found a supported mimetype. + aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aType }); + } else { + // This URI was not a supported mimetype, so let's try the next, if we have more. + if (aURIs.length > 0) { + _getContentTypeForURIs(aURIs); + } else { + // We were not able to find a supported mimetype. + aCallback(null); + } + } + }); + } + + // If we didn't find a good URI directly, let's look using async methods. + if (asyncURIs.length > 0) { + _getContentTypeForURIs(asyncURIs); + } + }, + + // This code depends on handleVideoBindingAttached setting mozAllowCasting + // so we can quickly figure out if the video is castable + isVideoCastable: function(aElement, aX, aY) { + // Use the flag set when the <video> binding was created as the check + if (aElement instanceof HTMLVideoElement) { + return aElement.mozAllowCasting; + } + + // This is called by the context menu system and the system will keep + // walking up the DOM giving us a chance to find an element we match. + // When it hits <html> things can go BOOM. + try { + // Maybe this is an overlay, with the video element under it + // Use the (x, y) location to guess at a <video> element + let elements = aElement.ownerDocument.querySelectorAll("video"); + for (let element of elements) { + // Look for a video element contained in the overlay bounds + let rect = element.getBoundingClientRect(); + if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) { + // Use the flag set when the <video> binding was created as the check + return element.mozAllowCasting; + } + } + } catch(e) {} + + return false; + }, + + filterCast: { + matches: function(aElement, aX, aY) { + // This behavior matches the pageaction: As long as a video is castable, + // we can cast it, even if it's already being cast to a device. + if (SimpleServiceDiscovery.services.length == 0) + return false; + return CastingApps.isVideoCastable(aElement, aX, aY); + } + }, + + pageAction: { + click: function() { + // Since this is a pageaction, we use the selected browser + let browser = BrowserApp.selectedBrowser; + if (!browser) { + return; + } + + // Look for a castable <video> that is playing, and start casting it + let videos = browser.contentDocument.querySelectorAll("video"); + for (let video of videos) { + if (!video.paused && video.mozAllowCasting) { + UITelemetry.addEvent("cast.1", "pageaction", null); + CastingApps.openExternal(video, 0, 0); + return; + } + } + } + }, + + _findCastableVideo: function _findCastableVideo(aBrowser) { + if (!aBrowser) { + return null; + } + + // Scan for a <video> being actively cast. Also look for a castable <video> + // on the page. + let castableVideo = null; + let videos = aBrowser.contentDocument.querySelectorAll("video"); + for (let video of videos) { + if (video.mozIsCasting) { + // This <video> is cast-active. Break out of loop. + return video; + } + + if (!video.paused && video.mozAllowCasting) { + // This <video> is cast-ready. Keep looking so cast-active could be found. + castableVideo = video; + } + } + + // Could be null + return castableVideo; + }, + + _updatePageActionForTab: function _updatePageActionForTab(aTab, aEvent) { + // We only care about events on the selected tab + if (aTab != BrowserApp.selectedTab) { + return; + } + + // Update the page action, scanning for a castable <video> + this._updatePageAction(); + }, + + _updatePageActionForVideo: function _updatePageActionForVideo(aVideo) { + this._updatePageAction(aVideo); + }, + + _updatePageAction: function _updatePageAction(aVideo) { + // Remove any exising pageaction first, in case state changes or we don't have + // a castable video + if (this.pageAction.id) { + PageActions.remove(this.pageAction.id); + delete this.pageAction.id; + } + + if (!aVideo) { + aVideo = this._findCastableVideo(BrowserApp.selectedBrowser); + if (!aVideo) { + return; + } + } + + // We only show pageactions if the <video> is from the selected tab + if (BrowserApp.selectedTab != BrowserApp.getTabForWindow(aVideo.ownerDocument.defaultView.top)) { + return; + } + + // We check for two state here: + // 1. The video is actively being cast + // 2. The video is allowed to be cast and is currently playing + // Both states have the same action: Show the cast page action + if (aVideo.mozIsCasting) { + this.pageAction.id = PageActions.add({ + title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"), + icon: "drawable://casting_active", + clickCallback: this.pageAction.click, + important: true + }); + } else if (aVideo.mozAllowCasting) { + this.pageAction.id = PageActions.add({ + title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"), + icon: "drawable://casting", + clickCallback: this.pageAction.click, + important: true + }); + } + }, + + prompt: function(aCallback, aFilterFunc) { + let items = []; + let filteredServices = []; + SimpleServiceDiscovery.services.forEach(function(aService) { + let item = { + label: aService.friendlyName, + selected: false + }; + if (!aFilterFunc || aFilterFunc(aService)) { + filteredServices.push(aService); + items.push(item); + } + }); + + if (items.length == 0) { + return; + } + + let prompt = new Prompt({ + title: Strings.browser.GetStringFromName("casting.sendToDevice") + }).setSingleChoiceItems(items).show(function(data) { + let selected = data.button; + let service = selected == -1 ? null : filteredServices[selected]; + if (aCallback) + aCallback(service); + }); + }, + + handleContextMenu: function(aElement, aX, aY) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_cast"); + UITelemetry.addEvent("cast.1", "contextmenu", null); + this.openExternal(aElement, aX, aY); + }, + + openExternal: function(aElement, aX, aY) { + // Start a second screen media service + this.getVideo(aElement, aX, aY, this._openExternal.bind(this)); + }, + + _openExternal: function(aVideo) { + if (!aVideo) { + return; + } + + function filterFunc(aService) { + return this.allowableExtension(aVideo.sourceURI, aService.extensions) || this.allowableMimeType(aVideo.type, aService.types); + } + + this.prompt(function(aService) { + if (!aService) + return; + + // Make sure we have a player app for the given service + let app = SimpleServiceDiscovery.findAppForService(aService); + if (!app) + return; + + if (aVideo.element) { + aVideo.title = aVideo.element.ownerDocument.defaultView.top.document.title; + + // If the video is currently playing on the device, pause it + if (!aVideo.element.paused) { + aVideo.element.pause(); + } + } + + app.stop(function() { + app.start(function(aStarted) { + if (!aStarted) { + dump("CastingApps: Unable to start app"); + return; + } + + app.remoteMedia(function(aRemoteMedia) { + if (!aRemoteMedia) { + dump("CastingApps: Failed to create remotemedia"); + return; + } + + this.session = { + service: aService, + app: app, + remoteMedia: aRemoteMedia, + data: { + title: aVideo.title, + source: aVideo.source, + poster: aVideo.poster + }, + videoRef: Cu.getWeakReference(aVideo.element) + }; + }.bind(this), this); + }.bind(this)); + }.bind(this)); + }.bind(this), filterFunc.bind(this)); + }, + + closeExternal: function() { + if (!this.session) { + return; + } + + this.session.remoteMedia.shutdown(); + this._shutdown(); + }, + + _shutdown: function() { + if (!this.session) { + return; + } + + this.session.app.stop(); + let video = this.session.videoRef.get(); + if (video) { + this._sendEventToVideo(video, { active: false }); + this._updatePageAction(); + } + + delete this.session; + }, + + // RemoteMedia callback API methods + onRemoteMediaStart: function(aRemoteMedia) { + if (!this.session) { + return; + } + + aRemoteMedia.load(this.session.data); + Messaging.sendRequest({ type: "Casting:Started", device: this.session.service.friendlyName }); + + let video = this.session.videoRef.get(); + if (video) { + this._sendEventToVideo(video, { active: true }); + this._updatePageAction(video); + } + }, + + onRemoteMediaStop: function(aRemoteMedia) { + Messaging.sendRequest({ type: "Casting:Stopped" }); + this._shutdown(); + }, + + onRemoteMediaStatus: function(aRemoteMedia) { + if (!this.session) { + return; + } + + let status = aRemoteMedia.status; + switch (status) { + case "started": + Messaging.sendRequest({ type: "Casting:Playing" }); + break; + case "paused": + Messaging.sendRequest({ type: "Casting:Paused" }); + break; + case "completed": + this.closeExternal(); + break; + } + } +}; |