@@ -9,16 +9,45 @@ import fitty from 'fitty';
99 */
1010export 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