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