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

Show track scrubber in playlist context #387

Merged
merged 4 commits into from
Feb 7, 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
11 changes: 6 additions & 5 deletions src/components/MediaPlayer/MediaPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => {
'videoJSCurrentTime',
'timeDivider',
'durationDisplay',
hasStructure ? 'videoJSTrackScrubber' : '',
(hasStructure || playlist.isPlaylist) ? 'videoJSTrackScrubber' : '',
playerConfig.tracks.length > 0 ? 'subsCapsButton' : '',
IS_MOBILE ? 'muteToggle' : 'volumePanel',
'qualitySelector',
Expand Down Expand Up @@ -401,16 +401,17 @@ const MediaPlayer = ({ enableFileDownload = false, enablePIP = false }) => {
}
};
}
// Iniitialize track scrubber button when the current Cavas has
// structure timespans
if (hasStructure) {
// Iniitialize track scrubber button when the current Canvas has
// structure timespans or the given Manifest is a playlist Manifest
if (hasStructure || playlist.isPlaylist) {
videoJsOptions = {
...videoJsOptions,
controlBar: {
...videoJsOptions.controlBar,
videoJSTrackScrubber: {
trackScrubberRef,
timeToolRef
timeToolRef,
isPlaylist: playlist.isPlaylist,
}
}
};
Expand Down
178 changes: 123 additions & 55 deletions src/components/MediaPlayer/MediaPlayer.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { withManifestAndPlayerProvider } from '../../services/testing-helpers';
import MediaPlayer from './MediaPlayer';
import { ErrorBoundary } from 'react-error-boundary';
Expand All @@ -22,7 +22,7 @@ describe('MediaPlayer component', () => {
console.error = originalError;
});

describe('with audio manifest', () => {
describe('with a regular audio Manifest', () => {
beforeEach(() => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: audioManifest, canvasIndex: 0 },
Expand All @@ -46,7 +46,7 @@ describe('MediaPlayer component', () => {
});
});

describe('with video manifest', () => {
describe('with a regular video Manifest', () => {
beforeEach(() => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: videoManifest, canvasIndex: 0 },
Expand All @@ -70,6 +70,26 @@ describe('MediaPlayer component', () => {
});
});

test('with a playlist Manifest renders successfully', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: {
manifest: playlistManifest,
canvasIndex: 1,
playlist: { isPlaylist: true }
},
initialPlayerState: {},
});
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
));
expect(screen.queryByTestId('inaccessible-item')).not.toBeInTheDocument();
expect(
screen.queryAllByTestId('videojs-video-element').length
).toBeGreaterThan(0);
});

describe('with props', () => {
test('enableFileDownload = false', () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
Expand Down Expand Up @@ -100,54 +120,126 @@ describe('MediaPlayer component', () => {
});
});

describe('with a non-playlist manifest', () => {
describe('with a single canvas', () => {
test('does not render previous/next section buttons', () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: audioManifest, canvasIndex: 0 },
initialPlayerState: {},
});
render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
);
expect(screen.queryByTestId('videojs-next-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).not.toBeInTheDocument();
describe('previous/next section buttons', () => {
test('does not render with a single Canvas Manifest', () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: audioManifest, canvasIndex: 0 },
initialPlayerState: {},
});
render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
);
expect(screen.queryByTestId('videojs-next-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).not.toBeInTheDocument();
});

test('with multiple sources does not render previous/next buttons', () => {
test('does not render with a single Canvas with multiple sources', () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: audioManifest, canvasIndex: 0 },
initialPlayerState: {},
});
render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
);
expect(screen.queryByTestId('videojs-next-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).not.toBeInTheDocument();
});

test('renders with a multi-Canvas regualr Manifest', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: videoManifest, canvasIndex: 0 },
initialPlayerState: {},
});
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
));
expect(screen.queryByTestId('videojs-next-button')).toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).toBeInTheDocument();
});

test('renders with a multi-Canvas playlist Manifest', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: {
manifest: playlistManifest,
canvasIndex: 1,
playlist: { isPlaylist: true }
},
initialPlayerState: {},
});
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
));
expect(screen.queryByTestId('videojs-previous-button')).toBeInTheDocument();
expect(screen.queryByTestId('videojs-next-button')).toBeInTheDocument();
});
});

describe('track scrubber button', () => {
test('does not render with a regular Manifest without structure timespans', () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: {
...manifestState,
manifest: audioManifest,
canvasIndex: 0
},
initialPlayerState: {},
});
render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
);
expect(screen.queryByTestId('videojs-track-scrubber-button')).not.toBeInTheDocument();
});

describe('renders', () => {
test('with a regular Manifest with structure timespans', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: audioManifest, canvasIndex: 0 },
initialManifestState: {
...manifestState,
manifest: videoManifest,
canvasIndex: 0,
hasStructure: true
},
initialPlayerState: {},
});
render(
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
);
expect(screen.queryByTestId('videojs-next-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).not.toBeInTheDocument();
));
expect(screen.queryByTestId('videojs-track-scrubber-button')).toBeInTheDocument();
});
});

describe('with multiple canvases', () => {
test('renders previous/next section buttons', async () => {
test('renders with a playlist Manifest', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: { ...manifestState, manifest: videoManifest, canvasIndex: 0 },
initialManifestState: {
manifest: playlistManifest,
canvasIndex: 1,
playlist: { isPlaylist: true }
},
initialPlayerState: {},
});
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
));
expect(screen.queryByTestId('videojs-next-button')).toBeInTheDocument();
expect(screen.queryByTestId('videojs-previous-button')).toBeInTheDocument();
expect(screen.queryByTestId('videojs-track-scrubber-button')).toBeInTheDocument();
});
});
});

test('renders a message with HTML from placeholderCanvas for empty canvas', () => {
describe('displays inaccessible message', () => {
test('with HTML from placeholderCanvas for an empty canvas', () => {
// Stub loading HTMLMediaElement for jsdom
window.HTMLMediaElement.prototype.load = () => { };

Expand All @@ -169,10 +261,8 @@ describe('MediaPlayer component', () => {
.toEqual('You do not have permission to playback this item. \nPlease ' +
'contact support to report this error: [email protected].\n');
});
});

describe('with a playlist manifest', () => {
test('renders a message for an inaccessible Canvas', () => {
test('for an inaccessible item in a playlist Manifest', () => {
// Stub loading HTMLMediaElement for jsdom
window.HTMLMediaElement.prototype.load = () => { };

Expand All @@ -192,27 +282,5 @@ describe('MediaPlayer component', () => {
expect(screen.queryByTestId('inaccessible-item')).toBeInTheDocument();
expect(screen.getByText('You do not have permission to playback this item.')).toBeInTheDocument();
});

test('renders player for a accessible Canvas', async () => {
const PlayerWithManifest = withManifestAndPlayerProvider(MediaPlayer, {
initialManifestState: {
manifest: playlistManifest,
canvasIndex: 1,
playlist: { isPlaylist: true }
},
initialPlayerState: {},
});
await act(async () => render(
<ErrorBoundary>
<PlayerWithManifest />
</ErrorBoundary>
));
expect(screen.queryByTestId('inaccessible-item')).not.toBeInTheDocument();
expect(
screen.queryAllByTestId('videojs-video-element').length
).toBeGreaterThan(0);
expect(screen.queryByTestId('videojs-previous-button')).toBeInTheDocument();
expect(screen.queryByTestId('videojs-next-button')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ class VideoJSProgress extends vjsComponent {
}

// Calculate the played percentage of the media file's duration
const played = Number(((curTime - start) * 100) / (end - start));
let trackoffset = curTime - start;
const played = Math.min(
100,
Math.max(0, 100 * trackoffset / (end - start))
);

document.documentElement.style.setProperty(
'--range-progress',
Expand Down Expand Up @@ -202,6 +206,7 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
let playerEventListener;

const { start, end } = times;
const altStart = targets[srcIndex].altStart;

// Clean up interval on component unmount
React.useEffect(() => {
Expand All @@ -228,7 +233,13 @@ function ProgressBar({ player, handleTimeUpdate, initCurrentTime, times, options
player.on('loadedmetadata', () => {
const curTime = player.currentTime();
setProgress(curTime);
setCurrentTime(curTime + targets[srcIndex].altStart);
setCurrentTime(curTime + altStart);

/** Set playable duration and alternate start as player properties to use in
* track scrubber component, when displaying playlist manifests
*/
player.playableDuration = (end - start) || player.duration();
player.altStart = start;

/**
* Using a time interval instead of 'timeupdate event in VideoJS, because Safari
Expand Down
Loading