Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
"use strict";
6
7
var EXPORTED_SYMBOLS = ["WebRTCChild"];
8
9
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
const { XPCOMUtils } = ChromeUtils.import(
12
);
13
const { AppConstants } = ChromeUtils.import(
15
);
16
XPCOMUtils.defineLazyServiceGetter(
17
this,
18
"MediaManagerService",
19
"@mozilla.org/mediaManagerService;1",
20
"nsIMediaManagerService"
21
);
22
23
const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
24
25
class WebRTCChild extends JSWindowActorChild {
26
// Called only for 'unload' to remove pending gUM prompts in reloaded frames.
27
static handleEvent(aEvent) {
28
let contentWindow = aEvent.target.defaultView;
29
let actor = getActorForWindow(contentWindow);
30
if (actor) {
31
for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
32
actor.sendAsyncMessage("webrtc:CancelRequest", key);
33
}
34
for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
35
actor.sendAsyncMessage("rtcpeer:CancelRequest", key);
36
}
37
}
38
}
39
40
// This observer is registered in ContentObservers.js to avoid
41
// loading this .jsm when WebRTC is not in use.
42
static observe(aSubject, aTopic, aData) {
43
switch (aTopic) {
44
case "getUserMedia:request":
45
handleGUMRequest(aSubject, aTopic, aData);
46
break;
47
case "recording-device-stopped":
48
handleGUMStop(aSubject, aTopic, aData);
49
break;
50
case "PeerConnection:request":
51
handlePCRequest(aSubject, aTopic, aData);
52
break;
53
case "recording-device-events":
54
updateIndicators(aSubject, aTopic, aData);
55
break;
56
case "recording-window-ended":
57
removeBrowserSpecificIndicator(aSubject, aTopic, aData);
58
break;
59
}
60
}
61
62
receiveMessage(aMessage) {
63
switch (aMessage.name) {
64
case "rtcpeer:Allow":
65
case "rtcpeer:Deny": {
66
let callID = aMessage.data.callID;
67
let contentWindow = Services.wm.getOuterWindowWithId(
68
aMessage.data.windowID
69
);
70
forgetPCRequest(contentWindow, callID);
71
let topic =
72
aMessage.name == "rtcpeer:Allow"
73
? "PeerConnection:response:allow"
74
: "PeerConnection:response:deny";
75
Services.obs.notifyObservers(null, topic, callID);
76
break;
77
}
78
case "webrtc:Allow": {
79
let callID = aMessage.data.callID;
80
let contentWindow = Services.wm.getOuterWindowWithId(
81
aMessage.data.windowID
82
);
83
let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
84
forgetGUMRequest(contentWindow, callID);
85
86
let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
87
Ci.nsIMutableArray
88
);
89
for (let deviceIndex of aMessage.data.devices) {
90
allowedDevices.appendElement(devices[deviceIndex]);
91
}
92
93
Services.obs.notifyObservers(
94
allowedDevices,
95
"getUserMedia:response:allow",
96
callID
97
);
98
break;
99
}
100
case "webrtc:Deny":
101
denyGUMRequest(aMessage.data);
102
break;
103
case "webrtc:StopSharing":
104
Services.obs.notifyObservers(
105
null,
106
"getUserMedia:revoke",
107
aMessage.data
108
);
109
break;
110
}
111
}
112
}
113
114
function getActorForWindow(window) {
115
let windowGlobal = window.windowGlobalChild;
116
try {
117
if (windowGlobal) {
118
return windowGlobal.getActor("WebRTC");
119
}
120
} catch (ex) {
121
// There might not be an actor for a parent process chrome URL.
122
}
123
124
return null;
125
}
126
127
function handlePCRequest(aSubject, aTopic, aData) {
128
let { windowID, innerWindowID, callID, isSecure } = aSubject;
129
let contentWindow = Services.wm.getOuterWindowWithId(windowID);
130
131
let mm = getMessageManagerForWindow(contentWindow);
132
if (!mm) {
133
// Workaround for Bug 1207784. To use WebRTC, add-ons right now use
134
// hiddenWindow.mozRTCPeerConnection which is only privileged on OSX. Other
135
// platforms end up here without a message manager.
136
// TODO: Remove once there's a better way (1215591).
137
138
// Skip permission check in the absence of a message manager.
139
Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
140
return;
141
}
142
143
if (!contentWindow.pendingPeerConnectionRequests) {
144
setupPendingListsInitially(contentWindow);
145
}
146
contentWindow.pendingPeerConnectionRequests.add(callID);
147
148
let request = {
149
windowID,
150
innerWindowID,
151
callID,
152
documentURI: contentWindow.document.documentURI,
153
secure: isSecure,
154
};
155
156
let actor = getActorForWindow(contentWindow);
157
if (actor) {
158
actor.sendAsyncMessage("rtcpeer:Request", request);
159
}
160
}
161
162
function handleGUMStop(aSubject, aTopic, aData) {
163
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
164
165
let request = {
166
windowID: aSubject.windowID,
167
rawID: aSubject.rawID,
168
mediaSource: aSubject.mediaSource,
169
};
170
171
let actor = getActorForWindow(contentWindow);
172
if (actor) {
173
actor.sendAsyncMessage("webrtc:StopRecording", request);
174
}
175
}
176
177
function handleGUMRequest(aSubject, aTopic, aData) {
178
let constraints = aSubject.getConstraints();
179
let secure = aSubject.isSecure;
180
let isHandlingUserInput = aSubject.isHandlingUserInput;
181
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
182
183
contentWindow.navigator.mozGetUserMediaDevices(
184
constraints,
185
function(devices) {
186
// If the window has been closed while we were waiting for the list of
187
// devices, there's nothing to do in the callback anymore.
188
if (contentWindow.closed) {
189
return;
190
}
191
192
prompt(
193
contentWindow,
194
aSubject.windowID,
195
aSubject.callID,
196
constraints,
197
devices,
198
secure,
199
isHandlingUserInput
200
);
201
},
202
function(error) {
203
// Device enumeration is done ahead of handleGUMRequest, so we're not
204
// responsible for handling the NotFoundError spec case.
205
denyGUMRequest({ callID: aSubject.callID });
206
},
207
aSubject.innerWindowID,
208
aSubject.callID
209
);
210
}
211
212
function prompt(
213
aContentWindow,
214
aWindowID,
215
aCallID,
216
aConstraints,
217
aDevices,
218
aSecure,
219
aIsHandlingUserInput
220
) {
221
let audioDevices = [];
222
let videoDevices = [];
223
let devices = [];
224
225
// MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
226
let video = aConstraints.video || aConstraints.picture;
227
let audio = aConstraints.audio;
228
let sharingScreen =
229
video && typeof video != "boolean" && video.mediaSource != "camera";
230
let sharingAudio =
231
audio && typeof audio != "boolean" && audio.mediaSource != "microphone";
232
for (let device of aDevices) {
233
device = device.QueryInterface(Ci.nsIMediaDevice);
234
switch (device.type) {
235
case "audioinput":
236
// Check that if we got a microphone, we have not requested an audio
237
// capture, and if we have requested an audio capture, we are not
238
// getting a microphone instead.
239
if (audio && (device.mediaSource == "microphone") != sharingAudio) {
240
audioDevices.push({
241
name: device.name,
242
deviceIndex: devices.length,
243
id: device.rawId,
244
mediaSource: device.mediaSource,
245
});
246
devices.push(device);
247
}
248
break;
249
case "videoinput":
250
// Verify that if we got a camera, we haven't requested a screen share,
251
// or that if we requested a screen share we aren't getting a camera.
252
if (video && (device.mediaSource == "camera") != sharingScreen) {
253
let deviceObject = {
254
name: device.name,
255
deviceIndex: devices.length,
256
id: device.rawId,
257
mediaSource: device.mediaSource,
258
};
259
if (device.scary) {
260
deviceObject.scary = true;
261
}
262
videoDevices.push(deviceObject);
263
devices.push(device);
264
}
265
break;
266
}
267
}
268
269
let requestTypes = [];
270
if (videoDevices.length) {
271
requestTypes.push(sharingScreen ? "Screen" : "Camera");
272
}
273
if (audioDevices.length) {
274
requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
275
}
276
277
if (!requestTypes.length) {
278
// Device enumeration is done ahead of handleGUMRequest, so we're not
279
// responsible for handling the NotFoundError spec case.
280
denyGUMRequest({ callID: aCallID });
281
return;
282
}
283
284
if (!aContentWindow.pendingGetUserMediaRequests) {
285
setupPendingListsInitially(aContentWindow);
286
}
287
aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
288
289
// WebRTC prompts have a bunch of special requirements, such as being able to
290
// grant two permissions (microphone and camera), selecting devices and showing
291
// a screen sharing preview. All this could have probably been baked into
292
// nsIContentPermissionRequest prompts, but the team that implemented this back
293
// then chose to just build their own prompting mechanism instead.
294
//
295
// So, what you are looking at here is not a real nsIContentPermissionRequest, but
296
// something that looks really similar and will be transmitted to webrtcUI.jsm
297
// for showing the prompt.
298
// Note that we basically do the permission delegate check in
299
// nsIContentPermissionRequest, but because webrtc uses their own prompting
300
// system, we should manually apply the delegate policy here. Permission
301
// should be delegated using Feature Policy and top principal
302
const permDelegateHandler = aContentWindow.document.permDelegateHandler.QueryInterface(
303
Ci.nsIPermissionDelegateHandler
304
);
305
306
const shouldDelegatePermission =
307
permDelegateHandler.permissionDelegateFPEnabled;
308
309
let secondOrigin = undefined;
310
if (
311
shouldDelegatePermission &&
312
permDelegateHandler.maybeUnsafePermissionDelegate(requestTypes)
313
) {
314
// We are going to prompt both first party and third party origin.
315
// SecondOrigin should be third party
316
secondOrigin = aContentWindow.document.nodePrincipal.origin;
317
}
318
319
let request = {
320
callID: aCallID,
321
windowID: aWindowID,
322
secondOrigin,
323
documentURI: aContentWindow.document.documentURI,
324
secure: aSecure,
325
isHandlingUserInput: aIsHandlingUserInput,
326
shouldDelegatePermission,
327
requestTypes,
328
sharingScreen,
329
sharingAudio,
330
audioDevices,
331
videoDevices,
332
};
333
334
let actor = getActorForWindow(aContentWindow);
335
if (actor) {
336
actor.sendAsyncMessage("webrtc:Request", request);
337
}
338
}
339
340
function denyGUMRequest(aData) {
341
let subject;
342
if (aData.noOSPermission) {
343
subject = "getUserMedia:response:noOSPermission";
344
} else {
345
subject = "getUserMedia:response:deny";
346
}
347
Services.obs.notifyObservers(null, subject, aData.callID);
348
349
if (!aData.windowID) {
350
return;
351
}
352
let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
353
if (contentWindow.pendingGetUserMediaRequests) {
354
forgetGUMRequest(contentWindow, aData.callID);
355
}
356
}
357
358
function forgetGUMRequest(aContentWindow, aCallID) {
359
aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
360
forgetPendingListsEventually(aContentWindow);
361
}
362
363
function forgetPCRequest(aContentWindow, aCallID) {
364
aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
365
forgetPendingListsEventually(aContentWindow);
366
}
367
368
function setupPendingListsInitially(aContentWindow) {
369
if (aContentWindow.pendingGetUserMediaRequests) {
370
return;
371
}
372
aContentWindow.pendingGetUserMediaRequests = new Map();
373
aContentWindow.pendingPeerConnectionRequests = new Set();
374
aContentWindow.addEventListener("unload", WebRTCChild.handleEvent);
375
}
376
377
function forgetPendingListsEventually(aContentWindow) {
378
if (
379
aContentWindow.pendingGetUserMediaRequests.size ||
380
aContentWindow.pendingPeerConnectionRequests.size
381
) {
382
return;
383
}
384
aContentWindow.pendingGetUserMediaRequests = null;
385
aContentWindow.pendingPeerConnectionRequests = null;
386
aContentWindow.removeEventListener("unload", WebRTCChild.handleEvent);
387
}
388
389
function updateIndicators(aSubject, aTopic, aData) {
390
if (
391
aSubject instanceof Ci.nsIPropertyBag &&
392
aSubject.getProperty("requestURL") == kBrowserURL
393
) {
394
// Ignore notifications caused by the browser UI showing previews.
395
return;
396
}
397
398
let contentWindow = aSubject.getProperty("window");
399
400
let actor = contentWindow ? getActorForWindow(contentWindow) : null;
401
if (actor) {
402
let tabState = getTabStateForContentWindow(contentWindow, false);
403
tabState.windowId = getInnerWindowIDForWindow(contentWindow);
404
405
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
406
}
407
}
408
409
function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
410
let contentWindow = Services.wm.getOuterWindowWithId(aData);
411
if (contentWindow.document.documentURI == kBrowserURL) {
412
// Ignore notifications caused by the browser UI showing previews.
413
return;
414
}
415
416
let tabState = getTabStateForContentWindow(contentWindow, true);
417
418
tabState.windowId = aData;
419
420
let actor = getActorForWindow(contentWindow);
421
if (actor) {
422
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
423
}
424
}
425
426
function getTabStateForContentWindow(aContentWindow, aForRemove = false) {
427
let camera = {},
428
microphone = {},
429
screen = {},
430
window = {},
431
browser = {};
432
MediaManagerService.mediaCaptureWindowState(
433
aContentWindow,
434
camera,
435
microphone,
436
screen,
437
window,
438
browser,
439
false
440
);
441
442
if (
443
camera.value == MediaManagerService.STATE_NOCAPTURE &&
444
microphone.value == MediaManagerService.STATE_NOCAPTURE &&
445
screen.value == MediaManagerService.STATE_NOCAPTURE &&
446
window.value == MediaManagerService.STATE_NOCAPTURE &&
447
browser.value == MediaManagerService.STATE_NOCAPTURE
448
) {
449
return {};
450
}
451
452
if (aForRemove) {
453
return {};
454
}
455
456
return {
457
camera: camera.value,
458
microphone: microphone.value,
459
screen: screen.value,
460
window: window.value,
461
browser: browser.value,
462
documentURI: aContentWindow.document.documentURI,
463
};
464
}
465
466
function getInnerWindowIDForWindow(aContentWindow) {
467
return aContentWindow.windowUtils.currentInnerWindowID;
468
}
469
470
function getMessageManagerForWindow(aContentWindow) {
471
let docShell = aContentWindow.docShell;
472
if (!docShell) {
473
// Closed tab.
474
return null;
475
}
476
477
return docShell.messageManager;
478
}