Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix playback and transcript scroll for mobile/tablet devices #369

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/MediaPlayer/MediaPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../../context/player-context';
import { useErrorBoundary } from "react-error-boundary";
import './MediaPlayer.scss';
import { IS_MOBILE, IS_IPAD } from '@Services/browser';

const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => {
const manifestState = useManifestState();
Expand Down Expand Up @@ -281,6 +282,10 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => {
aspectRatio: isVideo ? '16:9' : '1:0',
autoplay: false,
bigPlayButton: isVideo,
// Setting inactivity timeout to zero in mobile and tablet devices translates to
// user is always active. And the control bar is not hidden when user is active.
// With this user can always use the controls when the media is playing.
inactivityTimeout: (IS_MOBILE || IS_IPAD) ? 0 : 2000,
poster: isVideo ? getPlaceholderCanvas(manifest, canvasIndex, true) : null,
controls: true,
fluid: true,
Expand Down
6 changes: 6 additions & 0 deletions src/components/MediaPlayer/VideoJS/VideoJSPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getCanvasIndex,
} from '@Services/iiif-parser';
import { checkSrcRange, getMediaFragment, playerHotKeys } from '@Services/utility-helpers';
import { IS_IPAD, IS_MOBILE } from '@Services/browser';

/** VideoJS custom components */
import VideoJSProgress from './components/js/VideoJSProgress';
Expand Down Expand Up @@ -175,6 +176,11 @@ function VideoJSPlayer({
if (!isVideo) {
player.getChild('controlBar').getChild('VolumePanel').addClass('vjs-slider-active');
}
// Add this class in mobile/tablet devices to always show the control bar,
// since the inactivityTimeout is flaky in some browsers
if (IS_MOBILE || IS_IPAD) {
player.controlBar.addClass('vjs-mobile-visible');
}
});
player.on('ended', () => {
playerDispatch({ isEnded: true, type: 'setIsEnded' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function CurrentTimeDisplay({ player, options }) {
* because Safari stops firing the timeupdate event consistently while it works
* with other browsers.
*/
player.on('play', () => {
player.on('loadedmetadata', () => {
playerEventListener = setInterval(() => {
handleTimeUpdate();
}, 100);
Expand Down
56 changes: 30 additions & 26 deletions src/components/MediaPlayer/VideoJS/components/js/VideoJSProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import videojs from 'video.js';
import '../styles/VideoJSProgress.scss';
import { IS_MOBILE, IS_IPAD } from '@Services/browser';

const vjsComponent = videojs.getComponent('Component');

Expand Down Expand Up @@ -34,7 +35,7 @@ class VideoJSProgress extends vjsComponent {
this.state = { startTime: null, endTime: null };
this.times = options.targets[options.srcIndex];

player.on('loadedmetadata', () => {
player.ready(() => {
this.mount();
this.setTimes();
this.initProgressBar();
Expand Down Expand Up @@ -229,6 +230,15 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
setProgress(curTime);
setCurrentTime(curTime + targets[srcIndex].altStart);

/**
* Using a time interval instead of 'timeupdate event in VideoJS, because Safari
* and other browsers in MacOS stops firing the 'timeupdate' event consistently
* after a while
*/
playerEventListener = setInterval(() => {
timeUpdateHandler();
}, 100);

// Get the pixel ratio for the range
const ratio = sliderRangeRef.current.offsetWidth / (end - start);

Expand All @@ -244,6 +254,10 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
if (sliderIndex < srcIndex) leftWidth += slider.offsetWidth;
}

// Hide the timetooltip on mobile/tablet devices
if (IS_IPAD || IS_MOBILE) {
timeToolRef.current.style.display = 'none';
}
timeToolRef.current.style.left =
leftWidth - timeToolRef.current.offsetWidth / 2 + 'px';
});
Expand Down Expand Up @@ -276,28 +290,6 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
}
});

/**
* Using play event with a time interval instead of timeupdate event in VideoJS,
* because Safari stops firing the timeupdate event consistently while it works
* with other browsers.
*/
player.on('play', () => {
// Start interval to listen to timeupdate with playback
playerEventListener = setInterval(() => {
timeUpdateHandler();
}, 100);
});

/**
* Update progress bar when using structured navigation and transcripts component
* to navigate to a certain timestamp when the media is paused
*/
player.on('timeupdate', () => {
if (player.paused()) {
timeUpdateHandler();
}
});

/**
* Convert mouseover event to respective time in seconds
* @param {Object} e mouseover event for input range
Expand Down Expand Up @@ -355,7 +347,9 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
const sliderIndex = slider.dataset.srcindex;
if (sliderIndex < currentSrcIndex) leftWidth += slider.offsetWidth;
}
timeToolRef.current.style.left = leftWidth + 'px';
if (e.pointerType != 'touch') {
timeToolRef.current.style.left = leftWidth + 'px';
}
};

/**
Expand Down Expand Up @@ -388,6 +382,14 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
}
};

/**
* Handle touch events on the progress bar
* @param {Object} e touch event
*/
const handleTouchEvent = (e) => {
handleMouseMove(e, false);
};

/**
* Build input ranges for the inactive source segments
* in the manifest
Expand All @@ -407,7 +409,7 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
role="slider"
data-srcindex={t.sIndex}
className="vjs-custom-progress-inactive"
onPointerMove={(e) => handleMouseMove(e, true)}
onMouseMove={(e) => handleMouseMove(e, true)}
onClick={handleClick}
key={t.sIndex}
tabIndex={0}
Expand Down Expand Up @@ -444,7 +446,9 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
data-srcindex={srcIndex}
className="vjs-custom-progress"
onChange={updateProgress}
onPointerDown={(e) => handleMouseMove(e, false)}
onTouchEnd={handleTouchEvent}
onTouchStart={handleTouchEvent}
onMouseDown={(e) => handleMouseMove(e, false)}
onPointerMove={(e) => handleMouseMove(e, false)}
id="slider-range"
ref={sliderRangeRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import videojs from 'video.js';
import '../styles/VideoJSTrackScrubber.scss';
import '../styles/VideoJSProgress.scss';
import { timeToHHmmss } from '@Services/utility-helpers';
import { IS_MOBILE, IS_IPAD } from '@Services/browser';

const vjsComponent = videojs.getComponent('Component');

Expand Down Expand Up @@ -101,6 +102,15 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef }) {
_setCurrentTrack(t);
};

let playerEventListener;

// Clean up interval on component unmount
React.useEffect(() => {
return () => {
clearInterval(playerEventListener);
};
}, []);

/**
* Keydown event handler for the track button on the player controls,
* when using keyboard navigation
Expand Down Expand Up @@ -138,28 +148,58 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef }) {
populateTrackScrubber();
trackScrubberRef.current.classList.remove('hidden');

let pointerDragged = false;
// Attach mouse pointer events to track scrubber progress bar
let [_, progressBar, __] = trackScrubberRef.current.children;
progressBar.addEventListener('mouseenter', (e) => {
handleMouseMove(e);
});
progressBar.addEventListener('mousemove', (e) => {
/*
Using pointerup, pointermove, pointerdown events instead of
mouseup, mousemove, mousedown events to make it work with both
mouse pointer and touch events
*/
progressBar.addEventListener('pointerup', (e) => {
if (pointerDragged) {
handleSetProgress(e);
}
});
progressBar.addEventListener('pointermove', (e) => {
handleMouseMove(e);
pointerDragged = true;
});
progressBar.addEventListener('mousedown', (e) => {
progressBar.addEventListener('pointerdown', (e) => {
// Only handle left click event
if (e.which === 1) {
handleSetProgress(e);
pointerDragged = false;
}
});
}
}, [zoomedOut]);

player.on('loadedmetadata', () => {
// Hide the timetooltip on mobile/tablet devices
if (IS_IPAD || IS_MOBILE) {
timeToolRef.current.style.display = 'none';
}
playerEventListener = setInterval(() => {
timeUpdateHandler();
}, 100);
});

// Hide track scrubber if it is displayed when player is going fullscreen
player.on("fullscreenchange", () => {
if (player.isFullscreen() && !zoomedOut) {
setZoomedOut(zoomedOut => !zoomedOut);
}
});

/**
* Event handler for VideoJS player instance's 'timeupdate' event, which
* updates the track scrubber from player state.
*/
player.on('timeupdate', () => {
const timeUpdateHandler = () => {
if (player.isDisposed()) return;
// Get the current track from the player.markers created from the structure timespans
if (player.markers && player.markers.getMarkers()?.length > 0) {
Expand All @@ -179,7 +219,7 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef }) {
});
}
updateTrackScrubberProgressBar(player.currentTime(), player);
});
};

/**
* Update the track scrubber's current time, duration and played percentage
Expand Down Expand Up @@ -263,21 +303,23 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef }) {
return;
}
let trackoffset = getTrackTime(e);
// Calculate percentage of the progress based on the pointer position's
// time and duration of the track
let trackpercent = Math.min(
100,
Math.max(0, 100 * trackoffset / currentTrackRef.current.duration)
);

// Set the elapsed time in the scrubber progress bar
document.documentElement.style.setProperty(
'--range-scrubber',
`calc(${trackpercent}%)`
);

// Set player's current time as addition of start time of the track and offset
player.currentTime(currentTrackRef.current.time + trackoffset);
if (trackoffset != undefined) {
// Calculate percentage of the progress based on the pointer position's
// time and duration of the track
let trackpercent = Math.min(
100,
Math.max(0, 100 * trackoffset / currentTrackRef.current.duration)
);

// Set the elapsed time in the scrubber progress bar
document.documentElement.style.setProperty(
'--range-scrubber',
`calc(${trackpercent}%)`
);
// Set player's current time as addition of start time of the track and offset
player.currentTime(currentTrackRef.current.time + trackoffset);
}
};

/**
Expand All @@ -289,17 +331,13 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef }) {
if (!currentTrackRef.current) {
return;
}
let offsetx = 0;
// Use touch position information in touch devices
if (e.changedTouches?.length > 0) {
offsetx = e.changedTouches[0].pageX;
} else {
offsetx = e.offsetX;
let offsetx = e.offsetX;
if (offsetx && offsetx != undefined) {
let time =
(offsetx / e.target.clientWidth) * currentTrackRef.current.duration
;
return time;
}
let time =
(offsetx / e.target.clientWidth) * currentTrackRef.current.duration
;
return time;
};

return (
Expand Down
Loading