-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathindex.html
533 lines (533 loc) · 26.4 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<link href="style.css" rel="stylesheet">
<title>Audio Output Devices API</title>
<script class="remove" src=
"https://www.w3.org/Tools/respec/respec-w3c">
</script>
<script class="remove" src="script.js">
</script>
</head>
<body>
<section id="abstract">
<p>This document defines a set of JavaScript APIs that let a Web
application manage how audio is rendered on the user audio output
devices.</p>
</section>
<section id="sotd">
<p>The WebRTC and Device and Sensors Working Group intend to publish this
specification as a Candidate Recommendation soon. Consequently, this is a
Request for wide review of this document.</p>
</section>
<section class="informative" id="intro">
<h2>Introduction</h2>
<p>This proposal allows JavaScript to direct the audio output of a media
element to permitted devices other than the system or user agent default.
This can be helpful in a variety of real-time communication scenarios as
well as general media applications. For example, an application can use
this API to programmatically direct output to a device such as a Bluetooth
headset or speakerphone.</p>
</section>
<section id="htmlmediaelement-extensions">
<h2>{{HTMLMediaElement}} Extensions</h2>
<p>This section specifies additions to the {{HTMLMediaElement}} [[HTML]] when the Audio Output Devices API is
supported.</p>
<p>When the {{HTMLMediaElement}} constructor is invoked, the user
agent MUST add the following initializing step:</p>
<ol>
<li>
<p>Let the element have a <dfn data-dfn-for="HTMLMediaElement">[[\SinkId]]</dfn> internal slot,
initialized to <code>""</code>.
</li>
</ol>
<div>
<pre class="idl">
partial interface HTMLMediaElement {
[SecureContext] readonly attribute DOMString sinkId;
[SecureContext] Promise<undefined> setSinkId (DOMString sinkId);
};
</pre>
<section>
<h2>Attributes</h2>
<dl data-link-for="HTMLMediaElement" data-dfn-for="HTMLMediaElement"
class="attributes">
<dt><dfn>sinkId</dfn> of type {{DOMString}}, readonly</dt>
<dd>
<p>This attribute contains the ID of the audio device through which
output is being delivered, or the empty string if output is
delivered through the user-agent default device. If nonempty, this
ID should be equal to the {{MediaDeviceInfo/deviceId}}
attribute of one of the {{MediaDeviceInfo}} values returned from
{{MediaDevices/enumerateDevices()}}.</p>
<p>On getting, the
attribute MUST return the value of the {{HTMLMediaElement/[[SinkId]]}} slot.</p>
</dd>
</dl>
</section>
<section>
<h2>Methods</h2>
<dl data-link-for="HTMLMediaElement" data-dfn-for="HTMLMediaElement"
class="methods">
<dt><dfn>setSinkId</dfn></dt>
<dd>
<p>Sets the ID of the audio device through which audio output should
be rendered if the application is <a href=
"#privacy-considerations">permitted</a> to play out of a given
device.</p>
<p>When this method is invoked, the user agent must run the following
steps:</p>
<ol class="method-algorithm">
<li>
<p>Let <var>document</var> be the
<a data-cite="!HTML/webappapis.html#current-settings-object">
current settings object</a>'s
<a data-cite="!HTML/webappapis.html#concept-relevant-global">
relevant global object</a>'s
<a data-cite="!HTML/window-object.html#concept-document-window">
associated <code>Document</code></a>.</p>
</li>
<li>
<p>If <var>document</var> is not
<a data-cite="!HTML/iframe-embed-object.html#allowed-to-use">
allowed to use</a> the feature identified by
<a data-dfn-link-for="">"speaker-selection"</a>, return a
promise rejected with a new {{DOMException}}
whose name is {{NotAllowedError}}.
</p>
</li>
<li>
<p>Let <var>element</var> be the {{HTMLMediaElement}}
object on which this method was invoked.</p>
</li>
<li>
<p>Let <var>sinkId</var> be the method's first argument.</p>
</li>
<li>
<p>If <var>sinkId</var> is equal to <var>element</var>'s
{{HTMLMediaElement/[[SinkId]]}},
return a promise resolved with <code>undefined</code>.</p>
</li>
<li>
<p>Let <var>p</var> be a new promise.</p>
</li>
<li>
<p>Run the following substeps in parallel:</p>
<ol>
<li>
<p>If <var>sinkId</var> is not the empty string and does not
match any audio output device identified by the result that
would be provided by {{MediaDevices/enumerateDevices()}},
reject <var>p</var> with a new
{{DOMException}} whose name is
{{NotFoundError}} and abort these substeps.</p>
</li>
<li>
<p>If <var>sinkId</var> is not the empty string, and the
application would not be permitted to play audio through
the device identified by <var>sinkId</var> if it weren't the
current user agent default device, reject <var>p</var>
with a new {{DOMException}} whose name is
{{NotAllowedError}} and abort these substeps.</p>
</li>
<li>
<p>Switch the underlying audio output device for <var>element</var>
to the audio device identified by <var>sinkId</var>.</p>
<p class="note">If this substep is successful and the media
element's {{HTMLMediaElement/paused}} attribute is false, audio MUST stop playing
out of the device represented by the element's {{HTMLMediaElement/sinkId}} attribute and will start
playing out of the device identified by <var>sinkId</var></p>
</li>
<li>
<p>If the preceding substep failed, reject <var>p</var>
with a new {{DOMException}} whose name is
{{AbortError}},
and abort these substeps.</p>
</li>
<li>
<p>Queue a task that runs the following steps:</p>
<ol>
<li>
<p>Set <var>element</var>'s {{HTMLMediaElement/[[SinkId]]}} to
<var>sinkId</var>.</p>
</li>
<li>
<p>Resolve <var>p</var>.</p>
</li>
</ol>
</li>
</ol>
</li>
<li>
<p>Return <var>p</var>.</p>
</li>
</ol>
</dd>
</dl>
</section>
</div>
<section id="algorithms">
<h3>Algorithms</h3>
<section id="algorithms-sink-unavailable">
<h4>Sink no longer available</h4>
<p>The audio device identified by a media element's {{HTMLMediaElement/sinkId}} attribute may become
<dfn>unavailable</dfn>, for example if it is unplugged.</p>
<p>When the audio device identified by the {{HTMLMediaElement/sinkId}}
attribute is no longer
available, the user agent must take no action. For example, if the
media element's {{HTMLMediaElement/paused}}
attribute is false when the device identified by the
sinkId is no longer available, then playback will continue as normal.
In this case, audio will not be rendered because the device to which
the media element is attached is unavailable.</p>
<p><em>The following paragraph is non-normative.</em></p>
<p class="informative">If the application wishes to react to the device
change, the application can listen to the <dfn data-cite="GETUSERMEDIA#event-mediadevices-devicechange">
<code>devicechange</code></dfn> event and query
{{MediaDevices/enumerateDevices()}} for the list of updated devices. If
the value of the media element's {{HTMLMediaElement/sinkId}} attribute is no longer
present as the {{MediaDeviceInfo/deviceId}}
attribute in the returned list of {{MediaDeviceInfo}}s, the device is no longer available and the
application can choose to react accordingly.</p>
</section>
<section id="algorithms-new-sink-available">
<h4>New sink available</h4>
<p>New audio devices may become available to the user agent, or an
audio device (identified by a media element's {{HTMLMediaElement/sinkId}} attribute) that had
previously become [= unavailable =] may become available
again, for example, if it is unplugged and later plugged back in.</p>
<p>In this scenario, the user agent must run the following steps:</p>
<ol class="method-algorithm">
<li>
<p>Let <var>sinkId</var> be the identifier for the newly available
device.</p>
</li>
<li>
<p>For each media element whose {{HTMLMediaElement/sinkId}} attribute is equal to
<var>sinkId</var>:</p>
<ol>
<li>
<p>If the media element's
{{HTMLMediaElement/paused}} attribute is false, start rendering
this object's audio out of the device represented by the
{{HTMLMediaElement/sinkId}}
attribute.</p>
</li>
</ol>
</li>
</ol>
<p><em>The following paragraph is non-normative.</em></p>
<p class="informative">If the application wishes to react to the device
change, the application can listen to the <a>
<code>devicechange</code></a> event and query
{{MediaDevices/enumerateDevices()}} for the list of updated
devices.</p>
</section>
</section>
</section>
<section id="mediadevices-extensions">
<h2>{{MediaDevices}} Extensions</h2>
<p>This section specifies additions to the {{MediaDevices}}
when the Audio Output Devices API is
supported.</p>
<div>
<pre class="idl">
partial interface MediaDevices {
Promise<MediaDeviceInfo> selectAudioOutput(optional AudioOutputOptions options = {});
};
</pre>
<section>
<h2>Methods</h2>
<!-- FIXME: Update section to use links to promise resolve/reject spec, permission spec... -->
<dl data-link-for="MediaDevices" data-dfn-for="MediaDevices"
class="method">
<dt><dfn>selectAudioOutput</dfn></dt>
<dd>
<p>Prompts the user to select a specific audio output device.</p>
<p>When the {{selectAudioOutput}} method is called,
the [=user agent=] MUST run the following steps:</p>
<ol>
<li><p>If the [=relevant global object=] of [=this=] does not have
[=transient activation=], return a promise <a>rejected</a> with
a {{DOMException}} object whose {{DOMException/name}} attribute
has the value {{InvalidStateError}}.</p></li>
<li><p>Let <var>options</var> be the method's first argument.</p></li>
<li><p>Let <var>deviceId</var> be <var>options</var><code>.deviceId</code>.</p></li>
<li><p>Let <var>p</var> be a new promise.</p></li>
<li>
<p>Run the following steps in parallel:</p>
<ol>
<li><p>Let <var>descriptor</var> be a {{PermissionDescriptor}} with its
[=powerful feature/name=] set to "speaker-selection"</p>
</li>
<li>
<p>If <var>descriptor</var>'s [=permission state=] is
{{PermissionState/"denied"}}, reject
<var>p</var> with a new {{DOMException}} whose
{{DOMException/name}} attribute has the value
{{NotAllowedError}}, and abort these steps.</p>
</li>
<li><p>Probe the [=user agent=] for available audio output devices.</p></li>
<li>
<p>If there is no audio output device, reject <var>p</var>
with a new {{DOMException}} whose {{DOMException/name}}
attribute has the value {{NotFoundError}} and abort
these steps.</p>
</li>
<li>
<p>If <var>deviceId</var> is not <code>""</code>
run the following sub steps:</p>
<ol>
<li>
<p class="fingerprint">If <var>deviceId</var> matches a
a device id previously exposed by
{{MediaDevices/selectAudioOutput}} in this or an earlier browsing
session, or matches a device id of an audio output device
with the same groupId as an audio input device previously
exposed by {{MediaDevices/getUserMedia()}} in this or an earlier browsing
session, the user agent MAY decide, based on its previous
decision of whether to persist this id or not for this set
of origins, to run the following sub steps:</p>
<ol>
<li>
<p>Let <var>device</var> be the device identified by
<var>deviceId</var>, if available.</p>
</li>
<li><p>If <var>device</var> is available, resolve
<var>p</var> with either <var>deviceId</var> or a freshly
rotated device id for <var>device</var>, and abort the
in-parallel steps.</p></li>
</ol>
</li>
</ol>
</li>
<li><p>[=Prompt the user to choose=] an audio output device, with
<var>descriptor</var>.</p></li>
<li><p>If the result of the request is {{PermissionState/"denied"}}, reject
<var>p</var> with a new {{DOMException}} whose {{DOMException/name}} attribute
has the value {{NotAllowedError}} and abort these steps.</p></li>
<li>
<p>Let <var>selectedDevice</var> be the user-selected audio output device.</p>
</li>
<li><p>Let <var>deviceInfo</var> be the result of
[=creating a device info object=] to represent <var>selectedDevice</var>,
with <var>mediaDevices</var>.</p></li>
<li><p>Add <var>deviceInfo</var>.{{MediaDeviceInfo/deviceId}}
to <a data-link-for="relevant global object">[[\explicitlyGrantedAudioOutputDevices]]</a>.</p></li>
<li><p>Resolve <var>p</var> with <var>deviceInfo</var>.</p></li>
</ol>
</li>
<li><p>Return <var>p</var>.</p></li>
</ol>
<p>Once a device is exposed after a call to {{MediaDevices/selectAudioOutput}}, it MUST be listed by
{{MediaDevices/enumerateDevices()}} for the current browsing context.</p>
<p>If the promise returned by {{MediaDevices/selectAudioOutput}} is resolved,
then the user agent MUST ensure the document is both immediately
<a data-cite="!HTML/#allowed-to-play">allowed to play</a> media in an
{{HTMLMediaElement}}, and immediately
<a data-cite="!WEBAUDIO/#allowed-to-start">allowed to start</a> an
{{AudioContext}}, without needing any additional user gesture.</p>
<div class="note">
<p>This is imprecise due to the current lack of standardization of
autoplay in browsers.</p>
</div>
</dd>
</dl>
</section>
<section>
<h4>AudioOutputOptions dictionary</h4>
<p>This dictionary describes the options that can be used to obtain
access to an audio output device.</p>
<div>
<pre class="idl"
>dictionary AudioOutputOptions {
DOMString deviceId = "";
};</pre>
<section>
<h2>Dictionary <dfn>AudioOutputOptions</dfn> Members</h2>
<dl data-link-for="AudioOutputOptions" data-dfn-for="AudioOutputOptions"
class="dictionary-members">
<dt><dfn data-idl>deviceId</dfn> of type {{DOMString}}, defaulting to
<code>""</code></dt>
<dd>
<p class="fingerprint">When the value of this dictionary member
is not <code>""</code>, and matches the id previously exposed by
{{MediaDevices/selectAudioOutput}} or
a device id of an audio output device with the same groupId as an
audio input device previously exposed by
{{MediaDevices/getUserMedia()}} in this or an earlier session, the user
agent MAY opt to skip prompting the user in favor of resolving
with this id or a new rotated id for the same device, assuming
that device is currently available.</p>
<p class="note">Applications that wish to rely on user agents
supporting persisted device ids must pass these through
{{MediaDevices/selectAudioOutput}} successfully before they will
work with {{HTMLMediaElement/setSinkId}}. The reason for this is that it
exposes fingerprinting information, but at the risk of prompting
the user if the device is not available or the user agent
decides not to honor the device id.</p>
</dd>
</dl>
</section>
</div>
</section>
</div>
</section>
<section id="privacy-considerations">
<h2>Privacy Considerations</h2>
<section id="privacy-consent">
<h3>Consent</h3>
<p>This document extends the Web platform with the ability to direct
audio output to non-default devices, when user permission is given.
User permission is necessary because playing audio out of a non-default
device may be unexpected behavior to the user, and may cause a nuisance.
For example, suppose a user is in a library or other quiet public place
where she is using a laptop with system audio directed to a USB headset.
Her expectation is that the laptop’s audio is private and she will not
disturb others. If any Web application can direct audio output through
arbitrary output devices, a mischievous website may play loud audio out
of the laptop’s external speakers without the user’s consent.</p>
<p>To prevent these kinds of nuisance scenarios, the user agent must
acquire the user’s consent to access non-default audio output devices.
This would prevent the library example outlined earlier, because the
application would not be permitted to play out audio from the system
speakers.</p>
<p>The specification adds no permission requirement to the default audio
output device.</p>
</section>
<section id="privacy-obtaining-consent">
<h3>Obtaining Consent</h3>
<p>The user agent may explicitly obtain user consent to play audio out of
non-default output devices using {{MediaDevices/selectAudioOutput}}.</p>
<p>Implementations MUST also support implicit consent via the
{{MediaDevices/getUserMedia()}} permission prompt; when an audio input
device is permitted and opened via {{MediaDevices/getUserMedia()}}
, this also permits access to any associated
audio output devices (i.e., those with the same {{MediaDeviceInfo/groupId}}).
This conveniently handles the common case of wanting
to route both input and output audio through a headset or speakerphone
device.</p>
<p>On page load, run the following step:</p>
<ol>
<li>
<p>On the <a data-cite="!HTML/#concept-relevant-global">relevant global object</a>,
create an internal slot: <dfn data-dfn-for="relevant global object">[[\explicitlyGrantedAudioOutputDevices]]</dfn>,
used to store devices that the user grants explicitly through {{MediaDevices/selectAudioOutput}},
initialized to an empty set.</p>
</li>
</ol>
<p>This specification specifies the <dfn data-cite="!GETUSERMEDIA#device-exposure-decision-non-camera-microphone">
exposure decision algorithm for devices other than camera and microphone</dfn>.
The algorithm runs as follows, with <var>device</var>, <var>microphoneList</var> and <var>cameraList</var> as input:
</p>
<ol>
<li>
<p>Let <var>document</var> be the
<a data-cite="!HTML/webappapis.html#current-settings-object">
current settings object</a>'s
<a data-cite="!HTML/webappapis.html#concept-relevant-global">
relevant global object</a>'s
<a data-cite="!HTML/window-object.html#concept-document-window">
associated <code>Document</code></a>.</p>
</li>
<li>
<p>Let <var>deviceInfo</var> be the result of
[=creating a device info object=] to represent <var>device</var>.</p>
</li>
<li>
<p>If <var>document</var> is not <a data-cite="!HTML/iframe-embed-object.html#allowed-to-use">
allowed to use</a> the feature identified by <a>"speaker-selection"</a>,
or <var>deviceInfo</var>.{{MediaDeviceInfo/kind}} is not {{ MediaDeviceKind/"audiooutput" }},
return <code>false</code>.</p>
</li>
<li>
<p>If <var>deviceInfo</var>.{{MediaDeviceInfo/deviceId}}
is in <a data-link-for="relevant global object">[[\explicitlyGrantedAudioOutputDevices]]</a>, return <code>true</code>.</p>
</li>
<li>
<p>If <var>deviceInfo</var>.{{MediaDeviceInfo/groupId}}
is the same as the <a data-cite="!GETUSERMEDIA#def-mediadeviceinfo-groupId">groupId</a>
of any microphone in <var>microphoneList</var>,
return <code>true</code>.</p>
</li>
<li>
<p>return <code>false</code>.</p>
</li>
</ol>
</section>
<section>
<h3 id=permissions-integration>Permissions Integration</h3>
<p>The Audio Output Devices API is a [=powerful feature=] that is
identified by the [=powerful feature/name=] "speaker-selection".</p>
<p>It defines the following types and algorithms:</p>
<dl>
<dt>
[=powerful feature/permission descriptor type=]
</dt>
<dd>
<p>
A permission covers access to at least one non-default speaker output device.
</p>
<p>
The semantics of the descriptor is that it queries for access to any non-default speaker
output device. Thus, if a query for the "speaker-selection" [=powerful feature=] returns
{{PermissionState/"granted"}}, the client knows that at least one of the
{{AudioOutputOptions/deviceId}}s previously shared with it can be passed to
{{MediaDevices/selectAudioOutput}} without incurring a permission prompt,
and if {{PermissionState/"denied"}} is returned, it knows that no {{MediaDevices/selectAudioOutput}} request
for an audio output device will succeed.
</p>
<p>
If the User Agent considers permission given to some, but not all, audio output devices,
a query will return {{PermissionState/"granted"}}.
</p>
<p>
If the User Agent considers permission denied to all audio output devices, a query
will return {{PermissionState/"denied"}}.
</p>
</dd>
</dl>
</section>
<section>
<h3 id=permissions-policy-integration>Permissions Policy Integration</h3>
<p>This specification defines one
[=policy-controlled feature=] identified by the string
<code><dfn>"speaker-selection"</dfn></code>.
It has a [=policy-controlled feature/default allowlist=]
of <code class=featurepolicy>"self"</code>.
<div class="note">
<p>A [=document=]'s [=Document/permissions policy=]
determines whether any content in that document is
[=allowed to use=] {{MediaDevices/selectAudioOutput}} to prompt the user for
an audio output device, or
[=allowed to use=] {{HTMLMediaElement/setSinkId}} to change the device
through which audio output should be rendered, to a non-system-default
user-permitted device. For {{MediaDevices/selectAudioOutput}} this is
enforced by the [=prompt the user to choose=] algorithm.
</p>
</div>
</section>
</section>
<section id="conformance">
<p>This specification defines conformance criteria that apply to a single
product: the <dfn>user agent</dfn> that implements the interfaces that it
contains.</p>
<p>Conformance requirements phrased as algorithms or specific steps may be
implemented in any manner, so long as the end result is equivalent. (In
particular, the algorithms defined in this specification are intended to be
easy to follow, and not intended to be performant.)</p>
<p>Implementations that use ECMAScript to implement the APIs defined in
this specification must implement them in a manner consistent with the
ECMAScript Bindings defined in the Web IDL specification [[!WEBIDL]], as
this specification uses that specification and terminology.</p>
</section>
<section id="ack">
<h2>Acknowledgments</h2>
<p>The following people have contributed directly to the development of
this specification: Harald Alvestrand, Rick Byers, Dominique
Hazael-Massieux (via the HTML5Apps project), Philip Jägenstedt, Victoria
Kirst, Shijun Sun, Martin Thomson, Chris Wilson.</p>
</section>
</body>
</html>