зеркало из https://github.com/mozilla/gecko-dev.git
446 строки
13 KiB
JavaScript
446 строки
13 KiB
JavaScript
/* 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";
|
|
|
|
var EXPORTED_SYMBOLS = ["WebrtcUI"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
DoorHanger: "resource://gre/modules/Prompt.jsm",
|
|
Notifications: "resource://gre/modules/Notifications.jsm",
|
|
RuntimePermissions: "resource://gre/modules/RuntimePermissions.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"MediaManagerService",
|
|
"@mozilla.org/mediaManagerService;1",
|
|
"nsIMediaManagerService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"ParentalControls",
|
|
"@mozilla.org/parental-controls-service;1",
|
|
"nsIParentalControlsService"
|
|
);
|
|
|
|
var Strings = {};
|
|
|
|
XPCOMUtils.defineLazyGetter(Strings, "brand", _ =>
|
|
Services.strings.createBundle("chrome://branding/locale/brand.properties")
|
|
);
|
|
XPCOMUtils.defineLazyGetter(Strings, "browser", _ =>
|
|
Services.strings.createBundle("chrome://browser/locale/browser.properties")
|
|
);
|
|
|
|
var WebrtcUI = {
|
|
_notificationId: null,
|
|
|
|
// Add-ons can override stock permission behavior by doing:
|
|
//
|
|
// var stockObserve = WebrtcUI.observe;
|
|
//
|
|
// webrtcUI.observe = function(aSubject, aTopic, aData) {
|
|
// switch (aTopic) {
|
|
// case "PeerConnection:request": {
|
|
// // new code.
|
|
// break;
|
|
// ...
|
|
// default:
|
|
// return stockObserve.call(this, aSubject, aTopic, aData);
|
|
//
|
|
// See browser/modules/webrtcUI.jsm for details.
|
|
|
|
observe: function(aSubject, aTopic, aData) {
|
|
if (aTopic === "getUserMedia:ask-device-permission") {
|
|
RuntimePermissions.waitForPermissions(
|
|
this._determineNeededRuntimePermissions(aData)
|
|
).then(_ => {
|
|
Services.obs.notifyObservers(
|
|
aSubject,
|
|
"getUserMedia:got-device-permission"
|
|
);
|
|
});
|
|
} else if (aTopic === "getUserMedia:request") {
|
|
RuntimePermissions.checkPermissions(
|
|
this._determineNeededRuntimePermissions(aSubject)
|
|
).then(permissionGranted => {
|
|
if (permissionGranted) {
|
|
WebrtcUI.handleGumRequest(aSubject, aTopic, aData);
|
|
} else {
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"getUserMedia:response:deny",
|
|
aSubject.callID
|
|
);
|
|
}
|
|
});
|
|
} else if (aTopic === "PeerConnection:request") {
|
|
this.handlePCRequest(aSubject, aTopic, aData);
|
|
} else if (aTopic === "recording-device-events") {
|
|
this.notify();
|
|
} else if (aTopic === "VideoCapture:Paused") {
|
|
if (this._notificationId) {
|
|
Notifications.cancel(this._notificationId);
|
|
this._notificationId = null;
|
|
}
|
|
} else if (aTopic === "VideoCapture:Resumed") {
|
|
this.notify();
|
|
}
|
|
},
|
|
|
|
notify: function() {
|
|
let windows = MediaManagerService.activeMediaCaptureWindows;
|
|
let count = windows.length;
|
|
let msg = {};
|
|
if (count == 0) {
|
|
if (this._notificationId) {
|
|
Notifications.cancel(this._notificationId);
|
|
this._notificationId = null;
|
|
}
|
|
} else {
|
|
let notificationOptions = {
|
|
title: Strings.brand.GetStringFromName("brandShortName"),
|
|
when: null, // hide the date row
|
|
light: [0xff9500ff, 1000, 1000],
|
|
ongoing: true,
|
|
};
|
|
|
|
let cameraActive = false;
|
|
let audioActive = false;
|
|
for (let i = 0; i < count; i++) {
|
|
let win = windows.queryElementAt(i, Ci.nsIDOMWindow);
|
|
let hasCamera = {};
|
|
let hasMicrophone = {};
|
|
MediaManagerService.mediaCaptureWindowState(
|
|
win,
|
|
hasCamera,
|
|
hasMicrophone
|
|
);
|
|
if (hasCamera.value != MediaManagerService.STATE_NOCAPTURE) {
|
|
cameraActive = true;
|
|
}
|
|
if (hasMicrophone.value != MediaManagerService.STATE_NOCAPTURE) {
|
|
audioActive = true;
|
|
}
|
|
}
|
|
|
|
if (cameraActive && audioActive) {
|
|
notificationOptions.message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.sharingCameraAndMicrophone.message2"
|
|
);
|
|
notificationOptions.icon = "drawable:alert_mic_camera";
|
|
} else if (cameraActive) {
|
|
notificationOptions.message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.sharingCamera.message2"
|
|
);
|
|
notificationOptions.icon = "drawable:alert_camera";
|
|
} else if (audioActive) {
|
|
notificationOptions.message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.sharingMicrophone.message2"
|
|
);
|
|
notificationOptions.icon = "drawable:alert_mic";
|
|
} else {
|
|
// somethings wrong. lets throw
|
|
throw new Error("Couldn't find any cameras or microphones being used");
|
|
}
|
|
|
|
if (this._notificationId) {
|
|
Notifications.update(this._notificationId, notificationOptions);
|
|
} else {
|
|
this._notificationId = Notifications.create(notificationOptions);
|
|
}
|
|
if (count > 1) {
|
|
msg.count = count;
|
|
}
|
|
}
|
|
},
|
|
|
|
handlePCRequest: function handlePCRequest(aSubject, aTopic, aData) {
|
|
aSubject = aSubject.wrappedJSObject;
|
|
let { callID } = aSubject;
|
|
// Also available: windowID, isSecure, innerWindowID. For contentWindow do:
|
|
//
|
|
// let contentWindow = Services.wm.getOuterWindowWithId(windowID);
|
|
|
|
Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
|
|
},
|
|
|
|
handleGumRequest: function handleGumRequest(aSubject, aTopic, aData) {
|
|
let constraints = aSubject.getConstraints();
|
|
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
|
|
|
|
contentWindow.navigator.mozGetUserMediaDevices(
|
|
constraints,
|
|
function(devices) {
|
|
if (!ParentalControls.isAllowed(ParentalControls.CAMERA_MICROPHONE)) {
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"getUserMedia:response:deny",
|
|
aSubject.callID
|
|
);
|
|
WebrtcUI.showBlockMessage(contentWindow, devices);
|
|
return;
|
|
}
|
|
|
|
WebrtcUI.prompt(
|
|
contentWindow,
|
|
aSubject.callID,
|
|
constraints.audio,
|
|
constraints.video,
|
|
devices
|
|
);
|
|
},
|
|
function(error) {
|
|
Cu.reportError(error);
|
|
},
|
|
aSubject.innerWindowID,
|
|
aSubject.callID
|
|
);
|
|
},
|
|
|
|
getDeviceButtons: function(audioDevices, videoDevices, aCallID, aPrincipal) {
|
|
return [
|
|
{
|
|
label: Strings.browser.GetStringFromName(
|
|
"getUserMedia.denyRequest.label"
|
|
),
|
|
callback: function() {
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"getUserMedia:response:deny",
|
|
aCallID
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: Strings.browser.GetStringFromName(
|
|
"getUserMedia.shareRequest.label"
|
|
),
|
|
callback: function(checked /* ignored */, inputs) {
|
|
let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
|
|
Ci.nsIMutableArray
|
|
);
|
|
|
|
let audioId = 0;
|
|
if (inputs && inputs.audioDevice != undefined) {
|
|
audioId = inputs.audioDevice;
|
|
}
|
|
if (audioDevices[audioId]) {
|
|
allowedDevices.appendElement(audioDevices[audioId]);
|
|
}
|
|
|
|
let videoId = 0;
|
|
if (inputs && inputs.videoSource != undefined) {
|
|
videoId = inputs.videoSource;
|
|
}
|
|
if (videoDevices[videoId]) {
|
|
allowedDevices.appendElement(videoDevices[videoId]);
|
|
let perms = Services.perms;
|
|
// Although the lifetime is "session" it will be removed upon
|
|
// use so it's more of a one-shot.
|
|
perms.addFromPrincipal(
|
|
aPrincipal,
|
|
"MediaManagerVideo",
|
|
perms.ALLOW_ACTION,
|
|
perms.EXPIRE_SESSION
|
|
);
|
|
}
|
|
|
|
Services.obs.notifyObservers(
|
|
allowedDevices,
|
|
"getUserMedia:response:allow",
|
|
aCallID
|
|
);
|
|
},
|
|
positive: true,
|
|
},
|
|
];
|
|
},
|
|
|
|
_determineNeededRuntimePermissions: function(aSubject) {
|
|
let permissions = [];
|
|
|
|
let constraints;
|
|
if (typeof aSubject === "string") {
|
|
constraints = {
|
|
video: aSubject === "video" || aSubject === "all",
|
|
audio: aSubject === "audio" || aSubject === "all",
|
|
};
|
|
} else {
|
|
constraints = aSubject.getConstraints();
|
|
}
|
|
|
|
if (constraints.video) {
|
|
permissions.push(RuntimePermissions.CAMERA);
|
|
}
|
|
if (constraints.audio) {
|
|
permissions.push(RuntimePermissions.RECORD_AUDIO);
|
|
}
|
|
|
|
return permissions;
|
|
},
|
|
|
|
// Get a list of string names for devices. Ensures that none of the strings are blank
|
|
_getList: function(aDevices, aType) {
|
|
let defaultCount = 0;
|
|
return aDevices.map(function(device) {
|
|
// if this is a Camera input, convert the name to something readable
|
|
let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
|
|
if (res) {
|
|
return Strings.browser.GetStringFromName(
|
|
"getUserMedia." + aType + "." + res[1] + "Camera"
|
|
);
|
|
}
|
|
|
|
if (device.name.startsWith("&") && device.name.endsWith(";")) {
|
|
return Strings.browser.GetStringFromName(
|
|
device.name.substring(1, device.name.length - 1)
|
|
);
|
|
}
|
|
|
|
if (device.name.trim() == "") {
|
|
defaultCount++;
|
|
return Strings.browser.formatStringFromName(
|
|
"getUserMedia." + aType + ".default",
|
|
[defaultCount]
|
|
);
|
|
}
|
|
return device.name;
|
|
}, this);
|
|
},
|
|
|
|
_addDevicesToOptions: function(aDevices, aType, aOptions) {
|
|
if (aDevices.length) {
|
|
// Filter out empty items from the list
|
|
let list = this._getList(aDevices, aType);
|
|
|
|
if (list.length > 0) {
|
|
aOptions.inputs.push({
|
|
id: aType,
|
|
type: "menulist",
|
|
label: Strings.browser.GetStringFromName(
|
|
"getUserMedia." + aType + ".prompt"
|
|
),
|
|
values: list,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
showBlockMessage: function(aWindow, aDevices) {
|
|
let microphone = false;
|
|
let camera = false;
|
|
|
|
for (let device of aDevices) {
|
|
device = device.QueryInterface(Ci.nsIMediaDevice);
|
|
if (device.type == "audioinput") {
|
|
microphone = true;
|
|
} else if (device.type == "videoinput") {
|
|
camera = true;
|
|
}
|
|
}
|
|
|
|
let message;
|
|
if (microphone && !camera) {
|
|
message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.blockedMicrophoneAccess"
|
|
);
|
|
} else if (camera && !microphone) {
|
|
message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.blockedCameraAccess"
|
|
);
|
|
} else {
|
|
message = Strings.browser.GetStringFromName(
|
|
"getUserMedia.blockedCameraAndMicrophoneAccess"
|
|
);
|
|
}
|
|
|
|
DoorHanger.show(aWindow, message, "webrtc-blocked");
|
|
},
|
|
|
|
getChromeWindow: function getChromeWindow(aWindow) {
|
|
return aWindow.docShell.rootTreeItem.domWindow;
|
|
},
|
|
|
|
prompt: function prompt(
|
|
aContentWindow,
|
|
aCallID,
|
|
aAudioRequested,
|
|
aVideoRequested,
|
|
aDevices
|
|
) {
|
|
let audioDevices = [];
|
|
let videoDevices = [];
|
|
for (let device of aDevices) {
|
|
device = device.QueryInterface(Ci.nsIMediaDevice);
|
|
switch (device.type) {
|
|
case "audioinput":
|
|
if (aAudioRequested) {
|
|
audioDevices.push(device);
|
|
}
|
|
break;
|
|
case "videoinput":
|
|
if (aVideoRequested) {
|
|
videoDevices.push(device);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
let requestType;
|
|
if (audioDevices.length && videoDevices.length) {
|
|
requestType = "CameraAndMicrophone";
|
|
} else if (audioDevices.length) {
|
|
requestType = "Microphone";
|
|
} else if (videoDevices.length) {
|
|
requestType = "Camera";
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
let chromeWin = this.getChromeWindow(aContentWindow);
|
|
let principal = aContentWindow.document.nodePrincipal;
|
|
let host = principal.URI.host;
|
|
let requestor =
|
|
chromeWin.BrowserApp && chromeWin.BrowserApp.manifest
|
|
? "'" + chromeWin.BrowserApp.manifest.name + "'"
|
|
: host;
|
|
let message = Strings.browser.formatStringFromName(
|
|
"getUserMedia.share" + requestType + ".message",
|
|
[requestor]
|
|
);
|
|
|
|
let options = { inputs: [] };
|
|
if (videoDevices.length > 1 || audioDevices.length > 0) {
|
|
// videoSource is both the string used for l10n lookup and the object that will be returned
|
|
this._addDevicesToOptions(videoDevices, "videoSource", options);
|
|
}
|
|
|
|
if (audioDevices.length > 1 || videoDevices.length > 0) {
|
|
this._addDevicesToOptions(audioDevices, "audioDevice", options);
|
|
}
|
|
|
|
let buttons = this.getDeviceButtons(
|
|
audioDevices,
|
|
videoDevices,
|
|
aCallID,
|
|
principal
|
|
);
|
|
|
|
DoorHanger.show(
|
|
aContentWindow,
|
|
message,
|
|
"webrtc-request",
|
|
buttons,
|
|
options,
|
|
"WEBRTC"
|
|
);
|
|
},
|
|
};
|