diff --git a/packages/core/docs/playerContext.md b/packages/core/docs/playerContext.md index 8fdcf007..d357142d 100644 --- a/packages/core/docs/playerContext.md +++ b/packages/core/docs/playerContext.md @@ -18,6 +18,7 @@ | muted | A boolean showing whether the track volume is currently muted | | shuffle | A boolean showing whether the track is currently playing tracks in shuffle order | | stalled | A boolean showing whether the track playback is currently stalled due to network issues | +| mediaCannotPlay | A boolean showing whether the track playback failed while fetching the media data or if the type of the resource is not supported media format. | | playbackRate | A number showing the current rate of playback (1 is normal) | | setVolumeInProgress | A boolean showing whether the volume is currently being adjusted | | repeatStrategy | A value that is either "track", "playlist" or "none". Tells whether the playlist repeats at the playlist level, the track level, or none (playback stops at the end of the playlist). | diff --git a/packages/core/src/PlayerContext.js b/packages/core/src/PlayerContext.js index 14bfdda1..6f6925ed 100644 --- a/packages/core/src/PlayerContext.js +++ b/packages/core/src/PlayerContext.js @@ -24,6 +24,7 @@ export default createSingleGlobalContext({ 'stalled', 'playbackRate', 'setVolumeInProgress', - 'repeatStrategy' + 'repeatStrategy', + 'mediaCannotPlay' ] }); diff --git a/packages/core/src/PlayerContextProvider.js b/packages/core/src/PlayerContextProvider.js index c5369b61..8e5bacd7 100644 --- a/packages/core/src/PlayerContextProvider.js +++ b/packages/core/src/PlayerContextProvider.js @@ -67,7 +67,11 @@ const defaultState = { // true if the media is currently stalled pending data buffering stalled: false, // true if the active track should play on the next componentDidUpdate - awaitingPlay: false + awaitingPlay: false, + /* true if an error occurs while fetching the active track media data + * or if its type is not a supported media format + */ + mediaCannotPlay: false }; // assumes playlist is valid @@ -81,6 +85,8 @@ function getGoToTrackState({ return { activeTrackIndex: index, trackLoading: isNewTrack, + mediaCannotPlay: + prevState.mediaCannotPlay && !shouldForceLoad && !isNewTrack, currentTime: 0, loop: isNewTrack || shouldForceLoad ? false : prevState.loop, awaitingPlay: Boolean(shouldPlay), @@ -89,25 +95,6 @@ function getGoToTrackState({ }; } -function setMediaElementSources(mediaElement, sources) { - // remove current sources - let firstChild; - while ((firstChild = mediaElement.firstChild)) { - mediaElement.removeChild(firstChild); - } - // add new sources - for (const source of sources) { - const sourceElement = document.createElement('source'); - sourceElement.src = source.src; - if (source.type) { - sourceElement.type = source.type; - } - mediaElement.appendChild(sourceElement); - } - // cancel playback and re-scan new sources - mediaElement.load(); -} - /** * Wraps an area which shares a common [`playerContext`](#playercontext) */ @@ -166,6 +153,9 @@ export class PlayerContextProvider extends Component { this.videoHostOccupiedCallbacks = new Map(); this.videoHostVacatedCallbacks = new Map(); + // bind internal methods + this.onTrackPlaybackFailure = this.onTrackPlaybackFailure.bind(this); + // bind callback methods to pass to descendant elements this.togglePause = this.togglePause.bind(this); this.selectTrackIndex = this.selectTrackIndex.bind(this); @@ -266,7 +256,7 @@ export class PlayerContextProvider extends Component { media.addEventListener('loopchange', this.handleMediaLoopchange); // set source elements for current track - setMediaElementSources(media, getTrackSources(playlist, activeTrackIndex)); + this.setMediaElementSources(getTrackSources(playlist, activeTrackIndex)); // initially mount media element in the hidden container (this may change) this.mediaContainer.appendChild(media); @@ -345,7 +335,8 @@ export class PlayerContextProvider extends Component { // if not, then load the first track in the new playlist, and pause. return { ...baseNewState, - ...getGoToTrackState({ prevState, index: 0, shouldPlay: false }) + ...getGoToTrackState({ prevState, index: 0, shouldPlay: false }), + mediaCannotPlay: false }; } @@ -372,7 +363,7 @@ export class PlayerContextProvider extends Component { this.state.awaitingForceLoad || prevSources[0].src !== newSources[0].src ) { - setMediaElementSources(this.media, newSources); + this.setMediaElementSources(newSources); this.media.setAttribute( 'poster', this.props.getPosterImageForTrack(newTrack) @@ -452,6 +443,11 @@ export class PlayerContextProvider extends Component { // remove special event listeners on the media element media.removeEventListener('srcrequest', this.handleMediaSrcrequest); media.removeEventListener('loopchange', this.handleMediaLoopchange); + + const sourceElements = media.querySelectorAll('source'); + for (const sourceElement of sourceElements) { + sourceElement.removeEventListener('error', this.onTrackPlaybackFailure); + } } clearTimeout(this.gapLengthTimeout); clearTimeout(this.delayTimeout); @@ -499,6 +495,39 @@ export class PlayerContextProvider extends Component { }); } + setMediaElementSources(sources) { + // remove current sources + let firstChild; + while ((firstChild = this.media.firstChild)) { + this.media.removeChild(firstChild); + } + // add new sources + for (const source of sources) { + const sourceElement = document.createElement('source'); + sourceElement.src = source.src; + if (source.type) { + sourceElement.type = source.type; + } + sourceElement.addEventListener('error', this.onTrackPlaybackFailure); + this.media.appendChild(sourceElement); + } + // cancel playback and re-scan new sources + this.media.load(); + } + + onTrackPlaybackFailure(event) { + this.setState({ + mediaCannotPlay: true + }); + if (this.props.onTrackPlaybackFailure) { + this.props.onTrackPlaybackFailure( + this.props.playlist[this.state.activeTrackIndex], + this.state.activeTrackIndex, + event + ); + } + } + registerVideoHostElement(hostElement, { onHostOccupied, onHostVacated }) { this.videoHostElementList = this.videoHostElementList.concat(hostElement); this.videoHostOccupiedCallbacks.set(hostElement, onHostOccupied); @@ -942,6 +971,7 @@ export class PlayerContextProvider extends Component { shuffle: state.shuffle, stalled: state.stalled, playbackRate: state.playbackRate, + mediaCannotPlay: state.mediaCannotPlay, setVolumeInProgress: state.setVolumeInProgress, repeatStrategy: getRepeatStrategy(state.loop, state.cycle), registerVideoHostElement: this.registerVideoHostElement, @@ -1019,6 +1049,7 @@ PlayerContextProvider.propTypes = { }), onStateSnapshot: PropTypes.func, onActiveTrackUpdate: PropTypes.func, + onTrackPlaybackFailure: PropTypes.func, getPosterImageForTrack: PropTypes.func.isRequired, getMediaTitleAttributeForTrack: PropTypes.func.isRequired, children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired