rx-player
Version:
Canal+ HTML5 Video Player
873 lines (810 loc) • 31.1 kB
text/typescript
/**
* 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.
*/
/**
* /!\ This file is feature-switchable.
* It always should be imported through the `features` object.
*/
import type {
ICompatAudioTrack,
ICompatAudioTrackList,
ICompatTextTrackList,
ICompatVideoTrack,
ICompatVideoTrackList,
IMediaElement,
} from "../../compat/browser_compatibility_types";
import enableAudioTrack from "../../compat/enable_audio_track";
import type { IRepresentation } from "../../manifest";
import type {
IAudioTrack,
ITextTrack,
IVideoTrack,
IAvailableVideoTrack,
IAvailableAudioTrack,
IAvailableTextTrack,
} from "../../public_types";
import EventEmitter from "../../utils/event_emitter";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import normalizeLanguage from "../../utils/languages";
import TaskCanceller from "../../utils/task_canceller";
/** Events emitted by the MediaElementTracksStore. */
interface IMediaElementTracksStoreEvents {
availableVideoTracksChange: IAvailableVideoTrack[];
availableAudioTracksChange: IAvailableAudioTrack[];
availableTextTracksChange: IAvailableTextTrack[];
videoTrackChange: IVideoTrack | null;
audioTrackChange: IAudioTrack | null;
textTrackChange: ITextTrack | null;
}
/**
* Check if track array is different from an other one
* @param {Array.<Object>} oldTrackArray
* @param {Array.<Object>} newTrackArray
* @returns {boolean}
*/
function areTrackArraysDifferent(
oldTrackArray: Array<{
nativeTrack: ICompatVideoTrack | ICompatAudioTrack | TextTrack;
}>,
newTrackArray: Array<{
nativeTrack: ICompatVideoTrack | ICompatAudioTrack | TextTrack;
}>,
): boolean {
if (newTrackArray.length !== oldTrackArray.length) {
return true;
}
for (let i = 0; i < newTrackArray.length; i++) {
if (newTrackArray[i].nativeTrack !== oldTrackArray[i]?.nativeTrack) {
return true;
}
}
return false;
}
/**
* Create audio tracks from native audio tracks.
* @param {AudioTrackList} audioTracks
* @returns {Array.<Object>}
*/
function createAudioTracks(audioTracks: ICompatAudioTrackList): Array<{
track: {
id: string;
normalized: string;
language: string;
audioDescription: boolean;
representations: IRepresentation[];
};
nativeTrack: ICompatAudioTrack;
}> {
const newAudioTracks = [];
const languagesOccurences: Partial<Record<string, number>> = {};
for (let i = 0; i < audioTracks.length; i++) {
const audioTrack = audioTracks[i];
const language = audioTrack.language === "" ? "nolang" : audioTrack.language;
const occurences = languagesOccurences[language] ?? 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: [] as IRepresentation[],
};
newAudioTracks.push({ track, nativeTrack: audioTrack });
}
return newAudioTracks;
}
/**
* Create text tracks from native text tracks.
* @param {TextTrackList} textTracks
* @returns {Array.<Object>}
*/
function createTextTracks(
textTracks: ICompatTextTrackList,
): Array<{ track: ITextTrack; nativeTrack: TextTrack }> {
const newTextTracks = [];
const languagesOccurences: Partial<Record<string, number>> = {};
for (let i = 0; i < textTracks.length; i++) {
const textTrack = textTracks[i];
const language = textTrack.language === "" ? "nolang" : textTrack.language;
const occurences = languagesOccurences[language] ?? 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 as string) === "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: ICompatVideoTrackList): Array<{
track: { id: string; representations: IRepresentation[] };
nativeTrack: ICompatVideoTrack;
}> {
const newVideoTracks = [];
const languagesOccurences: Partial<Record<string, number>> = {};
for (let i = 0; i < videoTracks.length; i++) {
const videoTrack = videoTracks[i];
const language = videoTrack.language === "" ? "nolang" : videoTrack.language;
const occurences = languagesOccurences[language] ?? 1;
const id = "gen_video_" + language + "_" + occurences.toString();
languagesOccurences[language] = occurences + 1;
newVideoTracks.push({
track: { id, representations: [] as IRepresentation[] },
nativeTrack: videoTrack,
});
}
return newVideoTracks;
}
/**
* Manage video, audio and text tracks for current direct file content.
* @class MediaElementTracksStore
*/
export default class MediaElementTracksStore extends EventEmitter<IMediaElementTracksStoreEvents> {
/** List every available audio tracks available on the media element. */
private _audioTracks: Array<{
track: IAudioTrack;
nativeTrack: ICompatAudioTrack;
}>;
/** List every available text tracks available on the media element. */
private _textTracks: Array<{ track: ITextTrack; nativeTrack: TextTrack }>;
/** List every available video tracks available on the media element. */
private _videoTracks: Array<{
track: IVideoTrack;
nativeTrack: ICompatVideoTrack;
}>;
/** Last audio track emitted as active. */
private _lastEmittedNativeAudioTrack: ICompatAudioTrack | null | undefined;
/** Last video track emitted as active. */
private _lastEmittedNativeVideoTrack: ICompatVideoTrack | null | undefined;
/** Last text track emitted as active. */
private _lastEmittedNativeTextTrack: TextTrack | null | undefined;
/** Native `AudioTrackList` implemented on the media element. */
private _nativeAudioTracks: ICompatAudioTrackList | undefined;
/** Native `VideoTrackList` implemented on the media element. */
private _nativeVideoTracks: ICompatVideoTrackList | undefined;
/** Native `TextTrackList` implemented on the media element. */
private _nativeTextTracks: ICompatTextTrackList | undefined;
/**
* Last audio track manually set active through the corresponding
* MediaElementTracksStore's API(s).
* Allows to "lock on" a track, to be sure that choice will be kept even
* through audio track list updates, as long as it is still available.
* `null` if the audio track was disabled.
* `undefined` if the audio track was not manually set.
*/
private _audioTrackLockedOn: ICompatAudioTrack | undefined | null;
/**
* Last text track manually set active through the corresponding
* MediaElementTracksStore's API(s).
* Allows to "lock on" a track, to be sure that choice will be kept even
* through text track list updates, as long as it is still available.
* `null` if the text track was disabled.
* `undefined` if the text track was not manually set.
*/
private _textTrackLockedOn: TextTrack | undefined | null;
/**
* Last video track manually set active through the corresponding
* MediaElementTracksStore's API(s).
* Allows to "lock on" a track, to be sure that choice will be kept even
* through video track list updates, as long as it is still available.
* `null` if the video track was disabled.
* `undefined` if the video track was not manually set.
*/
private _videoTrackLockedOn: ICompatVideoTrack | undefined | null;
/** Task canceller that is cancel when the MediaTrackStore is disposed */
private taskCanceller: TaskCanceller;
constructor(mediaElement: IMediaElement) {
super();
this.taskCanceller = new TaskCanceller("media_element_track_store");
// 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 as ICompatTextTrackList | undefined;
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 = this._getCurrentAudioTrack()?.nativeTrack;
this._lastEmittedNativeVideoTrack = this._getCurrentVideoTrack()?.nativeTrack;
this._lastEmittedNativeTextTrack = this._getCurrentTextTrack()?.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
*/
public setAudioTrackById(id?: string | number): void {
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.
*/
public disableTextTrack(): void {
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
*/
public setTextTrackById(id?: string | number): void {
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.
*/
public disableVideoTrack(): void {
disableVideoTracks(this._videoTracks);
this._videoTrackLockedOn = null;
}
/**
* Disable the currently-active audio track, if one.
*/
public disableAudioTrack(): void {
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
*/
public setVideoTrackById(id?: string | number): void {
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}
*/
public getChosenAudioTrack(): IAudioTrack | null | undefined {
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}
*/
public getChosenTextTrack(): ITextTrack | null | undefined {
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}
*/
public getChosenVideoTrack(): IVideoTrack | null | undefined {
const currentVideoTrack = this._getCurrentVideoTrack();
return isNullOrUndefined(currentVideoTrack)
? currentVideoTrack
: currentVideoTrack.track;
}
/**
* Returns a description of every available audio tracks.
* @returns {Array.<Object>}
*/
public getAvailableAudioTracks(): IAvailableAudioTrack[] {
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>}
*/
public getAvailableTextTracks(): IAvailableTextTrack[] {
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>}
*/
public getAvailableVideoTracks(): IAvailableVideoTrack[] {
return this._videoTracks.map(({ track, nativeTrack }) => {
return {
id: track.id,
representations: track.representations,
active: nativeTrack.selected,
};
});
}
/**
* Free the resources used by the MediaElementTracksStore.
*/
public dispose(): void {
this.taskCanceller.cancel("disposed");
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}
*/
private _getCurrentAudioTrack():
| { track: IAudioTrack; nativeTrack: ICompatAudioTrack }
| undefined
| null {
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}
*/
private _getCurrentVideoTrack():
| { track: IVideoTrack; nativeTrack: ICompatVideoTrack }
| undefined
| null {
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}
*/
private _getCurrentTextTrack():
| { track: ITextTrack; nativeTrack: TextTrack }
| undefined
| null {
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
*/
private _setPreviouslyLockedAudioTrack(): void {
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.
*/
private _setPreviouslyLockedTextTrack(): void {
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
*/
private _setPreviouslyLockedVideoTrack(): void {
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.
*/
private _handleNativeTracksCallbacks(): void {
if (this._nativeAudioTracks !== undefined) {
const onAddAudioTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeAudioTrack) {
this.trigger("audioTrackChange", chosenAudioTrack?.track ?? null);
this._lastEmittedNativeAudioTrack = chosenAudioTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeAudioTracks.addEventListener?.("addtrack", onAddAudioTrack);
this.taskCanceller.signal.register(() => {
this._nativeAudioTracks?.removeEventListener?.("addtrack", onAddAudioTrack);
});
const onRemoveAudioTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeAudioTrack) {
this.trigger("audioTrackChange", chosenAudioTrack?.track ?? null);
this._lastEmittedNativeAudioTrack = chosenAudioTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeAudioTracks.addEventListener?.("removetrack", onRemoveAudioTrack);
this.taskCanceller.signal.register(() => {
this._nativeAudioTracks?.removeEventListener?.("removetrack", onRemoveAudioTrack);
});
const onAudioChange = () => {
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;
};
this._nativeAudioTracks.addEventListener?.("change", onAudioChange);
this.taskCanceller.signal.register(() => {
this._nativeAudioTracks?.removeEventListener?.("change", onAudioChange);
});
}
if (this._nativeTextTracks !== undefined) {
const onAddTextTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeTextTrack) {
this.trigger("textTrackChange", chosenTextTrack?.track ?? null);
this._lastEmittedNativeTextTrack = chosenTextTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeTextTracks.addEventListener?.("addtrack", onAddTextTrack);
this.taskCanceller.signal.register(() => {
this._nativeTextTracks?.removeEventListener?.("addtrack", onAddTextTrack);
});
const onRemoveTextTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeTextTrack) {
this.trigger("textTrackChange", chosenTextTrack?.track ?? null);
this._lastEmittedNativeTextTrack = chosenTextTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeTextTracks.addEventListener?.("removetrack", onRemoveTextTrack);
this.taskCanceller.signal.register(() => {
this._nativeTextTracks?.removeEventListener?.("removetrack", onRemoveTextTrack);
});
const onTextChange = () => {
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;
};
this._nativeTextTracks.addEventListener?.("change", onTextChange);
this.taskCanceller.signal.register(() => {
this._nativeTextTracks?.removeEventListener?.("change", onTextChange);
});
}
if (this._nativeVideoTracks !== undefined) {
const onAddVideoTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeVideoTrack) {
this.trigger("videoTrackChange", chosenVideoTrack?.track ?? null);
this._lastEmittedNativeVideoTrack = chosenVideoTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeVideoTracks.addEventListener?.("addtrack", onAddVideoTrack);
this.taskCanceller.signal.register(() => {
this._nativeVideoTracks?.removeEventListener?.("addtrack", onAddVideoTrack);
});
const onRemoveVideoTrack = () => {
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?.nativeTrack !== this._lastEmittedNativeVideoTrack) {
this.trigger("videoTrackChange", chosenVideoTrack?.track ?? null);
this._lastEmittedNativeVideoTrack = chosenVideoTrack?.nativeTrack ?? null;
}
}
}
};
this._nativeVideoTracks.addEventListener?.("removetrack", onRemoveVideoTrack);
this.taskCanceller.signal.register(() => {
this._nativeVideoTracks?.removeEventListener?.("removetrack", onRemoveVideoTrack);
});
const onVideoChange = () => {
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;
};
this._nativeVideoTracks.addEventListener?.("change", onVideoChange);
this.taskCanceller.signal.register(() => {
this._nativeVideoTracks?.removeEventListener?.("change", onVideoChange);
});
}
}
/**
* Enable an audio track (and disable all others), based on its index in the
* `this._audioTracks` array.
* @param {number} index}
*/
private _enableAudioTrackFromIndex(index: number): void {
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: Array<{ nativeTrack: TextTrack }>) {
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: Array<{ nativeTrack: TextTrack }>,
track: TextTrack,
) {
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: Array<{ nativeTrack: ICompatVideoTrack }>) {
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: Array<{ nativeTrack: ICompatAudioTrack }>) {
for (let i = 0; i < audioTracks.length; i++) {
const { nativeTrack } = audioTracks[i];
nativeTrack.enabled = false;
}
}