mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-07-15 03:29:01 +00:00
780 lines
26 KiB
JavaScript
780 lines
26 KiB
JavaScript
|
// -*- 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 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);
|
||
|
|
||
|
// 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 {
|
||
|
channel = Services.io.newChannelFromURI2(aURI,
|
||
|
aElement,
|
||
|
null, // aLoadingPrincipal
|
||
|
null, // aTriggeringPrincipal
|
||
|
Ci.nsILoadInfo.SEC_NORMAL,
|
||
|
Ci.nsIContentPolicy.TYPE_OTHER);
|
||
|
} 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.asyncOpen(listener, null);
|
||
|
} 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;
|
||
|
}
|
||
|
}
|
||
|
};
|