// -*- 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