117 lines
5.3 KiB
TypeScript
117 lines
5.3 KiB
TypeScript
import { TextTrackKinds, TextTrackModes } from '../constants.js';
|
|
import { getTextTracksList, updateTracksModeTo } from '../utils/captions.js';
|
|
import { TextTrackLike } from '../utils/TextTrackLike.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 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]));
|
|
};
|