gecko-dev/mobile/android/modules/WebrtcUI.jsm

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"
);
},
};