├── .github └── workflows │ └── auto-publish.yml ├── .gitignore ├── .manually_checked_links ├── .pr-preview.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── explainer.md ├── index.html ├── script.js ├── style.css └── w3c.json /.github/workflows/auto-publish.yml: -------------------------------------------------------------------------------- 1 | name: Auto Publish 2 | 3 | on: 4 | push: 5 | paths: index.html 6 | branches: 7 | - main 8 | pull_request: {} 9 | 10 | jobs: 11 | validate-and-publish: 12 | name: Validate and Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: w3c/spec-prod@v2 # use the action 17 | with: 18 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 19 | # Please use the URL that's appropriate for your working group! 20 | W3C_WG_DECISION_URL: "https://lists.w3.org/Archives/Public/public-webrtc/2016Mar/0031.html" 21 | W3C_NOTIFICATIONS_CC: "dom@w3.org" 22 | VALIDATE_LINKS: false 23 | W3C_BUILD_OVERRIDE: | 24 | shortName: audio-output 25 | specStatus: CRD 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | build 4 | support 5 | webrtc-respec-ci 6 | -------------------------------------------------------------------------------- /.manually_checked_links: -------------------------------------------------------------------------------- 1 | http://webaudio.github.io/web-audio-api/#the-audiocontext-interface 2 | http://webaudio.github.io/web-audio-api/#widl-AudioContext-createMediaStreamDestination-MediaStreamAudioDestinationNode 3 | http://webaudio.github.io/web-audio-api/#the-audiodestinationnode-interface 4 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.html", 3 | "type": "respec" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the 2 | [W3C Patent Policy](https://www.w3.org/Consortium/Patent-Policy/) and 3 | [Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). To make substantive contributions to specifications, you must either participate 4 | in the relevant W3C Working Group or make a non-member patent licensing commitment. 5 | 6 | If you are not the sole contributor to a contribution (pull request), please identify all 7 | contributors in the pull request comment. 8 | 9 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 10 | 11 | ``` 12 | +@github_username 13 | ``` 14 | 15 | If you added a contributor by mistake, you can remove them in a comment with: 16 | 17 | ``` 18 | -@github_username 19 | ``` 20 | 21 | If you are making a pull request on behalf of someone else but you had no part in designing the 22 | feature, you can remove yourself with the above syntax. 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All documents in this Repository are licensed by contributors 2 | under the 3 | [W3C Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Specification 'mediacapture-output' 3 | 4 | This is the repository for mediacapture-output. You're welcome to contribute! Let's make the Web rock our socks 5 | off! 6 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # Audio Output Devices API explained 2 | 3 | The Audio Output Devices API allows JavaScript applications to direct the audio 4 | output of a media element to authorized devices other than the system or user 5 | agent default. This can be helpful in a variety of real-time communication 6 | scenarios as well as general media applications. For example, an application 7 | can use this API to programmatically direct output to a device such as a 8 | Bluetooth headset or speakerphone. 9 | 10 | The Audio Output Devices API adds a setSinkId method and a sinkId field to 11 | HTML media elements. The setSinkId() method allows changing the audio output 12 | device on which the element renders audio, and the sinkId attribute is a string 13 | that contains the ID of the device where the element is currently rendering 14 | audio. 15 | 16 | By default, the sinkId field is the empty string, which means that audio will be 17 | rendered on the default audio output device in the system. 18 | 19 | The set of valid device IDs that can be used as argument to setSinkId() can be 20 | obtained from a call to [enumerateDevices](https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices). 21 | For example: 22 | 23 | ```js 24 | var audioElement = new Audio('sound.ogg'); 25 | audioElement.play(); 26 | navigator.mediaDevices.enumerateDevices().then(devices => { 27 | var lastOutputDeviceId = ''; 28 | for (i in devices) { 29 | if (devices[i].kind == 'audiooutput') { 30 | lastOutputDeviceId = devices[i].deviceId; 31 | } 32 | } 33 | audioElement.setSinkId(lastOutputDeviceId).then(() => { 34 | console.log('Audio output device set to ' + audioElement.sinkId); 35 | }).catch(err => { 36 | console.log('setSinkId() failed', err); 37 | }); 38 | }).catch(err => { 39 | console.log('enumerateDevices() failed :(', err); 40 | }); 41 | 42 | ``` 43 | 44 | In this example, the script creates an audio element that starts to play the 45 | 'sound.ogg' file on the default audio output device. Then the output device 46 | is changed to the last audio output device listed in the result of a call to 47 | enumerateDevices. 48 | 49 | SetSinkId requires the user to provide authorization if a device other than 50 | the system default is to be used. A programmatic way to get this authorization 51 | is to use the [getUserMedia](https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia()) function. 52 | getUserMedia can be used to produce audio streams from a microphone and it 53 | is required to authorize any corresponding output devices. 54 | If the application does not require the audio track produced by getUserMedia, 55 | it can be closed. For example, one standard way to ensure that all output 56 | devices are authorized (provided there is a corresponding microphone) is: 57 | 58 | ```js 59 | navigator.mediaDevices.enumerateDevices().then(async devices => { 60 | for (i in devices) { 61 | if (devices[i].kind == 'audiooutput') { 62 | var stream = await navigator.mediaDevices.getUserMedia({audio: {groupId: {exact: devices[i].groupId}}}); 63 | stream.getAudioTracks()[0].stop(); 64 | console.log('Authorization succeeded for device ' + devices[i].deviceId); 65 | } 66 | } 67 | }).catch(err => { 68 | console.log('An error occurred :(', err); 69 | }); 70 | ``` 71 | 72 | Note that implementations are not required to use a per-device authorization 73 | model. For example, an implementation may use a model based on authorization 74 | per device class. In this case a single getUserMedia call would suffice to 75 | authorize all devices. The [permissions API](https://w3c.github.io/permissions/) 76 | can be used to determine if it is necessary to request permissions. 77 | 78 | ## Demo 79 | 80 | [WebRTC samples](https://webrtc.github.io/samples) has a 81 | [demo](https://webrtc.github.io/samples/src/content/devices/multi/) that shows 82 | how to use the Audio OutputDevices API to change the output device used by media 83 | elements that render static files and sound coming from a microphone. 84 | 85 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |This document defines a set of JavaScript APIs that let a Web 16 | application manage how audio is rendered on the user audio output 17 | devices.
18 |The WebRTC and Device and Sensors Working Group intend to publish this 21 | specification as a Candidate Recommendation soon. Consequently, this is a 22 | Request for wide review of this document.
23 |This proposal allows JavaScript to direct the audio output of a media 27 | element to permitted devices other than the system or user agent default. 28 | This can be helpful in a variety of real-time communication scenarios as 29 | well as general media applications. For example, an application can use 30 | this API to programmatically direct output to a device such as a Bluetooth 31 | headset or speakerphone.
32 |This section specifies additions to the {{HTMLMediaElement}} [[HTML]] when the Audio Output Devices API is 36 | supported.
37 |When the {{HTMLMediaElement}} constructor is invoked, the user 38 | agent MUST add the following initializing step:
39 |Let the element have a [[\SinkId]] internal slot,
42 | initialized to ""
.
43 |
47 | partial interface HTMLMediaElement { 48 | [SecureContext] readonly attribute DOMString sinkId; 49 | [SecureContext] Promise<undefined> setSinkId (DOMString sinkId); 50 | }; 51 |52 |
This attribute contains the ID of the audio device through which 59 | output is being delivered, or the empty string if output is 60 | delivered through the user-agent default device. If nonempty, this 61 | ID should be equal to the {{MediaDeviceInfo/deviceId}} 62 | attribute of one of the {{MediaDeviceInfo}} values returned from 63 | {{MediaDevices/enumerateDevices()}}.
64 |On getting, the 65 | attribute MUST return the value of the {{HTMLMediaElement/[[SinkId]]}} slot.
66 |Sets the ID of the audio device through which audio output should 76 | be rendered if the application is permitted to play out of a given 78 | device.
79 |When this method is invoked, the user agent must run the following 80 | steps:
81 |Let document be the
84 |
85 | current settings object's
86 |
87 | relevant global object's
88 |
89 | associated Document
.
If document is not 93 | 94 | allowed to use the feature identified by 95 | "speaker-selection", return a 96 | promise rejected with a new {{DOMException}} 97 | whose name is {{NotAllowedError}}. 98 |
99 |Let element be the {{HTMLMediaElement}} 102 | object on which this method was invoked.
103 |Let sinkId be the method's first argument.
106 |If sinkId is equal to element's
109 | {{HTMLMediaElement/[[SinkId]]}},
110 | return a promise resolved with undefined
.
Let p be a new promise.
114 |Run the following substeps in parallel:
117 |If sinkId is not the empty string and does not 120 | match any audio output device identified by the result that 121 | would be provided by {{MediaDevices/enumerateDevices()}}, 122 | reject p with a new 123 | {{DOMException}} whose name is 124 | {{NotFoundError}} and abort these substeps.
125 |If sinkId is not the empty string, and the 128 | application would not be permitted to play audio through 129 | the device identified by sinkId if it weren't the 130 | current user agent default device, reject p 131 | with a new {{DOMException}} whose name is 132 | {{NotAllowedError}} and abort these substeps.
133 |Switch the underlying audio output device for element 136 | to the audio device identified by sinkId.
137 |If this substep is successful and the media 138 | element's {{HTMLMediaElement/paused}} attribute is false, audio MUST stop playing 139 | out of the device represented by the element's {{HTMLMediaElement/sinkId}} attribute and will start 140 | playing out of the device identified by sinkId
141 |If the preceding substep failed, reject p 144 | with a new {{DOMException}} whose name is 145 | {{AbortError}}, 146 | and abort these substeps.
147 |Queue a task that runs the following steps:
150 |Set element's {{HTMLMediaElement/[[SinkId]]}} to 153 | sinkId.
154 |Resolve p.
157 |Return p.
164 |New audio devices may become available to the user agent, or an 197 | audio device (identified by a media element's {{HTMLMediaElement/sinkId}} attribute) that had 198 | previously become [= unavailable =] may become available 199 | again, for example, if it is unplugged and later plugged back in.
200 |In this scenario, the user agent must run the following steps:
201 |Let sinkId be the identifier for the newly available 204 | device.
205 |For each media element whose {{HTMLMediaElement/sinkId}} attribute is equal to 208 | sinkId:
209 |If the media element's 212 | {{HTMLMediaElement/paused}} attribute is false, start rendering 213 | this object's audio out of the device represented by the 214 | {{HTMLMediaElement/sinkId}} 215 | attribute.
216 |The following paragraph is non-normative.
221 |If the application wishes to react to the device
222 | change, the application can listen to the
223 | devicechange
event and query
224 | {{MediaDevices/enumerateDevices()}} for the list of updated
225 | devices.
This section specifies additions to the {{MediaDevices}} 232 | when the Audio Output Devices API is 233 | supported.
234 |236 | partial interface MediaDevices { 237 | Promise<MediaDeviceInfo> selectAudioOutput(optional AudioOutputOptions options = {}); 238 | }; 239 |240 |
Prompts the user to select a specific audio output device.
248 |When the {{selectAudioOutput}} method is called, 249 | the [=user agent=] MUST run the following steps:
250 |If the [=relevant global object=] of [=this=] does not have 252 | [=transient activation=], return a promise rejected with 253 | a {{DOMException}} object whose {{DOMException/name}} attribute 254 | has the value {{InvalidStateError}}.
Let options be the method's first argument.
Let deviceId be options.deviceId
.
Let mediaDevices be [=this=].
Let p be a new promise.
Run the following steps in parallel:
261 |Let descriptor be a {{PermissionDescriptor}} with its 263 | [=powerful feature/name=] set to "speaker-selection"
264 |If descriptor's [=permission state=] is 267 | {{PermissionState/"denied"}}, reject 268 | p with a new {{DOMException}} whose 269 | {{DOMException/name}} attribute has the value 270 | {{NotAllowedError}}, and abort these steps.
271 |Probe the [=user agent=] for available audio output devices.
If there is no audio output device, reject p 275 | with a new {{DOMException}} whose {{DOMException/name}} 276 | attribute has the value {{NotFoundError}} and abort 277 | these steps.
278 |If deviceId is not ""
281 | run the following sub steps:
If deviceId matches a 285 | a device id previously exposed by 286 | {{MediaDevices/selectAudioOutput}} in this or an earlier browsing 287 | session, or matches a device id of an audio output device 288 | with the same groupId as an audio input device previously 289 | exposed by {{MediaDevices/getUserMedia()}} in this or an earlier browsing 290 | session, the user agent MAY decide, based on its previous 291 | decision of whether to persist this id or not for this set 292 | of origins, to run the following sub steps:
293 |Let device be the device identified by 296 | deviceId, if available.
297 |If device is available, resolve 299 | p with either deviceId or a freshly 300 | rotated device id for device, and abort the 301 | in-parallel steps.
[=Prompt the user to choose=] an audio output device, with 307 | descriptor.
If the result of the request is {{PermissionState/"denied"}}, reject 309 | p with a new {{DOMException}} whose {{DOMException/name}} attribute 310 | has the value {{NotAllowedError}} and abort these steps.
Let selectedDevice be the user-selected audio output device.
313 |Let deviceInfo be the result of 315 | [=creating a device info object=] to represent selectedDevice, 316 | with mediaDevices.
Add deviceInfo.{{MediaDeviceInfo/deviceId}} 318 | to [[\explicitlyGrantedAudioOutputDevices]].
Resolve p with deviceInfo.
Return p.
Once a device is exposed after a call to {{MediaDevices/selectAudioOutput}}, it MUST be listed by 325 | {{MediaDevices/enumerateDevices()}} for the current browsing context.
326 |If the promise returned by {{MediaDevices/selectAudioOutput}} is resolved, 327 | then the user agent MUST ensure the document is both immediately 328 | allowed to play media in an 329 | {{HTMLMediaElement}}, and immediately 330 | allowed to start an 331 | {{AudioContext}}, without needing any additional user gesture.
332 |This is imprecise due to the current lack of standardization of 334 | autoplay in browsers.
335 |This dictionary describes the options that can be used to obtain 342 | access to an audio output device.
343 |dictionary AudioOutputOptions { 346 | DOMString deviceId = ""; 347 | };348 |
""
When the value of this dictionary member
356 | is not ""
, and matches the id previously exposed by
357 | {{MediaDevices/selectAudioOutput}} or
358 | a device id of an audio output device with the same groupId as an
359 | audio input device previously exposed by
360 | {{MediaDevices/getUserMedia()}} in this or an earlier session, the user
361 | agent MAY opt to skip prompting the user in favor of resolving
362 | with this id or a new rotated id for the same device, assuming
363 | that device is currently available.
Applications that wish to rely on user agents 365 | supporting persisted device ids must pass these through 366 | {{MediaDevices/selectAudioOutput}} successfully before they will 367 | work with {{HTMLMediaElement/setSinkId}}. The reason for this is that it 368 | exposes fingerprinting information, but at the risk of prompting 369 | the user if the device is not available or the user agent 370 | decides not to honor the device id.
371 |This document extends the Web platform with the ability to direct 383 | audio output to non-default devices, when user permission is given. 384 | User permission is necessary because playing audio out of a non-default 385 | device may be unexpected behavior to the user, and may cause a nuisance. 386 | For example, suppose a user is in a library or other quiet public place 387 | where she is using a laptop with system audio directed to a USB headset. 388 | Her expectation is that the laptop’s audio is private and she will not 389 | disturb others. If any Web application can direct audio output through 390 | arbitrary output devices, a mischievous website may play loud audio out 391 | of the laptop’s external speakers without the user’s consent.
392 |To prevent these kinds of nuisance scenarios, the user agent must 393 | acquire the user’s consent to access non-default audio output devices. 394 | This would prevent the library example outlined earlier, because the 395 | application would not be permitted to play out audio from the system 396 | speakers.
397 |The specification adds no permission requirement to the default audio 398 | output device.
399 |The user agent may explicitly obtain user consent to play audio out of 403 | non-default output devices using {{MediaDevices/selectAudioOutput}}.
404 |Implementations MUST also support implicit consent via the 405 | {{MediaDevices/getUserMedia()}} permission prompt; when an audio input 406 | device is permitted and opened via {{MediaDevices/getUserMedia()}} 407 | , this also permits access to any associated 408 | audio output devices (i.e., those with the same {{MediaDeviceInfo/groupId}}). 409 | This conveniently handles the common case of wanting 410 | to route both input and output audio through a headset or speakerphone 411 | device.
412 |On page load, run the following step:
413 |On the relevant global object, 416 | create an internal slot: [[\explicitlyGrantedAudioOutputDevices]], 417 | used to store devices that the user grants explicitly through {{MediaDevices/selectAudioOutput}}, 418 | initialized to an empty set.
419 |This specification specifies the 422 | exposure decision algorithm for devices other than camera and microphone. 423 | The algorithm runs as follows, with device, microphoneList and cameraList as input: 424 |
425 |Let document be the
428 |
429 | current settings object's
430 |
431 | relevant global object's
432 |
433 | associated Document
.
Let mediaDevices be the
437 |
438 | current settings object's
439 |
440 | relevant global object's
441 |
442 | associated MediaDevices
.
Let deviceInfo be the result of 446 | [=creating a device info object=] to represent device, 447 | with mediaDevices.
448 |If document is not
451 | allowed to use the feature identified by "speaker-selection",
452 | or deviceInfo.{{MediaDeviceInfo/kind}} is not {{ MediaDeviceKind/"audiooutput" }},
453 | return false
.
If deviceInfo.{{MediaDeviceInfo/deviceId}}
457 | is in [[\explicitlyGrantedAudioOutputDevices]], return true
.
If [=microphone information can be exposed=] on mediaDevices is true
,
461 | return true
.
return false
.
The Audio Output Devices API is a [=powerful feature=] that is 471 | identified by the [=powerful feature/name=] "speaker-selection".
472 |It defines the following types and algorithms:
473 |479 | A permission covers access to at least one non-default speaker output device. 480 |
481 |482 | The semantics of the descriptor is that it queries for access to any non-default speaker 483 | output device. Thus, if a query for the "speaker-selection" [=powerful feature=] returns 484 | {{PermissionState/"granted"}}, the client knows that at least one of the 485 | {{AudioOutputOptions/deviceId}}s previously shared with it can be passed to 486 | {{MediaDevices/selectAudioOutput}} without incurring a permission prompt, 487 | and if {{PermissionState/"denied"}} is returned, it knows that no {{MediaDevices/selectAudioOutput}} request 488 | for an audio output device will succeed. 489 |
490 |491 | If the User Agent considers permission given to some, but not all, audio output devices, 492 | a query will return {{PermissionState/"granted"}}. 493 |
494 |495 | If the User Agent considers permission denied to all audio output devices, a query 496 | will return {{PermissionState/"denied"}}. 497 |
498 |This specification defines one
504 | [=policy-controlled feature=] identified by the string
505 | "speaker-selection"
.
506 | It has a [=policy-controlled feature/default allowlist=]
507 | of "self"
.
508 |
A [=document=]'s [=Document/permissions policy=] 510 | determines whether any content in that document is 511 | [=allowed to use=] {{MediaDevices/selectAudioOutput}} to prompt the user for 512 | an audio output device, or 513 | [=allowed to use=] {{HTMLMediaElement/setSinkId}} to change the device 514 | through which audio output should be rendered, to a non-system-default 515 | user-permitted device. For {{MediaDevices/selectAudioOutput}} this is 516 | enforced by the [=prompt the user to choose=] algorithm. 517 |
518 |This specification defines conformance criteria that apply to a single 523 | product: the user agent that implements the interfaces that it 524 | contains.
525 |Conformance requirements phrased as algorithms or specific steps may be 526 | implemented in any manner, so long as the end result is equivalent. (In 527 | particular, the algorithms defined in this specification are intended to be 528 | easy to follow, and not intended to be performant.)
529 |Implementations that use ECMAScript to implement the APIs defined in 530 | this specification must implement them in a manner consistent with the 531 | ECMAScript Bindings defined in the Web IDL specification [[!WEBIDL]], as 532 | this specification uses that specification and terminology.
533 |The following people have contributed directly to the development of 537 | this specification: Harald Alvestrand, Rick Byers, Dominique 538 | Hazael-Massieux (via the HTML5Apps project), Philip Jägenstedt, Victoria 539 | Kirst, Shijun Sun, Martin Thomson, Chris Wilson.
540 |