Skip to content

Commit

Permalink
Merge pull request #628 from samvera-labs/loop-playback-624
Browse files Browse the repository at this point in the history
Fix player replaying last section/single section item in a loop
  • Loading branch information
Dananji authored Aug 29, 2024
2 parents df6d816 + 3c00543 commit f039359
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 68 deletions.
132 changes: 73 additions & 59 deletions src/components/MediaPlayer/VideoJS/VideoJSPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,21 @@ function VideoJSPlayer({
const player = playerRef.current;

// Block player while metadata is loaded when canvas is not empty
if (!canvasIsEmptyRef.current) player.addClass('vjs-disabled');
if (!canvasIsEmptyRef.current) {
player.addClass('vjs-disabled');

setIsReady(false);
updatePlayer(player);
playerLoadedMetadata(player);
setIsReady(false);
updatePlayer(player);
playerLoadedMetadata(player);

playerDispatch({
player: player,
type: 'updatePlayer',
});
playerDispatch({
player: player,
type: 'updatePlayer',
});
} else {
// Mark as ready to for inaccessible canvas (empty)
setIsReady(true);
}
}
}, [options.sources, videoJSRef]);

Expand Down Expand Up @@ -792,66 +797,75 @@ function VideoJSPlayer({
* (not the next item in list) when the current item is coming to its end.
*/
const handleEnded = React.useMemo(() => throttle(() => {
if (!autoAdvanceRef.current && !hasMultiItems || canvasIsEmptyRef.current) {
const isLastCanvas = cIndexRef.current === lastCanvasIndex;
/**
* Do nothing if Canvas is not multi-sourced AND autoAdvance is turned off
* OR current Canvas is the last Canvas in the Manifest
*/
if ((!autoAdvanceRef.current || isLastCanvas) && !hasMultiItems) {
return;
}

// Remove all the existing structure related markers in the player
if (playerRef.current && playerRef.current.markers) {
playerRef.current.pause();
playerRef.current.markers.removeAll();
}
if (hasMultiItems) {
// When there are multiple sources in a single canvas
// advance to next source
if (srcIndex + 1 < targets.length) {
manifestDispatch({ srcIndex: srcIndex + 1, type: 'setSrcIndex' });
playerDispatch({ currentTime: 0, type: 'setCurrentTime' });
} else {
// Remove all the existing structure related markers in the player
if (playerRef.current && playerRef.current.markers) {
playerRef.current.pause();
playerRef.current.markers.removeAll();
}
} else if (structuresRef.current?.length > 0) {
const nextItem = structuresRef.current[cIndexRef.current + 1];

if (nextItem && nextItem != undefined) {
manifestDispatch({
canvasIndex: cIndexRef.current + 1,
type: 'switchCanvas',
});

// Reset startTime and currentTime to zero
playerDispatch({ startTime: 0, type: 'setTimeFragment' });
playerDispatch({ currentTime: 0, type: 'setCurrentTime' });

// Get first timespan in the next canvas
let firstTimespanInNextCanvas = canvasSegmentsRef.current.filter(
(t) => t.canvasIndex === nextItem.canvasIndex && t.itemIndex === 1
);
// If the nextItem doesn't have an ID (a Canvas media fragment) pick the first timespan
// in the next Canvas
let nextFirstItem = nextItem.id != undefined ? nextItem : firstTimespanInNextCanvas[0];

let start = 0;
if (nextFirstItem != undefined && nextFirstItem.id != undefined) {
start = getMediaFragment(nextFirstItem.id, canvasDurationRef.current).start;
if (hasMultiItems) {
// When there are multiple sources in a single canvas
// advance to next source
if (srcIndex + 1 < targets.length) {
manifestDispatch({ srcIndex: srcIndex + 1, type: 'setSrcIndex' });
playerDispatch({ currentTime: 0, type: 'setCurrentTime' });
playerRef.current.play();
} else {
return;
}
} else if (structuresRef.current?.length > 0) {
const nextItem = structuresRef.current[cIndexRef.current + 1];

// If there's a timespan item at the start of the next canvas
// mark it as the currentNavItem. Otherwise empty out the currentNavItem.
if (start === 0) {
if (nextItem) {
manifestDispatch({
item: nextFirstItem,
type: 'switchItem',
canvasIndex: cIndexRef.current + 1,
type: 'switchCanvas',
});
} else if (nextFirstItem.isEmpty) {
// Switch the currentNavItem and clear isEnded flag
manifestDispatch({
item: nextFirstItem,
type: 'switchItem',
});
playerRef.current.currentTime(start);

// Reset startTime and currentTime to zero
playerDispatch({ startTime: 0, type: 'setTimeFragment' });
playerDispatch({ currentTime: 0, type: 'setCurrentTime' });

// Get first timespan in the next canvas
let firstTimespanInNextCanvas = canvasSegmentsRef.current.filter(
(t) => t.canvasIndex === nextItem.canvasIndex && t.itemIndex === 1
);
// If the nextItem doesn't have an ID (a Canvas media fragment) pick the first timespan
// in the next Canvas
let nextFirstItem = nextItem.id != undefined ? nextItem : firstTimespanInNextCanvas[0];

let start = 0;
if (nextFirstItem != undefined && nextFirstItem.id != undefined) {
start = getMediaFragment(nextFirstItem.id, canvasDurationRef.current).start;
}

// If there's a timespan item at the start of the next canvas
// mark it as the currentNavItem. Otherwise empty out the currentNavItem.
if (start === 0) {
manifestDispatch({
item: nextFirstItem,
type: 'switchItem',
});
} else if (nextFirstItem.isEmpty) {
// Switch the currentNavItem and clear isEnded flag
manifestDispatch({
item: nextFirstItem,
type: 'switchItem',
});
playerRef.current.currentTime(start);
// Only play if the next item is not an inaccessible item
if (!nextItem.isEmpty) playerRef.current.play();
}
}
}
}
playerRef.current.play();
}), [cIndexRef.current]);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class VideoJSProgress extends vjsComponent {
handleTimeUpdate(curTime) {
const { player, options, el_ } = this;
const { srcIndex, targets } = options;
const { start, end } = targets[srcIndex];
const { start, end, duration } = targets[srcIndex];

// Avoid null player instance when Video.js is getting initialized
if (!el_ || !player) {
Expand All @@ -114,14 +114,17 @@ class VideoJSProgress extends vjsComponent {
if (curTime < start) {
player.currentTime(start);
}
// Some items, particularly in playlists, were not having `player.ended()` properly
// set by the 'ended' event. Providing a fallback check that the player is already
// paused prevents undesirable behavior from excess state changes after play ending.
// Some items, particularly in playlists, were not triggering `player.ended()` event as expected.
// This code block acts as a fallback when that happens. Player is momentarily paused to prevent
// crashing the player when it is in a transient state while switching canvases.
if (curTime >= end && !player.paused() && !player.isDisposed()) {
if (nextItems.length == 0) { options.nextItemClicked(0, targets[0].start); }
player.pause();
player.trigger('ended');

// Pause when playable range < duration of the full media. e.g. clipped playlist items
if (end < duration) {
player.pause();
}
// Delay ended event so that, it fires after pause and display replay icon instead of play/pause
this.setTimeout(() => { player.trigger('ended'); }, 10);

// On the next play event set the time to start or a seeked time
// in between the 'ended' event and 'play' event
Expand Down Expand Up @@ -443,13 +446,13 @@ function ProgressBar({
* Set start values for progress bar
* @param {Number} start canvas start time
*/
const initializeProgress = (start) => {
const initializeProgress = (start) => {
setProgress(start);
setInitTime(start);

setCurrentTime(start);
player.currentTime(start);
}
};

/**
* Set progress and player time when using the input range
Expand Down

0 comments on commit f039359

Please sign in to comment.