Skip to content

Commit 33bfe3b

Browse files
committed
new approach to handling browsers not allowing media with audio to play, reveal.js will now play videos muted and show an unmute button in the lower left
1 parent becc9bd commit 33bfe3b

12 files changed

Lines changed: 232 additions & 57 deletions

File tree

css/reveal.scss

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,7 +1494,7 @@ $controlsArrowAngleActive: 36deg;
14941494
height: var(--r-overlay-header-height);
14951495
gap: 6px;
14961496
}
1497-
.r-overlay-header .r-overlay-button {
1497+
.r-overlay-header .r-overlay-header-button {
14981498
all: unset;
14991499
display: flex;
15001500
align-items: center;
@@ -1510,7 +1510,7 @@ $controlsArrowAngleActive: 36deg;
15101510

15111511
box-sizing: border-box;
15121512
}
1513-
.r-overlay-header .r-overlay-button:hover {
1513+
.r-overlay-header .r-overlay-header-button:hover {
15141514
opacity: 1;
15151515
background-color: rgba( 255, 255, 255, 0.15 );
15161516
}
@@ -1924,6 +1924,47 @@ $notesWidthPercent: 25%;
19241924
}
19251925

19261926

1927+
/*********************************************
1928+
* MANUAL MEDIA PLAY BUTTON
1929+
*********************************************/
1930+
1931+
.reveal .r-overlay-button {
1932+
all: unset;
1933+
position: absolute;
1934+
display: flex;
1935+
align-items: center;
1936+
justify-content: center;
1937+
padding: 10px;
1938+
border-radius: 5px;
1939+
font-size: 0.4em;
1940+
z-index: 30;
1941+
cursor: pointer;
1942+
-webkit-tap-highlight-color: rgba( 0, 0, 0, 0 );
1943+
-webkit-appearance: none;
1944+
appearance: none;
1945+
color: #fff;
1946+
background: rgba(0, 0, 0, 0.7);
1947+
1948+
&:hover {
1949+
background: rgba(0, 0, 0, 0.9);
1950+
}
1951+
}
1952+
1953+
.reveal.has-light-background .r-overlay-button {
1954+
color: #222;
1955+
background: rgba(255, 255, 255, 0.7);
1956+
1957+
&:hover {
1958+
background: rgba(255, 255, 255, 0.9);
1959+
}
1960+
}
1961+
1962+
.reveal .r-media-play-button {
1963+
left: 15px;
1964+
bottom: 20px;
1965+
}
1966+
1967+
19271968
/*********************************************
19281969
* ZOOM PLUGIN
19291970
*********************************************/
@@ -1983,7 +2024,8 @@ $notesWidthPercent: 25%;
19832024
}
19842025

19852026
.r-overlay,
1986-
.pause-overlay {
2027+
.pause-overlay,
2028+
.r-media-play-button {
19872029
position: fixed;
19882030
}
19892031

dist/reveal.css

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/reveal.esm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/reveal.esm.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/reveal.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/reveal.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/assets/video.mp4

4.43 MB
Binary file not shown.

examples/media.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ <h2 style="color: #fff;">Iframe Background</h2>
3333

3434
<section>
3535
<h2>Video</h2>
36-
<video src="https://static.slid.es/site/homepage/v1/homepage-video-editor.mp4" data-autoplay></video>
36+
<video src="assets/video.mp4" data-autoplay></video>
3737
</section>
3838

39-
<section data-background-video="https://static.slid.es/site/homepage/v1/homepage-video-editor.mp4">
39+
<section data-background-video="assets/video.mp4">
4040
<h2>Background Video</h2>
4141
</section>
4242

js/controllers/overlay.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export default class Overlay {
7272

7373
this.viewport.innerHTML =
7474
`<header class="r-overlay-header">
75-
<a class="r-overlay-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
76-
<button class="r-overlay-button r-overlay-close"><span class="icon"></span></button>
75+
<a class="r-overlay-header-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
76+
<button class="r-overlay-header-button r-overlay-close"><span class="icon"></span></button>
7777
</header>
7878
<div class="r-overlay-spinner"></div>
7979
<div class="r-overlay-content">
@@ -125,7 +125,7 @@ export default class Overlay {
125125

126126
this.viewport.innerHTML =
127127
`<header class="r-overlay-header">
128-
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
128+
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
129129
</header>
130130
<div class="r-overlay-spinner"></div>
131131
<div class="r-overlay-content"></div>`;
@@ -262,7 +262,7 @@ export default class Overlay {
262262

263263
this.viewport.innerHTML = `
264264
<header class="r-overlay-header">
265-
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
265+
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
266266
</header>
267267
<div class="r-overlay-content">
268268
<div class="r-overlay-help-content">${html}</div>

js/controllers/slidecontent.js

Lines changed: 171 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,45 @@ import fitty from 'fitty';
99
*/
1010
export default class SlideContent {
1111

12-
allowedToPlay = true;
12+
allowedToPlayAudio = null;
1313

1414
constructor( Reveal ) {
1515

1616
this.Reveal = Reveal;
1717

18+
this.startEmbeddedMedia = this.startEmbeddedMedia.bind( this );
1819
this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
1920
this.preventIframeAutoFocus = this.preventIframeAutoFocus.bind( this );
2021
this.ensureMobileMediaPlaying = this.ensureMobileMediaPlaying.bind( this );
2122

23+
this.failedAudioPlaybackTargets = new Set();
24+
this.failedVideoPlaybackTargets = new Set();
25+
this.failedMutedVideoPlaybackTargets = new Set();
26+
27+
this.renderMediaPlayButton();
28+
29+
}
30+
31+
renderMediaPlayButton() {
32+
33+
this.mediaPlayButton = document.createElement( 'button' );
34+
this.mediaPlayButton.className = 'r-overlay-button r-media-play-button';
35+
this.mediaPlayButton.addEventListener( 'click', () => {
36+
this.resetTemporarilyMutedMedia();
37+
38+
const failedTargets = new Set( [
39+
...this.failedAudioPlaybackTargets,
40+
...this.failedVideoPlaybackTargets,
41+
...this.failedMutedVideoPlaybackTargets
42+
] );
43+
44+
failedTargets.forEach( target => {
45+
this.startEmbeddedMedia( { target: target } );
46+
} );
47+
48+
this.clearMediaPlaybackErrors();
49+
} );
50+
2251
}
2352

2453
/**
@@ -146,12 +175,7 @@ export default class SlideContent {
146175
}
147176

148177
// Enable inline playback in mobile Safari
149-
//
150-
// Mute is required for video to play when using
151-
// swipe gestures to navigate since they don't
152-
// count as direct user actions :'(
153178
if( isMobile ) {
154-
video.muted = true;
155179
video.setAttribute( 'playsinline', '' );
156180
}
157181

@@ -333,26 +357,9 @@ export default class SlideContent {
333357
// Mobile devices never fire a loaded event so instead
334358
// of waiting, we initiate playback
335359
else if( isMobile ) {
336-
let promise = el.play();
337-
338360
el.addEventListener( 'canplay', this.ensureMobileMediaPlaying );
339361

340-
// If autoplay does not work, ensure that the controls are visible so
341-
// that the viewer can start the media on their own
342-
if( promise && typeof promise.catch === 'function' && el.controls === false ) {
343-
promise
344-
.then( () => {
345-
this.allowedToPlay = true;
346-
})
347-
.catch( () => {
348-
el.controls = true;
349-
350-
// Once the video does start playing, hide the controls again
351-
el.addEventListener( 'play', () => {
352-
el.controls = false;
353-
} );
354-
} );
355-
}
362+
this.playMediaElement( el );
356363
}
357364
// If the media isn't loaded, wait before playing
358365
else {
@@ -444,26 +451,63 @@ export default class SlideContent {
444451
// Don't restart if media is already playing
445452
if( event.target.paused || event.target.ended ) {
446453
event.target.currentTime = 0;
447-
const promise = event.target.play();
448-
449-
if( promise && typeof promise.catch === 'function' ) {
450-
promise
451-
.then( () => {
452-
this.allowedToPlay = true;
453-
} )
454-
.catch( ( error ) => {
455-
if( error.name === 'NotAllowedError' ) {
456-
this.allowedToPlay = false;
457-
}
458-
} );
459-
}
454+
this.playMediaElement( event.target );
460455
}
461456
}
462457

463458
event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
464459

465460
}
466461

462+
/**
463+
* Plays the given HTMLMediaElement and handles any playback
464+
* errors, such as the browser not allowing audio to play without
465+
* user action.
466+
*
467+
* @param {HTMLElement} mediaElement
468+
*/
469+
playMediaElement( mediaElement ) {
470+
471+
const promise = mediaElement.play();
472+
473+
if( promise && typeof promise.catch === 'function' ) {
474+
promise
475+
.then( () => {
476+
if( !mediaElement.muted ) {
477+
this.allowedToPlayAudio = true;
478+
}
479+
} )
480+
.catch( ( error ) => {
481+
if( error.name === 'NotAllowedError' ) {
482+
this.allowedToPlayAudio = false;
483+
484+
// If this is a video, we record the error and try to play it
485+
// muted as a fallback. The user will be presented with an unmute
486+
// button.
487+
if( mediaElement.tagName === 'VIDEO' ) {
488+
this.onVideoPlaybackNotAllowed( mediaElement );
489+
490+
let isAttachedToDOM = !!closest( mediaElement, 'html' ),
491+
isVisible = !!closest( mediaElement, '.present' ),
492+
isMuted = mediaElement.muted;
493+
494+
if( isAttachedToDOM && isVisible && !isMuted ) {
495+
mediaElement.setAttribute( 'data-muted-by-reveal', 'true' );
496+
mediaElement.muted = true;
497+
mediaElement.play().catch(() => {
498+
this.onMutedVideoPlaybackNotAllowed( mediaElement );
499+
});
500+
}
501+
}
502+
else if( mediaElement.tagName === 'AUDIO' ) {
503+
this.onAudioPlaybackNotAllowed( mediaElement );
504+
}
505+
}
506+
} );
507+
}
508+
509+
}
510+
467511
/**
468512
* "Starts" the content of an embedded iframe using the
469513
* postMessage API.
@@ -576,9 +620,91 @@ export default class SlideContent {
576620
* typically happens when media playback is initiated without a
577621
* direct user interaction.
578622
*/
579-
isNotAllowedToPlay() {
623+
isAllowedToPlayAudio() {
580624

581-
return !this.allowedToPlay;
625+
return this.allowedToPlayAudio;
626+
627+
}
628+
629+
/**
630+
* Shows a manual button in situations where autoamtic media playback
631+
* is not allowed by the browser.
632+
*/
633+
showPlayOrUnmuteButton() {
634+
635+
const audioTargets = this.failedAudioPlaybackTargets.size;
636+
const videoTargets = this.failedVideoPlaybackTargets.size;
637+
const mutedVideoTargets = this.failedMutedVideoPlaybackTargets.size;
638+
639+
let label = 'Play media';
640+
641+
if( mutedVideoTargets > 0 ) {
642+
label = 'Play video';
643+
}
644+
else if( videoTargets > 0 ) {
645+
label = 'Unmute video';
646+
}
647+
else if( audioTargets > 0 ) {
648+
label = 'Play audio';
649+
}
650+
651+
this.mediaPlayButton.textContent = label;
652+
653+
this.Reveal.getRevealElement().appendChild( this.mediaPlayButton );
654+
655+
}
656+
657+
onAudioPlaybackNotAllowed( target ) {
658+
659+
this.failedAudioPlaybackTargets.add( target );
660+
this.showPlayOrUnmuteButton( target );
661+
662+
}
663+
664+
onVideoPlaybackNotAllowed( target ) {
665+
666+
this.failedVideoPlaybackTargets.add( target );
667+
this.showPlayOrUnmuteButton();
668+
669+
}
670+
671+
onMutedVideoPlaybackNotAllowed( target ) {
672+
673+
this.failedMutedVideoPlaybackTargets.add( target );
674+
this.showPlayOrUnmuteButton();
675+
676+
}
677+
678+
/**
679+
* Videos may be temporarily muted by us to get around browser
680+
* restrictions on automatic playback. This method rolls back
681+
* all such temporary audio changes.
682+
*/
683+
resetTemporarilyMutedMedia() {
684+
685+
const failedTargets = new Set( [
686+
...this.failedAudioPlaybackTargets,
687+
...this.failedVideoPlaybackTargets,
688+
...this.failedMutedVideoPlaybackTargets
689+
] );
690+
691+
failedTargets.forEach( target => {
692+
if( target.hasAttribute( 'data-muted-by-reveal' ) ) {
693+
target.muted = false;
694+
target.removeAttribute( 'data-muted-by-reveal' );
695+
}
696+
} );
697+
698+
}
699+
700+
clearMediaPlaybackErrors() {
701+
702+
this.resetTemporarilyMutedMedia();
703+
704+
this.failedAudioPlaybackTargets.clear();
705+
this.failedVideoPlaybackTargets.clear();
706+
this.failedMutedVideoPlaybackTargets.clear();
707+
this.mediaPlayButton.remove();
582708

583709
}
584710

@@ -591,8 +717,6 @@ export default class SlideContent {
591717

592718
const iframe = event.target;
593719

594-
console.log(111)
595-
596720
if( iframe && this.Reveal.getConfig().preventIframeAutoFocus ) {
597721

598722
let elapsed = 0;
@@ -613,4 +737,10 @@ export default class SlideContent {
613737

614738
}
615739

740+
afterSlideChanged() {
741+
742+
this.clearMediaPlaybackErrors();
743+
744+
}
745+
616746
}

0 commit comments

Comments
 (0)