Skip to content

Commit e12f73f

Browse files
Update explainer with interrupted state info (MicrosoftEdge#882)
* Adding interrupted state info * Use link aliases to make the markdown more readable
1 parent 4eaa4e0 commit e12f73f

File tree

1 file changed

+95
-53
lines changed

1 file changed

+95
-53
lines changed

IframeMediaPause/iframe_media_pausing.md

+95-53
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ There are scenarios where a website might want to just not render an iframe. For
2727

2828
## Proposed solution: media-playback-while-not-visible Permission Policy
2929

30-
We propose creating a new "media-playback-while-not-visible" [Permission Policy](https://www.w3.org/TR/permissions-policy/) that would pause any media being played by iframes which are not currently rendered. For example, this would apply whenever the iframe’s `"display"` CSS property is set to `"none"` or when the the `"visibility"` property is set to `"hidden"` or `"collapse"`.
30+
We propose creating a new "media-playback-while-not-visible" [permission policy] that would pause any media being played by iframes which are not currently rendered. For example, this would apply whenever the iframe’s `"display"` CSS property is set to `"none"` or when the the `"visibility"` property is set to `"hidden"` or `"collapse"`.
3131

3232
This policy will have a default value of '*', meaning that all of the nested iframes are allowed to play media when not rendered. The example below show how this permission policy could be used to prevent all the nested iframes from playing media. By doing it this way, even other iframes embedded by "foo.media.com" shouldn’t be allowed to play media if not rendered.
3333

@@ -58,21 +58,21 @@ Permissions-Policy: media-playback-while-not-visible=(self)
5858

5959
In this case, `example.com` serves a document that embeds an iframe with a document from `https://foo.media.com`. Since the HTTP header only allows documents from `https://example.com` to inherit `media-playback-while-not-visible`, the iframe will not be able to use the feature.
6060

61-
In the past, the ["execution-while-not-rendered" and "execution-while-out-of-viewport"](https://wicg.github.io/page-lifecycle/#feature-policies) permission policies have been proposed as additions to the Page Lifecycle draft specification. However, these policies freeze all iframe JavaScript execution when not rendered, which is not desirable for the featured use case. Moreover, this proposal has not been adopted or standardized.
61+
In the past, the `"execution-while-not-rendered"` and `"execution-while-out-of-viewport"` permission policies have been proposed as additions to the [Page Lifecycle API] draft specification. However, these policies freeze all iframe JavaScript execution when not rendered, which is not desirable for the featured use case. Moreover, this proposal has not been adopted or standardized.
6262

6363
## Interoperability with other Web API's
6464

6565
Given that there exists many ways for a website to render audio in the broader web platform, this proposal has points of contact with many API's. To be more specific, there are two scenarios where this interaction might happen. Let's consider an iframe, which is not allowed to play `media-playback-while-not-visible`:
6666
- Scenario 1: When the iframe is not rendered and it attempts to play audio; and
67-
- Callers should treat this scenario as if they weren't allowed to start media playback. Like when the [`autoplay` permission policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/autoplay) is set to `'none'` for an iframe.
67+
- Callers should treat this scenario as if they weren't allowed to start media playback. Like when the [`autoplay` permission policy] is set to `'none'` for an iframe.
6868
- Scenario 2: When the iframe is already playing audio and stops being rendered during media playback.
6969
- Callers should treat this scenario as if the user had paused media playback.
7070

7171
The following subsections covers how this proposal could interact with Web APIs that render audio.
7272

7373
### HTMLMediaElements
7474

75-
HTMLMediaElement media playback is started and paused, respectively, with the [`play`](https://html.spec.whatwg.org/multipage/media.html#dom-media-play) and [`pause`](https://html.spec.whatwg.org/multipage/media.html#dom-media-pause) methods. For scenario 1, the media element shouldn't be [allowed to play](https://html.spec.whatwg.org/multipage/media.html#allowed-to-play) and `play` should return a promise rejected with `"NotAllowedError"`. In this case, the website could easily handle this like shown below.
75+
HTMLMediaElement media playback is started and paused, respectively, with the [`play()`] and [`pause()`] methods. For scenario 1, the media element shouldn't be [allowed to play] and `play()` should return a promise rejected with `"NotAllowedError"`. In this case, the website could easily handle this like shown below.
7676

7777
```js
7878
const videoElem = document.querySelector("video");
@@ -109,9 +109,11 @@ videoElem.addEventListener("pause", (event) => {
109109

110110
### Web Audio API
111111

112+
_This Web Audio API integration is dependent on the successful specification and implementation of the new AudioContextState `"interrupted"` proposed in this [explainer][`"interrupted"`]._
113+
112114
The Web Audio API renders audio through an [AudioContext](https://webaudio.github.io/web-audio-api/#AudioContext) object. We propose that the `AudioContext` shouldn't be [allowed to start](https://webaudio.github.io/web-audio-api/#allowed-to-start) whenever it is not rendered and disallowed by the `media-playback-while-not-visible` policy.
113115

114-
For scenario 1, if the iframe is not rendered, any `AudioContext` will not be [allowed to start](https://webaudio.github.io/web-audio-api/#allowed-to-start). Therefore, attempting to [create](https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext) a new `AudioContext` or start playback by calling [`resume()`](https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume) shouldn't output any audio and put the `AudioContext` into a [`"suspended"`](https://webaudio.github.io/web-audio-api/#dom-audiocontextstate-suspended) state. It would be recommended for the iframe to wait for a new user interaction event before calling [`resume()`](https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume) - e.g., `click`.
116+
For scenario 1, if the iframe is not rendered, any `AudioContext` will not be [allowed to start]. Therefore, [creating][AudioContext constructor] a new `AudioContext` should initially put it into the [`"suspended"`] state. Consequently, attempting to start playback by calling [`resume()`] shouldn't output any audio and move the `AudioContext` to the [`"interrupted"`] state. In this case, the webpage can then listen to [`statechange`] events to determine when the interruption is over.
115117

116118
```js
117119
// AudioContext being created in a not rendered iframe, where
@@ -120,62 +122,72 @@ let audioCtx = new AudioContext();
120122
let oscillator = audioCtx.createOscillator();
121123
oscillator.connect(audioCtx.destination);
122124

123-
const resume_button = document.querySelector("resume_button");
124-
resume_button.addEventListener('click', () => {
125-
if (audioCtx.state === "suspended") {
126-
audioCtx.resume().then(() => {
127-
console.log("Context resumed");
128-
});
129-
}
130-
})
125+
audioCtx.onStateChange = () => {
126+
console.log(audioCtx.state);
127+
}
131128

132-
oscillator.start(0);
133129
// should print 'suspended'
134130
console.log(audioCtx.state)
131+
oscillator.start(0);
132+
// `audioCtx.onStateChange` should print 'interrupted'
135133
```
136134

137-
Similarly, for scenario 2, when the iframe becomes not rendered during audio playback, the user agent should run the [`suspend()`](https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend) steps. The audio context state should change to `'suspended'` and the website can monitor this by listening to the [`statechange`](https://webaudio.github.io/web-audio-api/#eventdef-baseaudiocontext-statechange) event.
135+
Similarly, for scenario 2, when the iframe becomes not rendered during audio playback, the user agent should interrupt the `AudioContext` by moving it to the [`"interrupted"`] state. Likewise, when the interruption is over, the UA should move the `AudioContext` back to the [`"running"`] state. Webpages can monitor these transitions by listening to the [`statechange`] event.
138136

139-
```js
140-
let audioCtx = new AudioContext();
141-
let oscillator = audioCtx.createOscillator();
142-
oscillator.connect(audioCtx.destination);
137+
The snippet below show this could work for scenario 2. Let's assume that the `AudioContext` in the iframe is [allowed to start]. When the web page is initialized, the `AudioContext` will be able to play audio and will transition to the [`"running"`] state. If the user clicks on the `"iframe_visibility_btn"`, the frame will get hidden and the `AudioContext` should be put in the [`"interrupted"`] state. Likewise, pressing again the button will show the iframe again and audio playback will be resumed.
143138

144-
const playback_control_btn = document.querySelector("playback_control_button");
145-
playback_control_btn.textContent = "start";
146-
playback_control_btn.addEventListener('click', () => {
147-
if (audioCtx.state === "suspended") {
148-
audioCtx.resume().then(() => {
149-
console.log("Context resumed");
150-
playback_control_btn.textContent = "suspend";
151-
});
152-
} else if (audioCtx.state === "running") {
153-
audioCtx.suspend().then(() => {
154-
console.log("Context suspended");
155-
playback_control_btn.textContent = "resume";
156-
});
157-
}
158-
})
139+
```html
140+
<!-- audio_context_iframe.html -->
141+
<html>
142+
<body>
143+
<script>
144+
let audioCtx = new AudioContext();
145+
let oscillator = audioCtx.createOscillator();
146+
oscillator.connect(audioCtx.destination);
159147
160-
audioCtx.addEventListener("statechange", (event) => {
161-
if(audioCtx.state === "suspended"){
162-
// Context has been suspended, because either suspend() has been
163-
// called or the document is not-rendered.
164-
console.log("Context suspended");
165-
playback_control_btn.textContent = "resume";
166-
}
167-
});
148+
audioCtx.onStateChange = () => {
149+
console.log(audioCtx.state);
150+
}
168151
169-
oscillator.start(0);
170-
console.log(audioCtx.state)
152+
oscillator.start(0);
153+
</script>
154+
</body>
155+
</html>
171156

157+
<!-- Top-level document -->
158+
<html>
159+
<body>
160+
<iframe id="media_frame"
161+
src="audio_context_iframe.html"
162+
allow="media-playback-while-not-visible 'none'">
163+
</iframe>
164+
<button id="iframe_visibility_btn">Hide Iframe</button>
165+
<script>
166+
const BTN_HIDE_DISPLAY_NONE_STR = 'Hide Iframe';
167+
const BTN_SHOW_DISPLAY_NONE_STR = 'Show Iframe';
168+
const iframe_visibility_btn = "iframe_visibility_btn"
169+
170+
let display_btn = document.getElementById(iframe_visibility_btn);
171+
display_btn.innerHTML = BTN_HIDE_DISPLAY_NONE_STR;
172+
display_btn.addEventListener('click', () => {
173+
if (display_btn.innerHTML == BTN_HIDE_DISPLAY_NONE_STR){
174+
iframe.style.setProperty('display', 'none')
175+
display_btn.innerHTML = BTN_SHOW_DISPLAY_NONE_STR
176+
} else {
177+
iframe.style.setProperty('display', 'block')
178+
display_btn.innerHTML = BTN_HIDE_DISPLAY_NONE_STR
179+
}
180+
});
181+
</script>
182+
</body>
183+
</html>
172184
```
173185

174186
### Web Speech API
175187

176-
The [Web Speech API](https://wicg.github.io/speech-api/) proposes a [SpeechSynthesis interface](https://wicg.github.io/speech-api/#tts-section). The latter interface allows websites to create text-to-speech output by calling [`window.speechSynthesis.speak`](https://wicg.github.io/speech-api/#dom-speechsynthesis-speak) with a [`SpeechSynthesisUtterance`](https://wicg.github.io/speech-api/#speechsynthesisutterance), which represents the text-to-be-said.
188+
The [Web Speech API] proposes a [SpeechSynthesis interface]. The latter interface allows websites to create text-to-speech output by calling [`window.speechSynthesis.speak`] with a [`SpeechSynthesisUtterance`], which represents the text-to-be-said.
177189

178-
For both scenarios, the iframe should listen for utterance errors when calling `window.speechSynthesis.speak()`. For scenario 1 it should fail with a [`"not-allowed"`](https://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-not-allowed) SpeechSyntesis error; and, for scenario 2, it should fail with an [`"interrupted"`](https://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-interrupted) error.
190+
For both scenarios, the iframe should listen for utterance errors when calling `window.speechSynthesis.speak()`. For scenario 1 it should fail with a [`"not-allowed"` SpeechSynthesisErrorCode]; and, for scenario 2, it should fail with an [`"interrupted"` SpeechSynthesisErrorCode].
179191

180192
```js
181193
let utterance = new SpeechSynthesisUtterance('blabla');
@@ -199,12 +211,12 @@ This proposal does not affect autoplay behavior unless the media-playing iframe
199211

200212
Both `execution-while-not-rendered` and `execution-while-out-of-viewport` permission policies should take precedence over `media-playback-while-not-visible`. Therefore, in the case that we have an iframe with colliding permissions for the same origin, `media-playback-while-not-visible` should only be considered if the iframe is allowed to execute. The user agent should perform the following checks:
201213

202-
1. If the origin is not [allowed to use](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#allowed-to-use) the [`"execution-while-not-rendered"`](https://wicg.github.io/page-lifecycle/#execution-while-not-rendered) feature, then:
203-
1. If the iframe is not [being rendered](https://html.spec.whatwg.org/multipage/rendering.html#being-rendered), freeze execution of the iframe context and return.
204-
2. If the origin is not [allowed to use](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#allowed-to-use) the [`"execution-while-out-of-viewport"`](https://wicg.github.io/page-lifecycle/#execution-while-out-of-viewport) feature, then:
205-
1. If the iframe does not intersect the [viewport](https://www.w3.org/TR/CSS2/visuren.html#viewport), freeze execution of the iframe context and return.
206-
3. If the origin is not [allowed to use](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#allowed-to-use) the [`"media-playback-while-not-visible"`](#proposed-solution-media-playback-while-not-visible-permission-policy) feature, then:
207-
1. If the iframe is not [being rendered](https://html.spec.whatwg.org/multipage/rendering.html#being-rendered), pause all media playback from the iframe context and return.
214+
1. If the origin is not [allowed to use] the [`"execution-while-not-rendered"`] feature, then:
215+
1. If the iframe is not [being rendered], freeze execution of the iframe context and return.
216+
2. If the origin is not [allowed to use] the [`"execution-while-out-of-viewport"`] feature, then:
217+
1. If the iframe does not intersect the [viewport], freeze execution of the iframe context and return.
218+
3. If the origin is not [allowed to use] the [`"media-playback-while-not-visible"`](#proposed-solution-media-playback-while-not-visible-permission-policy) feature, then:
219+
1. If the iframe is not [being rendered], pause all media playback from the iframe context and return.
208220

209221
## Alternative Solutions
210222

@@ -244,4 +256,34 @@ Similarly to the [HTMLMediaElement.muted](https://html.spec.whatwg.org/multipage
244256
</html>
245257
```
246258

247-
This alternative was not selected as the preferred one, because we think that pausing media playback is preferable to just muting it.
259+
This alternative was not selected as the preferred one, because we think that pausing media playback is preferable to just muting it.
260+
261+
[AudioContext constructor]: https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext
262+
[allowed to play]: https://html.spec.whatwg.org/multipage/media.html#allowed-to-play
263+
[allowed to start]: https://webaudio.github.io/web-audio-api/#allowed-to-start
264+
[allowed to use]: https://html.spec.whatwg.org/multipage/iframe-embed-object.html#allowed-to-use
265+
[`autoplay` permission policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/autoplay
266+
[being rendered]: https://html.spec.whatwg.org/multipage/rendering.html#being-rendered
267+
[`"execution-while-not-rendered"`]: https://wicg.github.io/page-lifecycle/#execution-while-not-rendered
268+
[`"execution-while-out-of-viewport"`]: https://wicg.github.io/page-lifecycle/#execution-while-out-of-viewport
269+
[`"interrupted"`]: https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/AudioContextInterruptedState/explainer.md
270+
[`"interrupted"` SpeechSynthesisErrorCode]: ttps://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-interrupted
271+
[`"not-allowed"` SpeechSynthesisErrorCode]: https://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-not-allowed
272+
[Page Lifecycle API]: https://wicg.github.io/page-lifecycle/#feature-policies
273+
[permission policy]: https://www.w3.org/TR/permissions-policy/
274+
[`pause()`]: https://html.spec.whatwg.org/multipage/media.html#dom-media-pause
275+
[`play()`]: https://html.spec.whatwg.org/multipage/media.html#dom-media-play
276+
[`SpeechSynthesisUtterance`]: https://wicg.github.io/speech-api/#speechsynthesisutterance
277+
[SpeechSynthesis interface]: https://wicg.github.io/speech-api/#tts-section
278+
[`statechange`]: https://webaudio.github.io/web-audio-api/#eventdef-baseaudiocontext-statechange
279+
[`"suspended"`]: https://webaudio.github.io/web-audio-api/#dom-audiocontextstate-suspended
280+
[`resume()`]: https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume
281+
[`"running"`]: https://webaudio.github.io/web-audio-api/#dom-audiocontextstate-running
282+
[viewport]: https://www.w3.org/TR/CSS2/visuren.html#viewport
283+
[Web Speech API]: https://wicg.github.io/speech-api/
284+
[`window.speechSynthesis.speak`]: https://wicg.github.io/speech-api/#dom-speechsynthesis-speak
285+
286+
287+
The [Web Speech API](https://wicg.github.io/speech-api/) proposes a [SpeechSynthesis interface](https://wicg.github.io/speech-api/#tts-section). The latter interface allows websites to create text-to-speech output by calling [`window.speechSynthesis.speak`](https://wicg.github.io/speech-api/#dom-speechsynthesis-speak) with a [`SpeechSynthesisUtterance`](https://wicg.github.io/speech-api/#speechsynthesisutterance), which represents the text-to-be-said.
288+
289+
For both scenarios, the iframe should listen for utterance errors when calling `window.speechSynthesis.speak()`. For scenario 1 it should fail with a [`"not-allowed" SpeechSynthesisErrorCode`](https://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-not-allowed) SpeechSyntesis error; and, for scenario 2, it should fail with an [`"interrupted" SpeechSynthesisErrorCode`](https://wicg.github.io/speech-api/#dom-speechsynthesiserrorcode-interrupted) error.

0 commit comments

Comments
 (0)