UNPKG

rx-player

Version:
682 lines (681 loc) 29.8 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import enableAudioTrack from "../../compat/enable_audio_track"; import EventEmitter from "../../utils/event_emitter"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import normalizeLanguage from "../../utils/languages"; /** * Check if track array is different from an other one * @param {Array.<Object>} oldTrackArray * @param {Array.<Object>} newTrackArray * @returns {boolean} */ function areTrackArraysDifferent(oldTrackArray, newTrackArray) { var _a; if (newTrackArray.length !== oldTrackArray.length) { return true; } for (let i = 0; i < newTrackArray.length; i++) { if (newTrackArray[i].nativeTrack !== ((_a = oldTrackArray[i]) === null || _a === void 0 ? void 0 : _a.nativeTrack)) { return true; } } return false; } /** * Create audio tracks from native audio tracks. * @param {AudioTrackList} audioTracks * @returns {Array.<Object>} */ function createAudioTracks(audioTracks) { var _a; const newAudioTracks = []; const languagesOccurences = {}; for (let i = 0; i < audioTracks.length; i++) { const audioTrack = audioTracks[i]; const language = audioTrack.language === "" ? "nolang" : audioTrack.language; const occurences = (_a = languagesOccurences[language]) !== null && _a !== void 0 ? _a : 1; const id = "gen_audio_" + language + "_" + occurences.toString(); languagesOccurences[language] = occurences + 1; const track = { language: audioTrack.language, id, normalized: normalizeLanguage(audioTrack.language), audioDescription: audioTrack.kind === "descriptions" || // Safari seem to prefer the non-standard singular // version, funnily enough audioTrack.kind === "description", representations: [], }; newAudioTracks.push({ track, nativeTrack: audioTrack }); } return newAudioTracks; } /** * Create text tracks from native text tracks. * @param {TextTrackList} textTracks * @returns {Array.<Object>} */ function createTextTracks(textTracks) { var _a; const newTextTracks = []; const languagesOccurences = {}; for (let i = 0; i < textTracks.length; i++) { const textTrack = textTracks[i]; const language = textTrack.language === "" ? "nolang" : textTrack.language; const occurences = (_a = languagesOccurences[language]) !== null && _a !== void 0 ? _a : 1; const id = "gen_text_" + language + "_" + occurences.toString(); languagesOccurences[language] = occurences + 1; // Safari seems to be indicating that the subtitles track is a forced // subtitles track by setting the `kind` attribute to `"forced"`. // As of now (2023-04-04), this is not standard. // @see https://github.com/whatwg/html/issues/4472 const forced = textTrack.kind === "forced" ? true : undefined; const track = { language: textTrack.language, forced, label: textTrack.label, id, normalized: normalizeLanguage(textTrack.language), closedCaption: textTrack.kind === "captions", }; newTextTracks.push({ track, nativeTrack: textTrack }); } return newTextTracks; } /** * Create video tracks from native video tracks. * @param {VideoTrackList} videoTracks * @returns {Array.<Object>} */ function createVideoTracks(videoTracks) { var _a; const newVideoTracks = []; const languagesOccurences = {}; for (let i = 0; i < videoTracks.length; i++) { const videoTrack = videoTracks[i]; const language = videoTrack.language === "" ? "nolang" : videoTrack.language; const occurences = (_a = languagesOccurences[language]) !== null && _a !== void 0 ? _a : 1; const id = "gen_video_" + language + "_" + occurences.toString(); languagesOccurences[language] = occurences + 1; newVideoTracks.push({ track: { id, representations: [] }, nativeTrack: videoTrack, }); } return newVideoTracks; } /** * Manage video, audio and text tracks for current direct file content. * @class MediaElementTracksStore */ export default class MediaElementTracksStore extends EventEmitter { constructor(mediaElement) { var _a, _b, _c; super(); // TODO In practice, the audio/video/text tracks API are not always implemented on // the media element, although Typescript HTMLMediaElement types tend to mean // that can't be undefined. this._nativeAudioTracks = mediaElement.audioTracks; this._nativeVideoTracks = mediaElement.videoTracks; this._nativeTextTracks = mediaElement.textTracks; this._audioTracks = this._nativeAudioTracks !== undefined ? createAudioTracks(this._nativeAudioTracks) : []; this._videoTracks = this._nativeVideoTracks !== undefined ? createVideoTracks(this._nativeVideoTracks) : []; this._textTracks = this._nativeTextTracks !== undefined ? createTextTracks(this._nativeTextTracks) : []; this._lastEmittedNativeAudioTrack = (_a = this._getCurrentAudioTrack()) === null || _a === void 0 ? void 0 : _a.nativeTrack; this._lastEmittedNativeVideoTrack = (_b = this._getCurrentVideoTrack()) === null || _b === void 0 ? void 0 : _b.nativeTrack; this._lastEmittedNativeTextTrack = (_c = this._getCurrentTextTrack()) === null || _c === void 0 ? void 0 : _c.nativeTrack; this._handleNativeTracksCallbacks(); } /** * Update the currently active audio track by setting the wanted audio track's * ID property. * Throws if the wanted audio track is not found. * @param {string|number|undefined} id */ setAudioTrackById(id) { for (let i = 0; i < this._audioTracks.length; i++) { const { track, nativeTrack } = this._audioTracks[i]; if (track.id === id) { this._enableAudioTrackFromIndex(i); this._audioTrackLockedOn = nativeTrack; return; } } throw new Error("Audio track not found."); } /** * Disable the currently-active text track, if one. */ disableTextTrack() { disableTextTracks(this._textTracks); this._textTrackLockedOn = null; } /** * Update the currently active text track by setting the wanted text track's * ID property. * Throws if the wanted text track is not found. * @param {string|number|undefined} id */ setTextTrackById(id) { let hasSetTrack = false; for (let i = 0; i < this._textTracks.length; i++) { const { track, nativeTrack } = this._textTracks[i]; if (track.id === id) { nativeTrack.mode = "showing"; hasSetTrack = true; this._textTrackLockedOn = nativeTrack; } else if (nativeTrack.mode === "showing" || nativeTrack.mode === "hidden") { nativeTrack.mode = "disabled"; } } if (!hasSetTrack) { throw new Error("Text track not found."); } } /** * Disable the currently-active video track, if one. */ disableVideoTrack() { disableVideoTracks(this._videoTracks); this._videoTrackLockedOn = null; } /** * Disable the currently-active audio track, if one. */ disableAudioTrack() { disableAudioTracks(this._audioTracks); this._audioTrackLockedOn = null; } /** * Update the currently active video track by setting the wanted video track's * ID property. * Throws if the wanted video track is not found. * @param {string|number|undefined} id */ setVideoTrackById(id) { for (let i = 0; i < this._videoTracks.length; i++) { const { track, nativeTrack } = this._videoTracks[i]; if (track.id === id) { nativeTrack.selected = true; this._videoTrackLockedOn = nativeTrack; return; } } throw new Error("Video track not found."); } /** * Returns the currently active audio track. * Returns `null` if no audio track is active. * Returns `undefined` if we cannot know which audio track is active. * @returns {Object|null|undefined} */ getChosenAudioTrack() { const currentAudioTrack = this._getCurrentAudioTrack(); return isNullOrUndefined(currentAudioTrack) ? currentAudioTrack : currentAudioTrack.track; } /** * Returns the currently active text track. * Returns `null` if no text track is active. * Returns `undefined` if we cannot know which text track is active. * @returns {Object|null|undefined} */ getChosenTextTrack() { const currentTextTrack = this._getCurrentTextTrack(); return isNullOrUndefined(currentTextTrack) ? currentTextTrack : currentTextTrack.track; } /** * Returns the currently active video track. * Returns `null` if no video track is active. * Returns `undefined` if we cannot know which video track is active. * @returns {Object|null|undefined} */ getChosenVideoTrack() { const currentVideoTrack = this._getCurrentVideoTrack(); return isNullOrUndefined(currentVideoTrack) ? currentVideoTrack : currentVideoTrack.track; } /** * Returns a description of every available audio tracks. * @returns {Array.<Object>} */ getAvailableAudioTracks() { return this._audioTracks.map(({ track, nativeTrack }) => { return { id: track.id, language: track.language, normalized: track.normalized, audioDescription: track.audioDescription, active: nativeTrack.enabled, representations: track.representations, }; }); } /** * Returns a description of every available text tracks. * @returns {Array.<Object>} */ getAvailableTextTracks() { return this._textTracks.map(({ track, nativeTrack }) => { return { id: track.id, label: track.label, forced: track.forced, language: track.language, normalized: track.normalized, closedCaption: track.closedCaption, active: nativeTrack.mode === "showing", }; }); } /** * Returns a description of every available video tracks. * @returns {Array.<Object>} */ getAvailableVideoTracks() { return this._videoTracks.map(({ track, nativeTrack }) => { return { id: track.id, representations: track.representations, active: nativeTrack.selected, }; }); } /** * Free the resources used by the MediaElementTracksStore. */ dispose() { if (this._nativeVideoTracks !== undefined) { this._nativeVideoTracks.onchange = null; this._nativeVideoTracks.onaddtrack = null; this._nativeVideoTracks.onremovetrack = null; } if (this._nativeAudioTracks !== undefined) { this._nativeAudioTracks.onchange = null; this._nativeAudioTracks.onaddtrack = null; this._nativeAudioTracks.onremovetrack = null; } if (this._nativeTextTracks !== undefined) { this._nativeTextTracks.onchange = null; this._nativeTextTracks.onaddtrack = null; this._nativeTextTracks.onremovetrack = null; } this.removeEventListener(); } /** * Get information about the currently chosen audio track. * `undefined` if we cannot know it. * `null` if no audio track is chosen. * @returns {Object|undefined|null} */ _getCurrentAudioTrack() { if (this._nativeAudioTracks === undefined) { return undefined; } for (let i = 0; i < this._audioTracks.length; i++) { const audioTrack = this._audioTracks[i]; if (audioTrack.nativeTrack.enabled) { return audioTrack; } } return null; } /** * Get information about the currently chosen video track. * `undefined` if we cannot know it. * `null` if no video track is chosen. * @returns {Object|undefined|null} */ _getCurrentVideoTrack() { if (this._nativeVideoTracks === undefined) { return undefined; } for (let i = 0; i < this._videoTracks.length; i++) { const videoTrack = this._videoTracks[i]; if (videoTrack.nativeTrack.selected) { return videoTrack; } } return null; } /** * Get information about the currently chosen text track. * `undefined` if we cannot know it. * `null` if no text track is chosen. * @returns {Object|undefined|null} */ _getCurrentTextTrack() { if (this._nativeTextTracks === undefined) { return undefined; } for (let i = 0; i < this._textTracks.length; i++) { const textTrack = this._textTracks[i]; if (textTrack.nativeTrack.mode === "showing") { return textTrack; } } return null; } /** * Iterate over every available audio tracks on the media element and either: * - if the last manually set audio track is found, set that one. * - if we still do not find an optimal track, let the one chosen by default */ _setPreviouslyLockedAudioTrack() { if (this._audioTrackLockedOn === undefined) { return; } else if (this._audioTrackLockedOn === null) { for (let i = 0; i < this._audioTracks.length; i++) { const { nativeTrack } = this._audioTracks[i]; nativeTrack.enabled = false; } } else { for (let i = 0; i < this._audioTracks.length; i++) { const { nativeTrack } = this._audioTracks[i]; if (nativeTrack === this._audioTrackLockedOn) { this._enableAudioTrackFromIndex(i); return; } } } } /** * Iterate over every available text tracks on the media element and either: * - if the last manually set text track is found, set that one. * - if we still do not find an optimal track, just disable it. */ _setPreviouslyLockedTextTrack() { if (this._textTrackLockedOn === undefined) { return; } else if (this._textTrackLockedOn === null) { disableTextTracks(this._textTracks); return; } else { for (let i = 0; i < this._textTracks.length; i++) { const { nativeTrack } = this._textTracks[i]; if (nativeTrack === this._textTrackLockedOn) { // disable the rest disableAllTextTracksBut(this._textTracks, nativeTrack); if (nativeTrack.mode !== "showing") { nativeTrack.mode = "showing"; } return; } } } } /** * Iterate over every available video tracks on the media element and either: * - if the last manually set video track is found, set that one. * - if we still do not find an optimal track, let the one chosen by default */ _setPreviouslyLockedVideoTrack() { if (this._videoTrackLockedOn === undefined) { return; } else if (this._videoTrackLockedOn === null) { disableVideoTracks(this._videoTracks); return; } else { for (let i = 0; i < this._videoTracks.length; i++) { const { nativeTrack } = this._videoTracks[i]; if (nativeTrack === this._videoTrackLockedOn) { nativeTrack.selected = true; return; } } } } /** * Monitor native tracks add, remove and change callback and trigger the * change events. */ _handleNativeTracksCallbacks() { if (this._nativeAudioTracks !== undefined) { this._nativeAudioTracks.onaddtrack = () => { var _a, _b; if (this._nativeAudioTracks !== undefined) { const newAudioTracks = createAudioTracks(this._nativeAudioTracks); if (areTrackArraysDifferent(this._audioTracks, newAudioTracks)) { this._audioTracks = newAudioTracks; this._setPreviouslyLockedAudioTrack(); this.trigger("availableAudioTracksChange", this.getAvailableAudioTracks()); const chosenAudioTrack = this._getCurrentAudioTrack(); if ((chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.nativeTrack) !== this._lastEmittedNativeAudioTrack) { this.trigger("audioTrackChange", (_a = chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeAudioTrack = (_b = chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeAudioTracks.onremovetrack = () => { var _a, _b; if (this._nativeAudioTracks !== undefined) { const newAudioTracks = createAudioTracks(this._nativeAudioTracks); if (areTrackArraysDifferent(this._audioTracks, newAudioTracks)) { this._audioTracks = newAudioTracks; this.trigger("availableAudioTracksChange", this.getAvailableAudioTracks()); const chosenAudioTrack = this._getCurrentAudioTrack(); if ((chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.nativeTrack) !== this._lastEmittedNativeAudioTrack) { this.trigger("audioTrackChange", (_a = chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeAudioTrack = (_b = chosenAudioTrack === null || chosenAudioTrack === void 0 ? void 0 : chosenAudioTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeAudioTracks.onchange = () => { if (this._audioTracks !== undefined) { for (let i = 0; i < this._audioTracks.length; i++) { const { track, nativeTrack } = this._audioTracks[i]; if (nativeTrack.enabled) { if (nativeTrack !== this._lastEmittedNativeAudioTrack) { this.trigger("audioTrackChange", track); this._lastEmittedNativeAudioTrack = nativeTrack; } return; } } } if (this._lastEmittedNativeAudioTrack !== null) { this.trigger("audioTrackChange", null); this._lastEmittedNativeAudioTrack = null; } return; }; } if (this._nativeTextTracks !== undefined) { this._nativeTextTracks.onaddtrack = () => { var _a, _b; if (this._nativeTextTracks !== undefined) { const newTextTracks = createTextTracks(this._nativeTextTracks); if (areTrackArraysDifferent(this._textTracks, newTextTracks)) { this._textTracks = newTextTracks; this._setPreviouslyLockedTextTrack(); this.trigger("availableTextTracksChange", this.getAvailableTextTracks()); const chosenTextTrack = this._getCurrentTextTrack(); if ((chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.nativeTrack) !== this._lastEmittedNativeTextTrack) { this.trigger("textTrackChange", (_a = chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeTextTrack = (_b = chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeTextTracks.onremovetrack = () => { var _a, _b; if (this._nativeTextTracks !== undefined) { const newTextTracks = createTextTracks(this._nativeTextTracks); if (areTrackArraysDifferent(this._textTracks, newTextTracks)) { this._textTracks = newTextTracks; this._setPreviouslyLockedTextTrack(); this.trigger("availableTextTracksChange", this.getAvailableTextTracks()); const chosenTextTrack = this._getCurrentTextTrack(); if ((chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.nativeTrack) !== this._lastEmittedNativeTextTrack) { this.trigger("textTrackChange", (_a = chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeTextTrack = (_b = chosenTextTrack === null || chosenTextTrack === void 0 ? void 0 : chosenTextTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeTextTracks.onchange = () => { if (this._textTracks !== undefined) { for (let i = 0; i < this._textTracks.length; i++) { const { track, nativeTrack } = this._textTracks[i]; if (nativeTrack.mode === "showing") { if (nativeTrack !== this._lastEmittedNativeTextTrack) { this.trigger("textTrackChange", track); this._lastEmittedNativeTextTrack = nativeTrack; } return; } } } if (this._lastEmittedNativeTextTrack !== null) { this.trigger("textTrackChange", null); this._lastEmittedNativeTextTrack = null; } return; }; } if (this._nativeVideoTracks !== undefined) { this._nativeVideoTracks.onaddtrack = () => { var _a, _b; if (this._nativeVideoTracks !== undefined) { const newVideoTracks = createVideoTracks(this._nativeVideoTracks); if (areTrackArraysDifferent(this._videoTracks, newVideoTracks)) { this._videoTracks = newVideoTracks; this._setPreviouslyLockedVideoTrack(); this.trigger("availableVideoTracksChange", this.getAvailableVideoTracks()); const chosenVideoTrack = this._getCurrentVideoTrack(); if ((chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.nativeTrack) !== this._lastEmittedNativeVideoTrack) { this.trigger("videoTrackChange", (_a = chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeVideoTrack = (_b = chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeVideoTracks.onremovetrack = () => { var _a, _b; if (this._nativeVideoTracks !== undefined) { const newVideoTracks = createVideoTracks(this._nativeVideoTracks); if (areTrackArraysDifferent(this._videoTracks, newVideoTracks)) { this._videoTracks = newVideoTracks; this._setPreviouslyLockedVideoTrack(); this.trigger("availableVideoTracksChange", this.getAvailableVideoTracks()); const chosenVideoTrack = this._getCurrentVideoTrack(); if ((chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.nativeTrack) !== this._lastEmittedNativeVideoTrack) { this.trigger("videoTrackChange", (_a = chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.track) !== null && _a !== void 0 ? _a : null); this._lastEmittedNativeVideoTrack = (_b = chosenVideoTrack === null || chosenVideoTrack === void 0 ? void 0 : chosenVideoTrack.nativeTrack) !== null && _b !== void 0 ? _b : null; } } } }; this._nativeVideoTracks.onchange = () => { if (this._videoTracks !== undefined) { for (let i = 0; i < this._videoTracks.length; i++) { const { track, nativeTrack } = this._videoTracks[i]; if (nativeTrack.selected) { if (nativeTrack !== this._lastEmittedNativeVideoTrack) { this.trigger("videoTrackChange", track); this._lastEmittedNativeVideoTrack = nativeTrack; } return; } } } if (this._lastEmittedNativeVideoTrack !== null) { this.trigger("videoTrackChange", null); this._lastEmittedNativeVideoTrack = null; } return; }; } } /** * Enable an audio track (and disable all others), based on its index in the * `this._audioTracks` array. * @param {number} index} */ _enableAudioTrackFromIndex(index) { enableAudioTrack(this._audioTracks.map(({ nativeTrack }) => nativeTrack), index); } } /** * Disable all text track elements in the given array from showing. * @param {Array.<Object>} textTracks */ function disableTextTracks(textTracks) { for (let i = 0; i < textTracks.length; i++) { const { nativeTrack } = textTracks[i]; nativeTrack.mode = "disabled"; } } /** * Disable all text track elements in the given array from showing but one which * should stay in the same state it was before. * @param {Array.<Object>} textTracks * @param {TextTrack} track */ function disableAllTextTracksBut(textTracks, track) { for (let i = 0; i < textTracks.length; i++) { const { nativeTrack } = textTracks[i]; if (nativeTrack !== track && (nativeTrack.mode === "showing" || nativeTrack.mode === "hidden")) { nativeTrack.mode = "disabled"; } } } /** * Disable all video track elements in the given array from showing. * Note that browser need to support that use case, which they often do not. * @param {Array.<Object>} videoTracks */ function disableVideoTracks(videoTracks) { for (let i = 0; i < videoTracks.length; i++) { const { nativeTrack } = videoTracks[i]; nativeTrack.selected = false; } } /** * Disable all audio track elements in the given array from showing. * Note that browser need to support that use case, which they often do not. * @param {Array.<Object>} audioTracks */ function disableAudioTracks(audioTracks) { for (let i = 0; i < audioTracks.length; i++) { const { nativeTrack } = audioTracks[i]; nativeTrack.enabled = false; } }