mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-14 14:02:47 +00:00
bug 1054959 - Add 'Send Video To Device' to the context menu for sending videos from desktop to a second screen r=gavin, ui-r=madhava
* * * bug 1054959 - follow up to fix context menut test r=orange CLOSED TREE
This commit is contained in:
parent
9388b058dc
commit
7ff01ce229
@ -240,6 +240,11 @@
|
|||||||
label="&emailVideoCmd.label;"
|
label="&emailVideoCmd.label;"
|
||||||
accesskey="&emailVideoCmd.accesskey;"
|
accesskey="&emailVideoCmd.accesskey;"
|
||||||
oncommand="gContextMenu.sendMedia();"/>
|
oncommand="gContextMenu.sendMedia();"/>
|
||||||
|
<menu id="context-castvideo"
|
||||||
|
label="&castVideoCmd.label;"
|
||||||
|
accesskey="&castVideoCmd.accesskey;">
|
||||||
|
<menupopup id="context-castvideo-popup" onpopupshowing="gContextMenu.populateCastVideoMenu(this)"/>
|
||||||
|
</menu>
|
||||||
<menuitem id="context-sendaudio"
|
<menuitem id="context-sendaudio"
|
||||||
label="&emailAudioCmd.label;"
|
label="&emailAudioCmd.label;"
|
||||||
accesskey="&emailAudioCmd.accesskey;"
|
accesskey="&emailAudioCmd.accesskey;"
|
||||||
|
@ -188,6 +188,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "FormValidationHandler",
|
|||||||
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
|
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
|
||||||
"resource:///modules/UITour.jsm");
|
"resource:///modules/UITour.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "CastingApps",
|
||||||
|
"resource:///modules/CastingApps.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
|
||||||
|
"resource://gre/modules/SimpleServiceDiscovery.jsm");
|
||||||
|
|
||||||
let gInitialPages = [
|
let gInitialPages = [
|
||||||
"about:blank",
|
"about:blank",
|
||||||
"about:newtab",
|
"about:newtab",
|
||||||
|
@ -208,9 +208,19 @@ nsContextMenu.prototype = {
|
|||||||
// Send media URL (but not for canvas, since it's a big data: URL)
|
// Send media URL (but not for canvas, since it's a big data: URL)
|
||||||
this.showItem("context-sendimage", this.onImage);
|
this.showItem("context-sendimage", this.onImage);
|
||||||
this.showItem("context-sendvideo", this.onVideo);
|
this.showItem("context-sendvideo", this.onVideo);
|
||||||
|
this.showItem("context-castvideo", this.onVideo);
|
||||||
this.showItem("context-sendaudio", this.onAudio);
|
this.showItem("context-sendaudio", this.onAudio);
|
||||||
this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
|
this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
|
||||||
this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
|
this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
|
||||||
|
// getServicesForVideo alone would be sufficient here (it depends on
|
||||||
|
// SimpleServiceDiscovery.services), but SimpleServiceDiscovery is garanteed
|
||||||
|
// to be already loaded, since we load it on startup, and CastingApps isn't,
|
||||||
|
// so check SimpleServiceDiscovery.services first to avoid needing to load
|
||||||
|
// CastingApps.jsm if we don't need to.
|
||||||
|
let shouldShowCast = this.mediaURL &&
|
||||||
|
SimpleServiceDiscovery.services.length > 0 &&
|
||||||
|
CastingApps.getServicesForVideo(this.target).length > 0;
|
||||||
|
this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
|
||||||
},
|
},
|
||||||
|
|
||||||
initViewItems: function CM_initViewItems() {
|
initViewItems: function CM_initViewItems() {
|
||||||
@ -1316,6 +1326,25 @@ nsContextMenu.prototype = {
|
|||||||
MailIntegration.sendMessage(this.mediaURL, "");
|
MailIntegration.sendMessage(this.mediaURL, "");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
castVideo: function() {
|
||||||
|
CastingApps.openExternal(this.target, window);
|
||||||
|
},
|
||||||
|
|
||||||
|
populateCastVideoMenu: function(popup) {
|
||||||
|
let videoEl = this.target;
|
||||||
|
popup.innerHTML = null;
|
||||||
|
let doc = popup.ownerDocument;
|
||||||
|
let services = CastingApps.getServicesForVideo(videoEl);
|
||||||
|
services.forEach(service => {
|
||||||
|
let item = doc.createElement("menuitem");
|
||||||
|
item.setAttribute("label", service.friendlyName);
|
||||||
|
item.addEventListener("command", event => {
|
||||||
|
CastingApps.sendVideoToService(videoEl, service);
|
||||||
|
});
|
||||||
|
popup.appendChild(item);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
playPlugin: function() {
|
playPlugin: function() {
|
||||||
gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
|
gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
|
||||||
},
|
},
|
||||||
|
@ -174,7 +174,8 @@ function runTest(testNum) {
|
|||||||
"---", null,
|
"---", null,
|
||||||
"context-savevideo", true,
|
"context-savevideo", true,
|
||||||
"context-video-saveimage", true,
|
"context-video-saveimage", true,
|
||||||
"context-sendvideo", true
|
"context-sendvideo", true,
|
||||||
|
"context-castvideo", false
|
||||||
].concat(inspectItems));
|
].concat(inspectItems));
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
openContextMenuFor(audio_in_video); // Invoke context menu for next test.
|
openContextMenuFor(audio_in_video); // Invoke context menu for next test.
|
||||||
|
@ -99,6 +99,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
|||||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
|
||||||
"resource://gre/modules/LoginManagerParent.jsm");
|
"resource://gre/modules/LoginManagerParent.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
|
||||||
|
"resource://gre/modules/SimpleServiceDiscovery.jsm");
|
||||||
|
|
||||||
#ifdef NIGHTLY_BUILD
|
#ifdef NIGHTLY_BUILD
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteUX",
|
XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteUX",
|
||||||
"resource:///modules/SignInToWebsite.jsm");
|
"resource:///modules/SignInToWebsite.jsm");
|
||||||
@ -747,8 +750,30 @@ BrowserGlue.prototype = {
|
|||||||
FormValidationHandler.uninit();
|
FormValidationHandler.uninit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_initServiceDiscovery: function () {
|
||||||
|
var rokuDevice = {
|
||||||
|
id: "roku:ecp",
|
||||||
|
target: "roku:ecp",
|
||||||
|
factory: function(aService) {
|
||||||
|
Cu.import("resource://gre/modules/RokuApp.jsm");
|
||||||
|
return new RokuApp(aService);
|
||||||
|
},
|
||||||
|
mirror: false,
|
||||||
|
types: ["video/mp4"],
|
||||||
|
extensions: ["mp4"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register targets
|
||||||
|
SimpleServiceDiscovery.registerDevice(rokuDevice);
|
||||||
|
|
||||||
|
// Search for devices continuously every 120 seconds
|
||||||
|
SimpleServiceDiscovery.search(120 * 1000);
|
||||||
|
},
|
||||||
|
|
||||||
// All initial windows have opened.
|
// All initial windows have opened.
|
||||||
_onWindowsRestored: function BG__onWindowsRestored() {
|
_onWindowsRestored: function BG__onWindowsRestored() {
|
||||||
|
this._initServiceDiscovery();
|
||||||
|
|
||||||
// Show update notification, if needed.
|
// Show update notification, if needed.
|
||||||
if (Services.prefs.prefHasUserValue("app.update.postupdate"))
|
if (Services.prefs.prefHasUserValue("app.update.postupdate"))
|
||||||
this._showUpdateNotification();
|
this._showUpdateNotification();
|
||||||
|
@ -484,6 +484,8 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
|||||||
<!ENTITY emailImageCmd.accesskey "g">
|
<!ENTITY emailImageCmd.accesskey "g">
|
||||||
<!ENTITY emailVideoCmd.label "Email Video…">
|
<!ENTITY emailVideoCmd.label "Email Video…">
|
||||||
<!ENTITY emailVideoCmd.accesskey "a">
|
<!ENTITY emailVideoCmd.accesskey "a">
|
||||||
|
<!ENTITY castVideoCmd.label "Send Video To Device">
|
||||||
|
<!ENTITY castVideoCmd.accesskey "d">
|
||||||
<!ENTITY emailAudioCmd.label "Email Audio…">
|
<!ENTITY emailAudioCmd.label "Email Audio…">
|
||||||
<!ENTITY emailAudioCmd.accesskey "a">
|
<!ENTITY emailAudioCmd.accesskey "a">
|
||||||
<!ENTITY playPluginCmd.label "Activate this plugin">
|
<!ENTITY playPluginCmd.label "Activate this plugin">
|
||||||
|
160
browser/modules/CastingApps.jsm
Normal file
160
browser/modules/CastingApps.jsm
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
// -*- 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";
|
||||||
|
this.EXPORTED_SYMBOLS = ["CastingApps"];
|
||||||
|
|
||||||
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
|
||||||
|
|
||||||
|
|
||||||
|
var CastingApps = {
|
||||||
|
_sendEventToVideo: function (element, data) {
|
||||||
|
let event = element.ownerDocument.createEvent("CustomEvent");
|
||||||
|
event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(data));
|
||||||
|
element.dispatchEvent(event);
|
||||||
|
},
|
||||||
|
|
||||||
|
makeURI: function (url, charset, baseURI) {
|
||||||
|
return Services.io.newURI(url, charset, baseURI);
|
||||||
|
},
|
||||||
|
|
||||||
|
getVideo: function (element) {
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extensions = SimpleServiceDiscovery.getSupportedExtensions();
|
||||||
|
let types = SimpleServiceDiscovery.getSupportedMimeTypes();
|
||||||
|
|
||||||
|
// Grab the poster attribute from the <video>
|
||||||
|
let posterURL = element.poster;
|
||||||
|
|
||||||
|
// First, look to see if the <video> has a src attribute
|
||||||
|
let sourceURL = element.src;
|
||||||
|
|
||||||
|
// If empty, try the currentSrc
|
||||||
|
if (!sourceURL) {
|
||||||
|
sourceURL = element.currentSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceURL) {
|
||||||
|
// Use the file extension to guess the mime type
|
||||||
|
let sourceURI = this.makeURI(sourceURL, null, this.makeURI(element.baseURI));
|
||||||
|
if (this.allowableExtension(sourceURI, extensions)) {
|
||||||
|
return { element: element, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, look to see if there is a <source> child element that meets
|
||||||
|
// our needs
|
||||||
|
let sourceNodes = element.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.allowableMimeType(sourceNode.type, types) || this.allowableExtension(sourceURI, extensions)) {
|
||||||
|
return { element: element, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
sendVideoToService: function (videoElement, service) {
|
||||||
|
if (!service)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let video = this.getVideo(videoElement);
|
||||||
|
if (!video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a player app for the given service
|
||||||
|
let app = SimpleServiceDiscovery.findAppForService(service);
|
||||||
|
if (!app)
|
||||||
|
return;
|
||||||
|
|
||||||
|
video.title = videoElement.ownerDocument.defaultView.top.document.title;
|
||||||
|
if (video.element) {
|
||||||
|
// If the video is currently playing on the device, pause it
|
||||||
|
if (!video.element.paused) {
|
||||||
|
video.element.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.stop(() => {
|
||||||
|
app.start(started => {
|
||||||
|
if (!started) {
|
||||||
|
Cu.reportError("CastingApps: Unable to start app");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.remoteMedia(remoteMedia => {
|
||||||
|
if (!remoteMedia) {
|
||||||
|
Cu.reportError("CastingApps: Failed to create remotemedia");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = {
|
||||||
|
service: service,
|
||||||
|
app: app,
|
||||||
|
remoteMedia: remoteMedia,
|
||||||
|
data: {
|
||||||
|
title: video.title,
|
||||||
|
source: video.source,
|
||||||
|
poster: video.poster
|
||||||
|
},
|
||||||
|
videoRef: Cu.getWeakReference(video.element)
|
||||||
|
};
|
||||||
|
}, this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServicesForVideo: function (videoElement) {
|
||||||
|
let video = this.getVideo(videoElement);
|
||||||
|
if (!video) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let filteredServices = SimpleServiceDiscovery.services.filter(service => {
|
||||||
|
return this.allowableExtension(video.sourceURI, service.extensions) ||
|
||||||
|
this.allowableMimeType(video.type, service.types);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredServices;
|
||||||
|
},
|
||||||
|
|
||||||
|
// RemoteMedia callback API methods
|
||||||
|
onRemoteMediaStart: function (remoteMedia) {
|
||||||
|
if (!this.session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteMedia.load(this.session.data);
|
||||||
|
|
||||||
|
let video = this.session.videoRef.get();
|
||||||
|
if (video) {
|
||||||
|
this._sendEventToVideo(video, { active: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemoteMediaStop: function (remoteMedia) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemoteMediaStatus: function (remoteMedia) {
|
||||||
|
},
|
||||||
|
|
||||||
|
allowableExtension: function (uri, extensions) {
|
||||||
|
return (uri instanceof Ci.nsIURL) && extensions.indexOf(uri.fileExtension) != -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
allowableMimeType: function (type, types) {
|
||||||
|
return types.indexOf(type) != -1;
|
||||||
|
}
|
||||||
|
};
|
@ -14,6 +14,7 @@ XPCSHELL_TESTS_MANIFESTS += [
|
|||||||
EXTRA_JS_MODULES += [
|
EXTRA_JS_MODULES += [
|
||||||
'BrowserNewTabPreloader.jsm',
|
'BrowserNewTabPreloader.jsm',
|
||||||
'BrowserUITelemetry.jsm',
|
'BrowserUITelemetry.jsm',
|
||||||
|
'CastingApps.jsm',
|
||||||
'Chat.jsm',
|
'Chat.jsm',
|
||||||
'ContentClick.jsm',
|
'ContentClick.jsm',
|
||||||
'ContentLinkHandler.jsm',
|
'ContentLinkHandler.jsm',
|
||||||
|
@ -135,7 +135,9 @@ var SimpleServiceDiscovery = {
|
|||||||
|
|
||||||
_usingLAN: function() {
|
_usingLAN: function() {
|
||||||
let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
|
let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
|
||||||
return (network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
|
return (network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI ||
|
||||||
|
network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET ||
|
||||||
|
network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN);
|
||||||
},
|
},
|
||||||
|
|
||||||
_search: function _search() {
|
_search: function _search() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user