import { TextTrackKinds, TextTrackModes } from '../constants.js'; import { getTextTracksList, updateTracksModeTo } from '../utils/captions.js'; import { TextTrackLike } from '../utils/TextTrackLike.js'; import { globalThis } from '../utils/server-safe-globals.js'; export const getSubtitleTracks = (stateOwners): TextTrackLike[] => { return getTextTracksList(stateOwners.media, (textTrack) => { return [TextTrackKinds.SUBTITLES, TextTrackKinds.CAPTIONS].includes( textTrack.kind as any ); }).sort((a, b) => (a.kind >= b.kind ? 1 : -1)); }; export const getShowingSubtitleTracks = (stateOwners): TextTrackLike[] => { return getTextTracksList(stateOwners.media, (textTrack) => { return ( textTrack.mode === TextTrackModes.SHOWING && [TextTrackKinds.SUBTITLES, TextTrackKinds.CAPTIONS].includes( textTrack.kind as any ) ); }); }; export const toggleSubtitleTracks = (stateOwners, force: boolean): void => { // NOTE: Like Element::toggleAttribute(), this event uses the detail for an optional "force" // value. When present, this means "toggle to" "on" (aka showing, even if something's already showing) // or "off" (aka disabled, even if all tracks are currently disabled). // See, e.g.: https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute#force (CJP) // NOTE: Like Element::toggleAttribute(), this event uses the detail for an optional "force" // value. When present, this means "toggle to" "on" (aka showing, even if something's already showing) // or "off" (aka disabled, even if all tracks are currently disabled). // See, e.g.: https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute#force (CJP) const tracks = getSubtitleTracks(stateOwners); const showingSubitleTracks = getShowingSubtitleTracks(stateOwners); const subtitlesShowing = !!showingSubitleTracks.length; // If there are no tracks, this request doesn't matter, so we're done. if (!tracks.length) return; // NOTE: not early bailing on forced cases so we may pick up async cases of toggling on, particularly for HAS-style // (e.g. HLS) media where we may not get our preferred subtitles lang until later (CJP) if (force === false || (subtitlesShowing && force !== true)) { updateTracksModeTo(TextTrackModes.DISABLED, tracks, showingSubitleTracks); } else if (force === true || (!subtitlesShowing && force !== false)) { let subTrack = tracks[0]; const { options } = stateOwners; if (!options?.noSubtitlesLangPref) { const subtitlesPref = globalThis.localStorage.getItem( 'media-chrome-pref-subtitles-lang' ); const userLangPrefs = subtitlesPref ? [subtitlesPref, ...globalThis.navigator.languages] : globalThis.navigator.languages; const preferredAvailableSubs = tracks .filter((textTrack) => { return userLangPrefs.some((lang) => textTrack.language.toLowerCase().startsWith(lang.split('-')[0]) ); }) .sort((textTrackA, textTrackB) => { const idxA = userLangPrefs.findIndex((lang) => textTrackA.language.toLowerCase().startsWith(lang.split('-')[0]) ); const idxB = userLangPrefs.findIndex((lang) => textTrackB.language.toLowerCase().startsWith(lang.split('-')[0]) ); return idxA - idxB; }); // Since there may not have been any user preferred subs/cc match, keep the default (picking the first) as // the subtitle track to show for these cases. if (preferredAvailableSubs[0]) { subTrack = preferredAvailableSubs[0]; } } const { language, label, kind } = subTrack; updateTracksModeTo(TextTrackModes.DISABLED, tracks, showingSubitleTracks); updateTracksModeTo(TextTrackModes.SHOWING, tracks, [ { language, label, kind }, ]); } }; export const areValuesEq = (x: any, y: any): boolean => { // If both are strictly equal, they're equal if (x === y) return true; // If either is null, they're not equal if (x == null || y == null) return false; // If their types don't match, they're not equal if (typeof x !== typeof y) return false; // Treat NaNs as equal if (typeof x === 'number' && Number.isNaN(x) && Number.isNaN(y)) return true; // NOTE: This impl does not support function values (CJP) // All other "simple" types are not equal, since they have the same type and were not strictly equal if (typeof x !== 'object') return false; if (Array.isArray(x)) return areArraysEq(x, y); // NOTE: This impl currently assumes that if y[key] -> x[key] (aka no "extra" keys in y) (CJP) // For objects, if every key's value in x has a corresponding key/value entry in y, the objects are equal return Object.entries(x).every( // NOTE: Checking key in y to disambiguate between between missing keys and keys whose value are undefined (CJP) ([key, value]) => key in y && areValuesEq(value as number, y[key]) ); }; export const areArraysEq = (xs: number[], ys: number[]): boolean => { const xIsArray = Array.isArray(xs); const yIsArray = Array.isArray(ys); // If one of the "arrays" is not an array, not equal if (xIsArray !== yIsArray) return false; // If both of the "arrays" are not arrays, equal if (!(xIsArray || yIsArray)) return true; // If arrays have different length, not equal if (xs.length !== ys.length) return false; // NOTE: presuming sort order is equivalent (CJP) // If and only every corresponding entry between the arrays is equal, arrays are equal return xs.every((x, i) => areValuesEq(x, ys[i])); };