update packages and add valign
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util.js'
|
||||
import { isMobile } from '../utils/device.js'
|
||||
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util'
|
||||
import { isMobile } from '../utils/device'
|
||||
|
||||
import fitty from 'fitty';
|
||||
|
||||
@@ -9,11 +9,44 @@ import fitty from 'fitty';
|
||||
*/
|
||||
export default class SlideContent {
|
||||
|
||||
allowedToPlayAudio = null;
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.startEmbeddedMedia = this.startEmbeddedMedia.bind( this );
|
||||
this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
|
||||
this.preventIframeAutoFocus = this.preventIframeAutoFocus.bind( this );
|
||||
this.ensureMobileMediaPlaying = this.ensureMobileMediaPlaying.bind( this );
|
||||
|
||||
this.failedAudioPlaybackTargets = new Set();
|
||||
this.failedVideoPlaybackTargets = new Set();
|
||||
this.failedMutedVideoPlaybackTargets = new Set();
|
||||
|
||||
this.renderMediaPlayButton();
|
||||
|
||||
}
|
||||
|
||||
renderMediaPlayButton() {
|
||||
|
||||
this.mediaPlayButton = document.createElement( 'button' );
|
||||
this.mediaPlayButton.className = 'r-overlay-button r-media-play-button';
|
||||
this.mediaPlayButton.addEventListener( 'click', () => {
|
||||
this.resetTemporarilyMutedMedia();
|
||||
|
||||
const failedTargets = new Set( [
|
||||
...this.failedAudioPlaybackTargets,
|
||||
...this.failedVideoPlaybackTargets,
|
||||
...this.failedMutedVideoPlaybackTargets
|
||||
] );
|
||||
|
||||
failedTargets.forEach( target => {
|
||||
this.startEmbeddedMedia( { target: target } );
|
||||
} );
|
||||
|
||||
this.clearMediaPlaybackErrors();
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
@@ -51,14 +84,25 @@ export default class SlideContent {
|
||||
load( slide, options = {} ) {
|
||||
|
||||
// Show the slide element
|
||||
slide.style.display = this.Reveal.getConfig().display;
|
||||
const displayValue = this.Reveal.getConfig().display;
|
||||
if( displayValue.includes('!important') ) {
|
||||
const value = displayValue.replace(/\s*!important\s*$/, '').trim();
|
||||
slide.style.setProperty('display', value, 'important');
|
||||
} else {
|
||||
slide.style.display = displayValue;
|
||||
}
|
||||
|
||||
// Media elements with data-src attributes
|
||||
// Media and iframe elements with data-src attributes
|
||||
queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {
|
||||
if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
|
||||
const isIframe = element.tagName === 'IFRAME';
|
||||
if( !isIframe || this.shouldPreload( element ) ) {
|
||||
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
|
||||
element.setAttribute( 'data-lazy-loaded', '' );
|
||||
element.removeAttribute( 'data-src' );
|
||||
|
||||
if( isIframe ) {
|
||||
element.addEventListener( 'load', this.preventIframeAutoFocus );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
@@ -131,12 +175,7 @@ export default class SlideContent {
|
||||
}
|
||||
|
||||
// Enable inline playback in mobile Safari
|
||||
//
|
||||
// Mute is required for video to play when using
|
||||
// swipe gestures to navigate since they don't
|
||||
// count as direct user actions :'(
|
||||
if( isMobile ) {
|
||||
video.muted = true;
|
||||
video.setAttribute( 'playsinline', '' );
|
||||
}
|
||||
|
||||
@@ -318,20 +357,9 @@ export default class SlideContent {
|
||||
// Mobile devices never fire a loaded event so instead
|
||||
// of waiting, we initiate playback
|
||||
else if( isMobile ) {
|
||||
let promise = el.play();
|
||||
el.addEventListener( 'canplay', this.ensureMobileMediaPlaying );
|
||||
|
||||
// If autoplay does not work, ensure that the controls are visible so
|
||||
// that the viewer can start the media on their own
|
||||
if( promise && typeof promise.catch === 'function' && el.controls === false ) {
|
||||
promise.catch( () => {
|
||||
el.controls = true;
|
||||
|
||||
// Once the video does start playing, hide the controls again
|
||||
el.addEventListener( 'play', () => {
|
||||
el.controls = false;
|
||||
} );
|
||||
} );
|
||||
}
|
||||
this.playMediaElement( el );
|
||||
}
|
||||
// If the media isn't loaded, wait before playing
|
||||
else {
|
||||
@@ -374,6 +402,40 @@ export default class SlideContent {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an HTMLMediaElement is playing on mobile devices.
|
||||
*
|
||||
* This is a workaround for a bug in mobile Safari where
|
||||
* the media fails to display if many videos are started
|
||||
* at the same moment. When this happens, Mobile Safari
|
||||
* reports the video is playing, and the current time
|
||||
* advances, but nothing is visible.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
ensureMobileMediaPlaying( event ) {
|
||||
|
||||
const el = event.target;
|
||||
|
||||
// Ignore this check incompatible browsers
|
||||
if( typeof el.getVideoPlaybackQuality !== 'function' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout( () => {
|
||||
|
||||
const playing = el.paused === false;
|
||||
const totalFrames = el.getVideoPlaybackQuality().totalVideoFrames;
|
||||
|
||||
if( playing && totalFrames === 0 ) {
|
||||
el.load();
|
||||
el.play();
|
||||
}
|
||||
|
||||
}, 1000 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playing an embedded video/audio element after
|
||||
* it has finished loading.
|
||||
@@ -389,7 +451,7 @@ export default class SlideContent {
|
||||
// Don't restart if media is already playing
|
||||
if( event.target.paused || event.target.ended ) {
|
||||
event.target.currentTime = 0;
|
||||
event.target.play();
|
||||
this.playMediaElement( event.target );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +459,55 @@ export default class SlideContent {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the given HTMLMediaElement and handles any playback
|
||||
* errors, such as the browser not allowing audio to play without
|
||||
* user action.
|
||||
*
|
||||
* @param {HTMLElement} mediaElement
|
||||
*/
|
||||
playMediaElement( mediaElement ) {
|
||||
|
||||
const promise = mediaElement.play();
|
||||
|
||||
if( promise && typeof promise.catch === 'function' ) {
|
||||
promise
|
||||
.then( () => {
|
||||
if( !mediaElement.muted ) {
|
||||
this.allowedToPlayAudio = true;
|
||||
}
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
if( error.name === 'NotAllowedError' ) {
|
||||
this.allowedToPlayAudio = false;
|
||||
|
||||
// If this is a video, we record the error and try to play it
|
||||
// muted as a fallback. The user will be presented with an unmute
|
||||
// button.
|
||||
if( mediaElement.tagName === 'VIDEO' ) {
|
||||
this.onVideoPlaybackNotAllowed( mediaElement );
|
||||
|
||||
let isAttachedToDOM = !!closest( mediaElement, 'html' ),
|
||||
isVisible = !!closest( mediaElement, '.present' ),
|
||||
isMuted = mediaElement.muted;
|
||||
|
||||
if( isAttachedToDOM && isVisible && !isMuted ) {
|
||||
mediaElement.setAttribute( 'data-muted-by-reveal', 'true' );
|
||||
mediaElement.muted = true;
|
||||
mediaElement.play().catch(() => {
|
||||
this.onMutedVideoPlaybackNotAllowed( mediaElement );
|
||||
});
|
||||
}
|
||||
}
|
||||
else if( mediaElement.tagName === 'AUDIO' ) {
|
||||
this.onAudioPlaybackNotAllowed( mediaElement );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* "Starts" the content of an embedded iframe using the
|
||||
* postMessage API.
|
||||
@@ -407,6 +518,8 @@ export default class SlideContent {
|
||||
|
||||
let iframe = event.target;
|
||||
|
||||
this.preventIframeAutoFocus( event );
|
||||
|
||||
if( iframe && iframe.contentWindow ) {
|
||||
|
||||
let isAttachedToDOM = !!closest( event.target, 'html' ),
|
||||
@@ -461,12 +574,17 @@ export default class SlideContent {
|
||||
if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
|
||||
el.setAttribute('data-paused-by-reveal', '');
|
||||
el.pause();
|
||||
|
||||
if( isMobile ) {
|
||||
el.removeEventListener( 'canplay', this.ensureMobileMediaPlaying );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
// Generic postMessage API for non-lazy loaded iframes
|
||||
queryAll( element, 'iframe' ).forEach( el => {
|
||||
if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
|
||||
el.removeEventListener( 'load', this.preventIframeAutoFocus );
|
||||
el.removeEventListener( 'load', this.startEmbeddedIframe );
|
||||
});
|
||||
|
||||
@@ -497,4 +615,132 @@ export default class SlideContent {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether media playback is blocked by the browser. This
|
||||
* typically happens when media playback is initiated without a
|
||||
* direct user interaction.
|
||||
*/
|
||||
isAllowedToPlayAudio() {
|
||||
|
||||
return this.allowedToPlayAudio;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a manual button in situations where autoamtic media playback
|
||||
* is not allowed by the browser.
|
||||
*/
|
||||
showPlayOrUnmuteButton() {
|
||||
|
||||
const audioTargets = this.failedAudioPlaybackTargets.size;
|
||||
const videoTargets = this.failedVideoPlaybackTargets.size;
|
||||
const mutedVideoTargets = this.failedMutedVideoPlaybackTargets.size;
|
||||
|
||||
let label = 'Play media';
|
||||
|
||||
if( mutedVideoTargets > 0 ) {
|
||||
label = 'Play video';
|
||||
}
|
||||
else if( videoTargets > 0 ) {
|
||||
label = 'Unmute video';
|
||||
}
|
||||
else if( audioTargets > 0 ) {
|
||||
label = 'Play audio';
|
||||
}
|
||||
|
||||
this.mediaPlayButton.textContent = label;
|
||||
|
||||
this.Reveal.getRevealElement().appendChild( this.mediaPlayButton );
|
||||
|
||||
}
|
||||
|
||||
onAudioPlaybackNotAllowed( target ) {
|
||||
|
||||
this.failedAudioPlaybackTargets.add( target );
|
||||
this.showPlayOrUnmuteButton( target );
|
||||
|
||||
}
|
||||
|
||||
onVideoPlaybackNotAllowed( target ) {
|
||||
|
||||
this.failedVideoPlaybackTargets.add( target );
|
||||
this.showPlayOrUnmuteButton();
|
||||
|
||||
}
|
||||
|
||||
onMutedVideoPlaybackNotAllowed( target ) {
|
||||
|
||||
this.failedMutedVideoPlaybackTargets.add( target );
|
||||
this.showPlayOrUnmuteButton();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Videos may be temporarily muted by us to get around browser
|
||||
* restrictions on automatic playback. This method rolls back
|
||||
* all such temporary audio changes.
|
||||
*/
|
||||
resetTemporarilyMutedMedia() {
|
||||
|
||||
const failedTargets = new Set( [
|
||||
...this.failedAudioPlaybackTargets,
|
||||
...this.failedVideoPlaybackTargets,
|
||||
...this.failedMutedVideoPlaybackTargets
|
||||
] );
|
||||
|
||||
failedTargets.forEach( target => {
|
||||
if( target.hasAttribute( 'data-muted-by-reveal' ) ) {
|
||||
target.muted = false;
|
||||
target.removeAttribute( 'data-muted-by-reveal' );
|
||||
}
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
clearMediaPlaybackErrors() {
|
||||
|
||||
this.resetTemporarilyMutedMedia();
|
||||
|
||||
this.failedAudioPlaybackTargets.clear();
|
||||
this.failedVideoPlaybackTargets.clear();
|
||||
this.failedMutedVideoPlaybackTargets.clear();
|
||||
this.mediaPlayButton.remove();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents iframes from automatically focusing themselves.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
preventIframeAutoFocus( event ) {
|
||||
|
||||
const iframe = event.target;
|
||||
|
||||
if( iframe && this.Reveal.getConfig().preventIframeAutoFocus ) {
|
||||
|
||||
let elapsed = 0;
|
||||
const interval = 100;
|
||||
const maxTime = 1000;
|
||||
const checkFocus = () => {
|
||||
if( document.activeElement === iframe ) {
|
||||
document.activeElement.blur();
|
||||
} else if( elapsed < maxTime ) {
|
||||
elapsed += interval;
|
||||
setTimeout( checkFocus, interval );
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout( checkFocus, interval );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
afterSlideChanged() {
|
||||
|
||||
this.clearMediaPlaybackErrors();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user