diff --git a/src/components/MediaPlayer/MediaPlayer.js b/src/components/MediaPlayer/MediaPlayer.js index 3bc52b3f..7bfd7463 100644 --- a/src/components/MediaPlayer/MediaPlayer.js +++ b/src/components/MediaPlayer/MediaPlayer.js @@ -44,7 +44,7 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { playlist } = manifestState; - const { player } = playerState; + const { player, currentTime } = playerState; React.useEffect(() => { if (manifest) { @@ -106,7 +106,7 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { sources, tracks, }); - updatePlayerSrcDetails(canvas.duration, sources, isMultiSource); + updatePlayerSrcDetails(canvas.duration, sources, canvasId, isMultiSource); setIsMultiSource(isMultiSource); setCIndex(canvasId); @@ -131,10 +131,11 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { * Update contexts based on the items in the canvas(es) in manifest * @param {Number} duration canvas duration * @param {Array} sources array of sources passed into player + * @param {Number} cIndex latest canvas index * @param {Boolean} isMultiSource flag indicating whether there are * multiple items in the canvas */ - const updatePlayerSrcDetails = (duration, sources, isMultiSource) => { + const updatePlayerSrcDetails = (duration, sources, cIndex, isMultiSource) => { let timeFragment = {}; if (isMultiSource) { playerDispatch({ @@ -147,7 +148,7 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { playerDispatch({ type: 'updatePlayer' }); - const itemMessage = inaccessibleItemMessage(manifest, canvasIndex); + const itemMessage = inaccessibleItemMessage(manifest, cIndex); setPlayerConfig({ ...playerConfig, error: itemMessage @@ -228,11 +229,13 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { duration: canvasDuration, srcIndex, targets, + currentTime: currentTime || 0, nextItemClicked, }, videoJSCurrentTime: { srcIndex, targets, + currentTime: currentTime || 0, }, // make the volume slider horizontal for audio volumePanel: { inline: isVideo ? false : true }, @@ -350,7 +353,7 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { }; } - if (playlist.isPlaylist && canvasIsEmpty) { + if (canvasIsEmpty) { return ( <div data-testid="inaccessible-item" @@ -359,8 +362,8 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => { role="presentation" > <div className="ramp--no-media-message"> - <div className="message-display"> - <p>{playerConfig.error}</p> + <div className="message-display" data-testid="inaccessible-message" + dangerouslySetInnerHTML={{ __html: playerConfig.error }}> </div> <VideoJSPlayer isVideo={true} diff --git a/src/components/MediaPlayer/MediaPlayer.scss b/src/components/MediaPlayer/MediaPlayer.scss index fb7281a5..e266c8a6 100644 --- a/src/components/MediaPlayer/MediaPlayer.scss +++ b/src/components/MediaPlayer/MediaPlayer.scss @@ -1,15 +1,21 @@ +@import '../../styles/vars'; + .ramp--no-media-message { aspect-ratio: 16/9; - background-color: black; - width: 60%; + background-color: $primaryDarkest; + width: 80%; margin: auto; .message-display { - transform: translate(40%, 50%); + transform: translate(60%, 80%); position: absolute; margin: 0; color: white; width: 25%; + + a { + color: $primaryGreen; + } } #iiif-media-player { diff --git a/src/components/MediaPlayer/MediaPlayer.test.js b/src/components/MediaPlayer/MediaPlayer.test.js index d026e702..5afeac88 100644 --- a/src/components/MediaPlayer/MediaPlayer.test.js +++ b/src/components/MediaPlayer/MediaPlayer.test.js @@ -4,6 +4,7 @@ import { withManifestAndPlayerProvider } from '../../services/testing-helpers'; import MediaPlayer from './MediaPlayer'; import audioManifest from '@TestData/transcript-canvas'; import videoManifest from '@TestData/lunchroom-manners'; +import emptyCanvasManifest from '@TestData/transcript-annotation'; import playlistManifest from '@TestData/playlist'; let manifestState = { @@ -82,7 +83,7 @@ describe('MediaPlayer component', () => { }); }); - describe('with a manifest', () => { + describe('with a non-playlist manifest', () => { describe('with a single canvas', () => { test('does not render previous/next section buttons', () => { const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, { @@ -116,6 +117,25 @@ describe('MediaPlayer component', () => { expect(screen.queryByTestId('videojs-previous-button')).toBeInTheDocument(); }); }); + + test('renders a message with HTML from placeholderCanvas for empty canvas', () => { + // Stub loading HTMLMediaElement for jsdom + window.HTMLMediaElement.prototype.load = () => { }; + + const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, { + initialManifestState: { + manifest: emptyCanvasManifest, + canvasIndex: 1, + playlist: { isPlaylist: false } + }, + initialPlayerState: {}, + }); + render(<PlayerWithManifest />); + expect(screen.queryByTestId('inaccessible-item')).toBeInTheDocument(); + expect(screen.getByTestId('inaccessible-message').textContent) + .toEqual('You do not have permission to playback this item. \nPlease ' + + 'contact support to report this error: admin-list@example.com.\n'); + }); }); describe('with a playlist manifest', () => { diff --git a/src/components/MediaPlayer/VideoJS/VideoJSPlayer.js b/src/components/MediaPlayer/VideoJS/VideoJSPlayer.js index 8deb2b44..b5351863 100644 --- a/src/components/MediaPlayer/VideoJS/VideoJSPlayer.js +++ b/src/components/MediaPlayer/VideoJS/VideoJSPlayer.js @@ -452,7 +452,7 @@ function VideoJSPlayer({ * in the player's time rail. * */ const handleTimeUpdate = () => { - if (player !== null && isReadyRef.current) { + if (player !== null && isReadyRef.current && !isClicked) { const activeSegment = getActiveSegment(player.currentTime()); if (activeSegment && activeIdRef.current != activeSegment['id']) { // Set the active segment id in component's state diff --git a/src/components/MediaPlayer/VideoJS/components/js/VideoJSCurrentTime.js b/src/components/MediaPlayer/VideoJS/components/js/VideoJSCurrentTime.js index 4956d512..e4f22745 100644 --- a/src/components/MediaPlayer/VideoJS/components/js/VideoJSCurrentTime.js +++ b/src/components/MediaPlayer/VideoJS/components/js/VideoJSCurrentTime.js @@ -47,12 +47,23 @@ function CurrentTimeDisplay({ player, options }) { const [currTime, setCurrTime] = React.useState(player.currentTime()); + let initTimeRef = React.useRef(options.currentTime); + const setInitTime = (t) => { + initTimeRef.current = t; + }; + player.on('timeupdate', () => { if (player.isDisposed()) return; - let time = player.currentTime(); - + let time; + // Update time from the given initial time if it is not zero + if (initTimeRef.current > 0 && player.currentTime() == 0) { + time = initTimeRef.current; + } else { + time = player.currentTime(); + } if (targets.length > 1) time += targets[srcIndex].altStart; setCurrTime(time); + setInitTime(0); }); return ( diff --git a/src/components/MediaPlayer/VideoJS/components/js/VideoJSProgress.js b/src/components/MediaPlayer/VideoJS/components/js/VideoJSProgress.js index 02292596..3e5afe77 100644 --- a/src/components/MediaPlayer/VideoJS/components/js/VideoJSProgress.js +++ b/src/components/MediaPlayer/VideoJS/components/js/VideoJSProgress.js @@ -30,6 +30,7 @@ class VideoJSProgress extends vjsComponent { this.player = player; this.options = options; + this.currentTime = options.currentTime; this.state = { startTime: null, endTime: null }; this.times = options.targets[options.srcIndex]; @@ -154,6 +155,7 @@ class VideoJSProgress extends vjsComponent { handleOnChange={this.handleOnChange} player={this.player} handleTimeUpdate={this.handleTimeUpdate} + initCurrentTime={this.options.currentTime} times={this.times} options={this.options} />, @@ -171,8 +173,8 @@ class VideoJSProgress extends vjsComponent { * @param {obj.options} - options passed when initilizing the component * @returns */ -function ProgressBar({ player, handleTimeUpdate, times, options }) { - const [progress, _setProgress] = React.useState(0); +function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options }) { + const [progress, _setProgress] = React.useState(initCurrentTime); const [currentTime, setCurrentTime] = React.useState(player.currentTime()); const timeToolRef = React.useRef(); const leftBlockRef = React.useRef(); @@ -184,6 +186,10 @@ function ProgressBar({ player, handleTimeUpdate, times, options }) { const isMultiSourced = options.targets.length > 1 ? true : false; + let initTimeRef = React.useRef(initCurrentTime); + const setInitTime = (t) => { + initTimeRef.current = t; + }; let progressRef = React.useRef(progress); const setProgress = (p) => { progressRef.current = p; @@ -233,9 +239,18 @@ function ProgressBar({ player, handleTimeUpdate, times, options }) { player.on('timeupdate', () => { if (player.isDisposed()) return; - const curTime = player.currentTime(); + let curTime; + // Initially update progress from the prop passed from Ramp, + // this accounts for structured navigation when switching canvases + if ((initTimeRef.current > 0 && player.currentTime() == 0)) { + curTime = initTimeRef.current; + player.currentTime(initTimeRef.current); + } else { + curTime = player.currentTime(); + } setProgress(curTime); handleTimeUpdate(curTime); + setInitTime(0); }); /** diff --git a/src/components/StructuredNavigation/StructuredNavigation.js b/src/components/StructuredNavigation/StructuredNavigation.js index ae42a938..228ff204 100644 --- a/src/components/StructuredNavigation/StructuredNavigation.js +++ b/src/components/StructuredNavigation/StructuredNavigation.js @@ -26,6 +26,7 @@ const StructuredNavigation = () => { useManifestState(); let structureItemsRef = React.useRef(); + let canvasIsEmptyRef = React.useRef(canvasIsEmpty); React.useEffect(() => { // Update currentTime and canvasIndex in state if a @@ -52,7 +53,7 @@ const StructuredNavigation = () => { } }, [manifest]); - // Set currentNavItem when current Canvas is an inaccessible item + // Set currentNavItem when current Canvas is an inaccessible/empty item React.useEffect(() => { if (canvasIsEmpty && playlist.isPlaylist) { manifestDispatch({ @@ -96,14 +97,11 @@ const StructuredNavigation = () => { canvasIndex: currentCanvasIndex, type: 'switchCanvas', }); + canvasIsEmptyRef.current = structureItemsRef.current[currentCanvasIndex].isEmpty; } } - if (canvasIsEmpty) { - // Reset isClicked in state for - // inaccessible items (empty canvases) - playerDispatch({ type: 'resetClick' }); - } else if (player) { + if (player && !canvasIsEmptyRef.current) { player.currentTime(timeFragmentStart); playerDispatch({ startTime: timeFragment.start, @@ -118,7 +116,10 @@ const StructuredNavigation = () => { // Setting userActive to true shows timerail breifly, helps // to visualize the structure in player while playing if (isPlaying) player.userActive(true); - player.currentTime(timeFragmentStart); + } else if (canvasIsEmptyRef.current) { + // Reset isClicked in state for + // inaccessible items (empty canvases) + playerDispatch({ type: 'resetClick' }); } } }, [isClicked, player]); diff --git a/src/services/iiif-parser.js b/src/services/iiif-parser.js index d7304a45..3473120f 100644 --- a/src/services/iiif-parser.js +++ b/src/services/iiif-parser.js @@ -268,11 +268,11 @@ export function inaccessibleItemMessage(manifest, canvasIndex) { const item = items[0].getBody()[0]; itemMessage = item.getLabel().getValue() ? getLabelValue(item.getLabel().getValue()) - : 'No associated media source(s) in the Canvas'; + : 'This item cannot be played.'; } return itemMessage; } else { - return null; + return 'This item cannot be played.'; } } diff --git a/src/services/iiif-parser.test.js b/src/services/iiif-parser.test.js index ce843545..a8547c20 100644 --- a/src/services/iiif-parser.test.js +++ b/src/services/iiif-parser.test.js @@ -1,5 +1,4 @@ import manifest from '@TestData/transcript-annotation'; -import playlistManifest from '@TestData/playlist'; import volleyballManifest from '@TestData/volleyball-for-boys'; import lunchroomManifest from '@TestData/lunchroom-manners'; import manifestWoStructure from '@TestData/transcript-canvas'; @@ -122,7 +121,7 @@ describe('iiif-parser', () => { expect(sources[0].selected).toBeTruthy(); }); - if ('sets default source when not multisourced', () => { + it('sets default source when not multisourced', () => { const { sources } = iiifParser.getMediaInfo({ manifest: singleSrcManifest, canvasIndex: 0 @@ -354,17 +353,17 @@ describe('iiif-parser', () => { describe('inaccessibleItemMessage()', () => { it('returns text under placeholderCanvas', () => { const itemMessage = iiifParser.inaccessibleItemMessage(manifest, 1); - expect(itemMessage).toEqual('You do not have permission to playback this item.'); + expect(itemMessage).toEqual('You do not have permission to playback this item. \nPlease contact support to report this error: <a href="mailto:admin-list@example.com">admin-list@example.com</a>.\n'); }); it('returns hard coded text when placeholderCanvas has no text', () => { const itemMessage = iiifParser.inaccessibleItemMessage(lunchroomManifest, 0); - expect(itemMessage).toEqual('No associated media source(s) in the Canvas'); + expect(itemMessage).toEqual('This item cannot be played.'); }); - if ('returns null when no placeholderCanvas is in the Canvas', () => { + it('returns null when no placeholderCanvas is in the Canvas', () => { const itemMessage = iiifParser.inaccessibleItemMessage(singleSrcManifest, 0); - expect(itemMessage).toBeNull(); + expect(itemMessage).toEqual('This item cannot be played.'); }); }); diff --git a/src/test_data/transcript-annotation.js b/src/test_data/transcript-annotation.js index 4919b2e4..b33ec4ed 100644 --- a/src/test_data/transcript-annotation.js +++ b/src/test_data/transcript-annotation.js @@ -138,7 +138,7 @@ export default { id: null, type: "Text", format: "text/plain", - label: { en: ['You do not have permission to playback this item.'] } + label: { en: ['You do not have permission to playback this item. \nPlease contact support to report this error: <a href="mailto:admin-list@example.com">admin-list@example.com</a>.\n'] } }, target: 'https://example.com/sample/transcript-annotation/canvas/2/placeholder' }