Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: openedx/frontend-app-learner-portal-enterprise
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: df41d58db57da4328ff2167fb11529253b9ec32c
Choose a base ref
..
head repository: openedx/frontend-app-learner-portal-enterprise
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: d454fa466031d3626c75af25087fa85ee0dd20a2
Choose a head ref
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ const ytUrl = 'https://www.youtube.com/watch?v=oHg5SJYRHA0';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: () => 'en',
getPrimaryLanguageSubtag: () => 'en',
}));

describe('Course Preview Tests', () => {
22 changes: 16 additions & 6 deletions src/components/video/VideoJS.jsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import 'videojs-youtube';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import { getLocale } from '@edx/frontend-platform/i18n';
import { getLocale, getPrimaryLanguageSubtag } from '@edx/frontend-platform/i18n';
import { PLAYBACK_RATES } from './data/constants';
import { usePlayerOptions, useTranscripts } from './data';

@@ -15,7 +15,10 @@ require('videojs-vjstranscribe');
const VideoJS = ({ options, onReady, customOptions }) => {
const videoRef = useRef(null);
const playerRef = useRef(null);
const siteLanguage = getLocale();

// Some language codes include full locales like es-419, which may not match the codes in the text tracks.
// To ensure proper matching, we strip a locale down to that first subtag.
const siteLanguage = getPrimaryLanguageSubtag(getLocale());

const transcripts = useTranscripts({
player: playerRef.current,
@@ -29,8 +32,7 @@ const VideoJS = ({ options, onReady, customOptions }) => {
customOptions,
});

const handlePlayerReady = useCallback(() => {
// Add remote text tracks
const addTextTracks = useCallback(() => {
const textTracks = Object.entries(transcripts.textTracks);
textTracks.forEach(([lang, webVttFileUrl]) => {
playerRef.current.addRemoteTextTrack({
@@ -40,6 +42,11 @@ const VideoJS = ({ options, onReady, customOptions }) => {
label: lang,
}, false);
});
}, [transcripts.textTracks]);

const handlePlayerReady = useCallback(() => {
// Add remote text tracks
addTextTracks();

// Set playback rates
if (customOptions?.showPlaybackMenu) {
@@ -50,7 +57,7 @@ const VideoJS = ({ options, onReady, customOptions }) => {
if (onReady) {
onReady(playerRef.current);
}
}, [customOptions?.showPlaybackMenu, onReady, transcripts.textTracks]);
}, [customOptions?.showPlaybackMenu, onReady, addTextTracks]);

useEffect(() => {
if (transcripts.isLoading) {
@@ -70,8 +77,11 @@ const VideoJS = ({ options, onReady, customOptions }) => {
} else {
playerRef.current.autoplay(playerOptions.autoplay);
playerRef.current.src(playerOptions.sources);

// Re-add the text tracks if the player already exists
addTextTracks();
}
}, [transcripts.isLoading, playerOptions, handlePlayerReady]);
}, [transcripts.isLoading, playerOptions, handlePlayerReady, addTextTracks]);

// Dispose the Video.js player when the functional component unmounts
useEffect(() => {
9 changes: 5 additions & 4 deletions src/components/video/data/hooks.js
Original file line number Diff line number Diff line change
@@ -18,12 +18,13 @@ export function useTranscripts({ player, customOptions, siteLanguage }) {
const result = await fetchAndAddTranscripts(customOptions.transcriptUrls, player);

// Sort the text tracks to prioritize the site language at the top of the list.
// Currently, video.js selects the top language from the list of transcripts.
const sortedResult = sortTextTracks(result, siteLanguage);
// since video.js selects the top language from the list of transcripts.
// Preferred language is the site language, with English as the fallback.
const preferredLanguage = result?.[siteLanguage] ? siteLanguage : 'en';
const sortedResult = sortTextTracks(result, preferredLanguage);
setTextTracks(sortedResult);

// Default to site language, fallback to English
const preferredTranscript = sortedResult[siteLanguage] || sortedResult.en;
const preferredTranscript = sortedResult?.[preferredLanguage];
setTranscriptUrl(preferredTranscript);
} catch (error) {
logError(`Error fetching transcripts for player: ${error}`);
6 changes: 1 addition & 5 deletions src/components/video/data/utils.js
Original file line number Diff line number Diff line change
@@ -27,11 +27,7 @@ export const createWebVttFile = (webVttContent) => {
return URL.createObjectURL(blob);
};

export const sortTextTracks = (tracks, siteLanguage) => {
// Some language codes returned by getLocale include a hyphen, which may not match the codes in the text tracks.
// To ensure proper matching, the hyphen is removed from the site language code if present.
const preferredLanguage = siteLanguage?.includes('-') ? siteLanguage.split('-')[0].toLowerCase() : siteLanguage.toLowerCase();

export const sortTextTracks = (tracks, preferredLanguage) => {
const sortedKeys = Object.keys(tracks).sort((a, b) => {
if (a === preferredLanguage) { return -1; }
if (b === preferredLanguage) { return 1; }
87 changes: 82 additions & 5 deletions src/components/video/tests/VideoJS.test.jsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,8 @@ jest.mock('../data', () => ({

jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: () => 'en',
getLocale: () => 'es-419',
getPrimaryLanguageSubtag: () => 'es',
}));

const hlsUrl = 'https://test-domain.com/test-prefix/id.m3u8';
@@ -64,16 +65,16 @@ describe('VideoJS', () => {
const customOptions = {
showTranscripts: true,
transcriptUrls: {
en: 'https://example.com/transcript-en.txt',
es: 'https://example.com/transcript-es.txt',
},
};

useTranscripts.mockReturnValue({
isLoading: false,
textTracks: {
en: 'https://example.com/transcript-en.txt',
es: 'https://example.com/transcript-es.txt',
},
transcriptUrl: 'https://example.com/transcript-en.txt',
transcriptUrl: 'https://example.com/transcript-es.txt',
});

const { container } = renderWithRouter(<VideoJS options={HLSVideoOptions} customOptions={customOptions} />);
@@ -86,11 +87,87 @@ describe('VideoJS', () => {
});
});

it('Correctly adds text tracks using the addTextTracks function.', async () => {
jest.mock('video.js', () => {
const actualVideoJs = jest.requireActual('video.js');
return {
...actualVideoJs,
videojs: jest.fn().mockImplementation(() => ({
addRemoteTextTrack: jest.fn(),
playbackRates: jest.fn(),
src: jest.fn(),
dispose: jest.fn(),
autoplay: jest.fn(),
on: jest.fn(),
off: jest.fn(),
ready: jest.fn(),
isDisposed: jest.fn(),
})),
};
});
const mockAddRemoteTextTrack = jest.fn();
const mockPlayerRef = {
current: {
addRemoteTextTrack: mockAddRemoteTextTrack,
},
};
useTranscripts.mockReturnValue({
isLoading: false,
textTracks: {
es: 'https://example.com/transcript-es.vtt',
},
transcriptUrl: 'https://example.com/transcript-es.vtt',
});
const options = {
autoplay: false,
controls: true,
responsive: true,
fluid: true,
sources: [{
src: 'https://example.com/video.mp4',
type: 'video/mp4',
}],
};
const customOptions = {
showTranscripts: true,
transcriptUrls: {
es: 'https://example.com/transcript-es.vtt',
},
};
const onReady = () => {
mockPlayerRef.current.addRemoteTextTrack({
kind: 'subtitles',
src: 'https://example.com/transcript-es.vtt',
srclang: 'es',
label: 'es',
}, false);
};

const { container } = renderWithRouter(
<VideoJS options={options} customOptions={customOptions} onReady={onReady} />,
);

await waitFor(() => {
const videoJsInstance = container.querySelector('video-js');
expect(videoJsInstance).toBeTruthy();

expect(mockAddRemoteTextTrack).toHaveBeenCalledWith(
{
kind: 'subtitles',
src: 'https://example.com/transcript-es.vtt',
srclang: 'es',
label: 'es',
},
false,
);
});
});

it('Does not initialize VideoJS player while transcripts are loading.', async () => {
const customOptions = {
showTranscripts: true,
transcriptUrls: {
en: 'https://example.com/transcript-en.txt',
es: 'https://example.com/transcript-es.txt',
},
};

1 change: 1 addition & 0 deletions src/components/video/tests/VideoPlayer.test.jsx
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ const mp3Url = 'https://example.com/audio.mp3';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: () => 'en',
getPrimaryLanguageSubtag: () => 'en',
}));

describe('Video Player component', () => {