rx-player
Version:
Canal+ HTML5 Video Player
1,092 lines (1,091 loc) • 61.5 kB
JavaScript
/**
* 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 used to abstract the notion of text, audio and video tracks
* switching for an easier API management.
*/
import config from "../../config";
import { MediaError } from "../../errors";
import log from "../../log";
import { getSupportedAdaptations, isRepresentationPlayable, toAudioTrack, toTextTrack, toVideoTrack, } from "../../manifest";
import arrayFind from "../../utils/array_find";
import assert, { assertUnreachable } from "../../utils/assert";
import EventEmitter from "../../utils/event_emitter";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import objectAssign from "../../utils/object_assign";
import SharedReference from "../../utils/reference";
import TrackDispatcher from "./track_dispatcher";
/**
* Class helping with the management of the audio, video and text tracks and
* qualities.
*
* The `TracksStore` allows to choose a track and qualities for different types
* of media through a simpler API.
*
* @class TracksStore
*/
export default class TracksStore extends EventEmitter {
constructor(args) {
var _a;
super();
this._storedPeriodInfo = [];
this._isDisposed = false;
this._cachedPeriodInfo = new WeakMap();
this._isTrickModeTrackEnabled = args.preferTrickModeTracks;
this._defaultAudioTrackSwitchingMode =
(_a = args.defaultAudioTrackSwitchingMode) !== null && _a !== void 0 ? _a : config.getCurrent().DEFAULT_AUDIO_TRACK_SWITCHING_MODE;
this.onTracksNotPlayableForType = args.onTracksNotPlayableForType;
}
/**
* Return Array of Period information, to allow an outside application to
* modify the track of any Period.
* @returns {Array.<Object>}
*/
getAvailablePeriods() {
return this._storedPeriodInfo.reduce((acc, p) => {
if (p.isPeriodAdvertised) {
acc.push(toExposedPeriod(p.period));
}
return acc;
}, []);
}
/**
* Callack that needs to be called as codec support is either first known or
* updated on the Manifest.
*/
onManifestCodecSupportUpdate() {
this._selectInitialTrackIfNeeded();
}
/**
* Check that a period has a supported track for the given track type.
* Trigger an error with `MANIFEST_INCOMPATIBLE_CODECS_ERROR` if no codecs are
* supported and `onTracksNotPlayableForType[ttype]` is "error".
*
* @param period - The period to check
* @param ttype - The track type.
*/
checkPeriodHasSupportedTrack(period, ttype) {
var _a, _b;
const adaptationsForType = period.adaptations[ttype];
const audioVideoAdaptations = [
...((_a = period.adaptations.audio) !== null && _a !== void 0 ? _a : []),
...((_b = period.adaptations.video) !== null && _b !== void 0 ? _b : []),
];
const periodHasAdaptationForType = adaptationsForType !== undefined && adaptationsForType.length > 0;
if (!periodHasAdaptationForType) {
// The content does not have audio/video media, e.g. it's an audio only content.
// there is no track of this type to select.
return;
}
const isTypeUnsupported = adaptationsForType.every((adapt) => adapt.supportStatus.hasSupportedCodec === false);
const isAudioAndVideoUnsupported = audioVideoAdaptations.every((adapt) => adapt.supportStatus.hasSupportedCodec === false);
if (isTypeUnsupported) {
const err = new MediaError("MANIFEST_INCOMPATIBLE_CODECS_ERROR", "No supported " + ttype + " adaptations", { tracks: undefined });
if (isAudioAndVideoUnsupported) {
// Both video and audio are unsupported or the content is audio/video only.
// The content cannot be played without this media type as there is no other media type.
this.trigger("error", err);
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
this.dispose();
return;
}
else if (this.onTracksNotPlayableForType[ttype] === "continue") {
// audio or video is not playable, but let's continue the playback without audio
// or without video because of the option was set to "continue".
this.trigger("warning", err);
}
else {
this.trigger("error", err);
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
this.dispose();
return;
}
}
}
/**
* Update the list of Periods handled by the TracksStore and make a
* track choice decision for each of them.
* @param {Object} manifest - The new Manifest object
*/
onManifestUpdate(manifest) {
var _a, _b, _c;
const { periods } = manifest;
// We assume that they are always sorted chronologically
// In dev mode, perform a runtime check that this is the case
if (0 /* __ENVIRONMENT__.CURRENT_ENV */ === 1 /* __ENVIRONMENT__.DEV */) {
for (let i = 1; i < periods.length; i++) {
assert(periods[i - 1].start <= periods[i].start);
}
}
/** Periods which have just been added. */
const addedPeriods = [];
for (const period of periods) {
["audio", "video"].forEach((ttype) => {
this.checkPeriodHasSupportedTrack(period, ttype);
});
if (this._isDisposed) {
return;
}
}
let newPListIdx = 0;
for (let i = 0; i < this._storedPeriodInfo.length; i++) {
const oldPeriod = this._storedPeriodInfo[i].period;
const newPeriod = periods[newPListIdx];
if (newPeriod === undefined) {
// We reached the end of the new Periods, remove remaining old Periods
for (let j = this._storedPeriodInfo.length - 1; j >= i; j--) {
this._storedPeriodInfo[j].inManifest = false;
if (isPeriodItemRemovable(this._storedPeriodInfo[j])) {
this._removePeriodObject(j);
}
}
}
else if (oldPeriod === newPeriod) {
newPListIdx++;
this.resetSelectedTrackIfNotAvailableAnymore(this._storedPeriodInfo[i], newPeriod, "video");
this.resetSelectedTrackIfNotAvailableAnymore(this._storedPeriodInfo[i], newPeriod, "audio");
this.resetSelectedTrackIfNotAvailableAnymore(this._storedPeriodInfo[i], newPeriod, "text");
// (If not, what do?)
}
else if (oldPeriod.start <= newPeriod.start) {
// This old Period does not exist anymore.
this._storedPeriodInfo[i].inManifest = false;
if (isPeriodItemRemovable(this._storedPeriodInfo[i])) {
this._removePeriodObject(i);
i--;
}
}
else {
const newPeriodInfo = generatePeriodInfo(newPeriod, true);
// oldPeriod.start > newPeriod.start: insert newPeriod before
this._storedPeriodInfo.splice(i, 0, newPeriodInfo);
addedPeriods.push(newPeriodInfo);
newPListIdx++;
// Note: we don't increment `i` on purpose here, as we want to check the
// same oldPeriod at the next loop iteration
}
}
if (newPListIdx < periods.length) {
// Add further new Period
const periodsToAdd = periods
.slice(newPListIdx)
.map((p) => generatePeriodInfo(p, true));
this._storedPeriodInfo.push(...periodsToAdd);
addedPeriods.push(...periodsToAdd);
}
for (const storedPeriodInfo of this._storedPeriodInfo) {
(_a = storedPeriodInfo.audio.dispatcher) === null || _a === void 0 ? void 0 : _a.refresh();
(_b = storedPeriodInfo.video.dispatcher) === null || _b === void 0 ? void 0 : _b.refresh();
(_c = storedPeriodInfo.text.dispatcher) === null || _c === void 0 ? void 0 : _c.refresh();
}
}
onDecipherabilityUpdates() {
var _a, _b, _c;
for (const storedPeriodInfo of this._storedPeriodInfo) {
(_a = storedPeriodInfo.audio.dispatcher) === null || _a === void 0 ? void 0 : _a.refresh();
(_b = storedPeriodInfo.video.dispatcher) === null || _b === void 0 ? void 0 : _b.refresh();
(_c = storedPeriodInfo.text.dispatcher) === null || _c === void 0 ? void 0 : _c.refresh();
}
}
/**
* Reset the stored settings if the track is not available anymore in
* the new manifest.
* @param periodInfo
* @param newPeriod
* @param type
* @returns
*/
resetSelectedTrackIfNotAvailableAnymore(periodInfo, newPeriod, type) {
var _a;
const wantedTrack = periodInfo[type].storedSettings;
if (isNullOrUndefined(wantedTrack)) {
return;
}
const supportedAdaptations = getSupportedAdaptations(newPeriod, type);
const stillHere = supportedAdaptations.some((a) => a.id === wantedTrack.adaptation.id);
if (stillHere) {
// The track still exists, it don't need to be resetted.
return;
}
log.warn(`Track`, `Chosen ${type} Adaptation not available anymore`);
if (type === "video") {
periodInfo.video.storedSettings = this.getDefaultStoredSettingsForAdaptation(supportedAdaptations, "video");
}
else if (type === "audio") {
periodInfo.audio.storedSettings = this.getDefaultStoredSettingsForAdaptation(supportedAdaptations, "audio");
}
else if (type === "text") {
periodInfo.text.storedSettings = this.getDefaultStoredSettingsForAdaptation(supportedAdaptations, "text");
}
this.trigger("trackUpdate", {
period: toExposedPeriod(newPeriod),
trackType: type,
reason: "missing",
});
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // The current TracksStore is disposed, we can abort
}
const periodItem = getPeriodItem(this._storedPeriodInfo, periodInfo.period.id);
if (periodItem !== undefined &&
periodItem.isPeriodAdvertised &&
periodItem[type].storedSettings === null) {
(_a = periodItem[type].dispatcher) === null || _a === void 0 ? void 0 : _a.updateTrack(null);
}
}
getDefaultStoredSettingsForAdaptation(supportedAdaptations, type) {
const { DEFAULT_VIDEO_TRACK_SWITCHING_MODE } = config.getCurrent();
if (type === "text") {
return null;
}
if (supportedAdaptations.length === 0) {
return null;
}
switch (type) {
case "text":
return null;
case "video": {
const adaptationBase = supportedAdaptations[0];
const adaptation = getRightVideoTrack(adaptationBase, this._isTrickModeTrackEnabled);
const lockedRepresentations = new SharedReference(null);
return {
adaptationBase,
adaptation,
switchingMode: DEFAULT_VIDEO_TRACK_SWITCHING_MODE,
lockedRepresentations,
};
}
case "audio": {
return {
adaptation: supportedAdaptations[0],
switchingMode: this._defaultAudioTrackSwitchingMode,
lockedRepresentations: new SharedReference(null),
};
}
default: {
assertUnreachable(type);
}
}
}
/**
* Add shared reference to choose Adaptation for new "audio", "video" or
* "text" Period.
*
* Note that such reference has to be removed through `removeTrackReference`
* so ressources can be freed.
* @param {string} bufferType - The concerned buffer type
* @param {Period} period - The concerned Period.
* @param {Object} adaptationRef - A reference through which
* the choice will be given.
*/
addTrackReference(bufferType, period, adaptationRef) {
log.debug("Track", "Adding Track Reference", { bufferType, periodId: period.id });
let periodObj = getPeriodItem(this._storedPeriodInfo, period.id);
if (periodObj === undefined) {
// The Period has not yet been added.
periodObj = generatePeriodInfo(period, false);
let found = false;
for (let i = 0; i < this._storedPeriodInfo.length; i++) {
if (this._storedPeriodInfo[i].period.start > period.start) {
this._storedPeriodInfo.splice(i, 0, periodObj);
found = true;
}
}
if (!found) {
this._storedPeriodInfo.push(periodObj);
}
}
if (periodObj[bufferType].dispatcher !== null) {
log.error(`Track`, `Subject already added for type and Period`, {
bufferType,
periodId: period.id,
});
periodObj[bufferType].dispatcher.dispose();
}
const dispatcher = new TrackDispatcher(adaptationRef);
periodObj[bufferType].dispatcher = dispatcher;
dispatcher.addEventListener("noPlayableRepresentation", () => {
this.handleMissingOrUnplayableTrack(period, bufferType, false);
});
dispatcher.addEventListener("noPlayableLockedRepresentation", () => {
// TODO check that it doesn't already lead to segment loading or MediaSource
// reloading
if (periodObj === undefined) {
return;
}
this.unlockVideoRepresentations(periodObj);
this.trigger("brokenRepresentationsLock", {
period: { id: period.id, start: period.start, end: period.end },
trackType: bufferType,
});
});
this._selectInitialTrackIfNeeded();
// Ensure `newAvailablePeriods` is sent
if (this._shouldAdvertisePeriod(periodObj)) {
periodObj.isPeriodAdvertised = true;
this.trigger("newAvailablePeriods", [
{
id: period.id,
start: period.start,
end: period.end,
},
]);
if (this._isDisposed) {
return;
}
}
// Ensure the initial track is set for each type now)
const trackTypes = ["audio", "video", "text"];
for (const ttype of trackTypes) {
const trackObj = periodObj[ttype];
if (periodObj.isPeriodAdvertised &&
trackObj.dispatcher !== null &&
!trackObj.dispatcher.hasSetTrack() &&
trackObj.storedSettings !== undefined) {
trackObj.dispatcher.updateTrack(trackObj.storedSettings);
}
if (this._isDisposed) {
return;
}
}
}
/**
* Throws an error if neither audio nor video tracks are selected for the given period.
*
* This indicates that the application or user has not selected any tracks for playback,
* which is considered an invalid state.
*
* @param {object} period - The period to check for selected tracks.
* @returns {void}
* @throws {erorr} If no audio or video tracks are set for the period.
*/
throwIfTracksAreNotSetForPeriod(period) {
const periodItem = getPeriodItem(this._storedPeriodInfo, period.id);
if (periodItem !== undefined) {
const hasNoTrackAtAll = ["audio", "video"].every((ttype) => periodItem[ttype].storedSettings === null);
if (hasNoTrackAtAll) {
const err = new MediaError("NO_AUDIO_VIDEO_TRACKS", "No audio and no video tracks are set.");
this.trigger("error", err);
this.dispose();
}
}
}
/**
* Handles the case when no playable representations are available, or when no initial track has been set,
* for a given period and track type (e.g., `'video'` or `'audio'`).
*
* It attempts to fall back to another available track.
*
* If no fallback tracks are available, the function may either:
* - Throw an error
* - Continue playback by disabling the affected media type (audio/video)
*
* The behavior depends on how the `onVideoTracksNotPlayable` and `onAudioTracksNotPlayable` options are configured.
*
* @param {Object} period - The period object containing the adaptations.
* @param {string} trackType - The type of media track (e.g., `'video'` or `'audio'`) that became unplayable.
*@param {boolean} isInitialSelection - Indicates if this occurs during initial track selection.
* @returns {void}
*/
handleMissingOrUnplayableTrack(period, trackType, isInitialSelection) {
var _a, _b, _c, _d;
const { fallbackTrack, noSourceMedia } = getFallbackTrack(period, trackType);
const typeInfo = (_a = getPeriodItem(this._storedPeriodInfo, period.id)) === null || _a === void 0 ? void 0 : _a[trackType];
if (typeInfo === undefined) {
log.warn("Track", "Could not find period", { periodId: period.id });
return;
}
const initialStoredSettings = typeInfo.storedSettings;
const hasStoredSettingsChanged = () => {
return typeInfo.storedSettings !== initialStoredSettings;
};
if (fallbackTrack !== null) {
let switchingMode;
if (trackType === "audio") {
switchingMode = this._defaultAudioTrackSwitchingMode;
}
else if (trackType === "text") {
switchingMode = "direct";
}
else {
switchingMode = "reload";
}
const storedSettings = {
adaptation: fallbackTrack,
switchingMode,
lockedRepresentations: new SharedReference(null),
};
typeInfo.storedSettings = storedSettings;
if (!isInitialSelection) {
// "trackUpdate" events are not sent for the initial track.
// See documentation: #Player_Events.md, section ###trackUpdate
this.trigger("trackUpdate", {
period: toExposedPeriod(period),
trackType,
reason: "no-playable-representation",
});
}
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
// Check again that no track change occurred in the meantime
if (typeInfo.storedSettings === storedSettings) {
(_b = typeInfo.dispatcher) === null || _b === void 0 ? void 0 : _b.updateTrack(storedSettings);
}
}
else if (fallbackTrack === null && !noSourceMedia) {
this.trigger("noPlayableTrack", {
trackType,
period: { id: period.id, start: period.start, end: period.end },
});
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
const fallbackBehavior = this.onTracksNotPlayableForType[trackType];
if (hasStoredSettingsChanged()) {
// The previous "noPlayableTrack" event might have caused changes,
// so we re-check to see if the selected track has been updated.
// If it has, we exit early because the API consumer likely adjusted the settings,
// and throwing an error now would be out of sync with their changes.
}
else if (fallbackBehavior === "continue") {
log.warn("Track", `No playable ${trackType}, continuing without ${trackType}`);
typeInfo.storedSettings = null;
if (!isInitialSelection) {
// "trackUpdate" events are not sent for the initial track.
// See documentation: #Player_Events.md, section ###trackUpdate
this.trigger("trackUpdate", {
period: toExposedPeriod(period),
trackType,
reason: "no-playable-representation",
});
}
if (typeInfo.storedSettings !== null || this._isDisposed) {
// The previous "trackUpdate" event might have caused changes,
// so we re-check to see if the selected track has been updated.
// If it has, we exit early because the API consumer likely adjusted the settings,
// and throwing an error now would be out of sync with their changes.
}
else {
(_c = typeInfo.dispatcher) === null || _c === void 0 ? void 0 : _c.updateTrack(null);
}
}
else if (fallbackBehavior === "error") {
const noRepErr = new MediaError("NO_PLAYABLE_REPRESENTATION", `No ${trackType} Representation can be played`, { tracks: undefined });
this.trigger("error", noRepErr);
}
}
else if (fallbackTrack === null && noSourceMedia) {
log.debug(`Track`, `The period does not have adaptation for ${trackType} there is no track to choose`);
typeInfo.storedSettings = null;
if (!isInitialSelection) {
// "trackUpdate" events are not sent for the initial track.
// See documentation: #Player_Events.md, section ###trackUpdate
this.trigger("trackUpdate", {
period: toExposedPeriod(period),
trackType,
reason: "no-playable-representation",
});
}
if (typeInfo.storedSettings !== null || this._isDisposed) {
// The previous "trackUpdate" event might have caused changes,
// so we re-check to see if the selected track has been updated.
// If it has, we exit early because the API consumer likely adjusted the settings,
// and throwing an error now would be out of sync with their changes.
}
else {
(_d = typeInfo.dispatcher) === null || _d === void 0 ? void 0 : _d.updateTrack(null);
}
}
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
if (trackType !== "text") {
// Allow missing text tracks; only enforce for audio/video
this.throwIfTracksAreNotSetForPeriod(period);
}
}
/**
* Remove shared reference to choose an "audio", "video" or "text" Adaptation
* for a Period.
* @param {string} bufferType - The concerned buffer type
* @param {string} periodId - The concerned Period's `id`.
*/
removeTrackReference(bufferType, periodId) {
log.debug("Track", "Removing Track Reference", { bufferType, periodId });
let periodIndex;
for (let i = 0; i < this._storedPeriodInfo.length; i++) {
const periodI = this._storedPeriodInfo[i];
if (periodI.period.id === periodId) {
periodIndex = i;
break;
}
}
if (periodIndex === undefined) {
log.warn(`Track`, `type not found for period`, { bufferType, periodId });
return;
}
const periodObj = this._storedPeriodInfo[periodIndex];
const choiceItem = periodObj[bufferType];
if ((choiceItem === null || choiceItem === void 0 ? void 0 : choiceItem.dispatcher) === null) {
log.warn(`Track`, `TrackDispatcher already removed for type and Period`, {
bufferType,
periodId,
});
return;
}
choiceItem.dispatcher.dispose();
choiceItem.dispatcher = null;
if (isPeriodItemRemovable(periodObj)) {
this._removePeriodObject(periodIndex);
}
}
/**
* Allows to recuperate a "Period Object" - used in get/set methods of the
* `TracksStore` - by giving the Period itself.
*
* This method should be preferred when possible over `getPeriodObjectFromId`
* because it is able to fallback on an internal cache in case the
* corresponding Period is not stored anymore.
* This for example could happen when a Period has been removed from the
* Manifest yet may still be needed (e.g. because its linked segments might
* still live in the buffers).
*
* Note however that this cache-retrieval logic is based on a Map whose key
* is the Period's JavaScript reference. As such, the cache won't be used if
* `Period` corresponds to a copy of the original `Period` object.
*
* @param {Object} period
* @returns {Object}
*/
getPeriodObjectFromPeriod(period) {
const periodObj = getPeriodItem(this._storedPeriodInfo, period.id);
if (periodObj === undefined && period !== undefined) {
return this._cachedPeriodInfo.get(period);
}
return periodObj;
}
/**
* Allows to recuperate a "Period Object" - used in get/set methods of the
* `TracksStore` - by giving the Period's id.
*
* Note that unlike `getPeriodObjectFromPeriod` this method is only going to look
* into currently stored Period and as such old Periods not in the Manifest
* anymore might not be retrievable.
* If you want to retrieve Period objects linked to such Period, you might
* prefer to use `getPeriodObjectFromPeriod` (which necessitates the original
* Period object).
*
* @param {string} periodId - The concerned Period's id
* @returns {Object}
*/
getPeriodObjectFromId(periodId) {
return getPeriodItem(this._storedPeriodInfo, periodId);
}
disableVideoTrickModeTracks() {
if (!this._isTrickModeTrackEnabled) {
return;
}
this._isTrickModeTrackEnabled = false;
this._resetVideoTrackChoices("trickmode-disabled");
}
enableVideoTrickModeTracks() {
if (this._isTrickModeTrackEnabled) {
return;
}
this._isTrickModeTrackEnabled = true;
this._resetVideoTrackChoices("trickmode-enabled");
}
/**
* Reset the TracksStore's Period objects:
* - All Period which are not in the manifest currently will be removed.
* - All References used to communicate the wanted track will be removed.
*
* You might want to call this API when restarting playback.
*/
resetPeriodObjects() {
var _a, _b, _c;
log.debug("Track", "Resetting Period Objects");
for (let i = this._storedPeriodInfo.length - 1; i >= 0; i--) {
const storedObj = this._storedPeriodInfo[i];
(_a = storedObj.audio.dispatcher) === null || _a === void 0 ? void 0 : _a.dispose();
storedObj.audio.dispatcher = null;
(_b = storedObj.video.dispatcher) === null || _b === void 0 ? void 0 : _b.dispose();
storedObj.video.dispatcher = null;
(_c = storedObj.text.dispatcher) === null || _c === void 0 ? void 0 : _c.dispose();
storedObj.text.dispatcher = null;
if (!storedObj.inManifest) {
this._removePeriodObject(i);
}
}
}
/**
* @returns {boolean}
*/
isTrickModeEnabled() {
return this._isTrickModeTrackEnabled;
}
/**
* Set audio track based on the ID of its Adaptation for a given added Period.
* @param {Object} params
* @param {Object} params.periodRef - The concerned Period's object.
* @param {string} params.trackId - adaptation id of the wanted track.
* @param {string} params.switchingMode - Behavior when replacing the track by
* another.
* @param {Object|null} params.lockedRepresentations - Audio Representations
* that should be locked after switching to that track.
* `null` if no Audio Representation should be locked.
* @param {number} params.relativeResumingPosition
*/
setAudioTrack(payload) {
const { periodRef, trackId, switchingMode, lockedRepresentations, relativeResumingPosition, } = payload;
return this._setAudioOrTextTrack({
bufferType: "audio",
periodRef,
trackId,
switchingMode: switchingMode !== null && switchingMode !== void 0 ? switchingMode : this._defaultAudioTrackSwitchingMode,
lockedRepresentations,
relativeResumingPosition,
});
}
/**
* Set text track based on the ID of its Adaptation for a given added Period.
* @param {Object} periodObj - The concerned Period's object.
* @param {string} wantedId - adaptation id of the wanted track.
*/
setTextTrack(periodObj, wantedId) {
return this._setAudioOrTextTrack({
bufferType: "text",
periodRef: periodObj,
trackId: wantedId,
switchingMode: "direct",
lockedRepresentations: null,
relativeResumingPosition: undefined,
});
}
/**
* Set audio track based on the ID of its Adaptation for a given added Period.
* @param {Object} params
* @param {string} params.bufferType
* @param {Object} params.periodRef - The concerned Period's object.
* @param {string} params.trackId - adaptation id of the wanted track.
* @param {string} params.switchingMode - Behavior when replacing the track by
* another.
* @param {Array.<string>|null} params.lockedRepresentations - Audio
* Representations that should be locked after switchingMode to that track.
* `null` if no Audio Representation should be locked.
* @param {number|undefined} params.relativeResumingPosition
*/
_setAudioOrTextTrack({ bufferType, periodRef, trackId, switchingMode, lockedRepresentations, relativeResumingPosition, }) {
var _a, _b;
if (!periodRef.isPeriodAdvertised) {
throw new Error("Wanted Period not yet advertised.");
}
const period = periodRef.period;
const wantedAdaptation = arrayFind((_a = period.adaptations[bufferType]) !== null && _a !== void 0 ? _a : [], ({ id, supportStatus }) => supportStatus.hasSupportedCodec !== false &&
supportStatus.isDecipherable !== false &&
id === trackId);
if (wantedAdaptation === undefined) {
throw new Error(`Wanted ${bufferType} track not found.`);
}
const typeInfo = periodRef[bufferType];
let lockedRepresentationsRef;
if (lockedRepresentations === null) {
lockedRepresentationsRef = new SharedReference(null);
}
else {
const representationsToLock = this._getRepresentationsToLock(wantedAdaptation, lockedRepresentations);
const repSwitchingMode = bufferType === "audio"
? this._defaultAudioTrackSwitchingMode
: "direct";
lockedRepresentationsRef = new SharedReference({
representationIds: representationsToLock,
switchingMode: repSwitchingMode,
});
}
const storedSettings = {
adaptation: wantedAdaptation,
switchingMode,
lockedRepresentations: lockedRepresentationsRef,
relativeResumingPosition,
};
typeInfo.storedSettings = storedSettings;
this.trigger("trackUpdate", {
period: toExposedPeriod(period),
trackType: bufferType,
reason: "manual",
});
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
const newPeriodItem = getPeriodItem(this._storedPeriodInfo, period.id);
if (newPeriodItem !== undefined &&
newPeriodItem[bufferType].storedSettings === storedSettings) {
(_b = newPeriodItem[bufferType].dispatcher) === null || _b === void 0 ? void 0 : _b.updateTrack(storedSettings);
}
}
/**
* Set video track based on the ID of its Adaptation for a given added Period.
* @param {Object} params
* @param {Object} params.periodRef - The concerned Period's object.
* @param {string} params.trackId - adaptation id of the wanted track.
* @param {string} params.switchingMode - Behavior when replacing the track by
* another.
* @param {Array.<string>|null} params.lockedRepresentations - Video
* Representations that should be locked after switching to that track.
* `null` if no Video Representation should be locked.
* @param {number|undefined} params.relativeResumingPosition
*/
setVideoTrack(payload) {
var _a, _b;
const { periodRef, trackId, switchingMode, lockedRepresentations, relativeResumingPosition, } = payload;
if (!periodRef.isPeriodAdvertised) {
throw new Error("Wanted Period not yet advertised.");
}
const period = periodRef.period;
const wantedAdaptation = arrayFind((_a = period.adaptations.video) !== null && _a !== void 0 ? _a : [], ({ id, supportStatus }) => supportStatus.isDecipherable !== false &&
supportStatus.hasSupportedCodec !== false &&
id === trackId);
if (wantedAdaptation === undefined) {
throw new Error("Wanted video track not found.");
}
const { DEFAULT_VIDEO_TRACK_SWITCHING_MODE } = config.getCurrent();
const typeInfo = periodRef.video;
const newAdaptation = getRightVideoTrack(wantedAdaptation, this._isTrickModeTrackEnabled);
let lockedRepresentationsRef;
if (lockedRepresentations === null) {
lockedRepresentationsRef = new SharedReference(null);
}
else {
const representationsToLock = this._getRepresentationsToLock(wantedAdaptation, lockedRepresentations);
const repSwitchingMode = DEFAULT_VIDEO_TRACK_SWITCHING_MODE;
lockedRepresentationsRef = new SharedReference({
representationIds: representationsToLock,
switchingMode: repSwitchingMode,
});
}
const storedSettings = {
adaptationBase: wantedAdaptation,
switchingMode: switchingMode !== null && switchingMode !== void 0 ? switchingMode : DEFAULT_VIDEO_TRACK_SWITCHING_MODE,
adaptation: newAdaptation,
relativeResumingPosition,
lockedRepresentations: lockedRepresentationsRef,
};
typeInfo.storedSettings = storedSettings;
this.trigger("trackUpdate", {
period: toExposedPeriod(period),
trackType: "video",
reason: "manual",
});
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
const newPeriodItem = getPeriodItem(this._storedPeriodInfo, period.id);
if (newPeriodItem !== undefined &&
newPeriodItem.video.storedSettings === storedSettings) {
(_b = newPeriodItem.video.dispatcher) === null || _b === void 0 ? void 0 : _b.updateTrack(storedSettings);
}
}
/**
* Disable the current text track for a given period.
*
* @param {Object} periodObj - The concerned Period's object
* @param {string} bufferType - The type of track to disable.
* @throws Error - Throws if the period given has not been added
*/
disableTrack(periodObj, bufferType) {
var _a, _b;
if (!periodObj.isPeriodAdvertised) {
throw new Error("Wanted Period not yet advertised.");
}
const trackInfo = periodObj[bufferType];
if (trackInfo.storedSettings === null) {
return;
}
if (bufferType !== "text") {
// Potentially unneeded, but let's be clean
(_a = periodObj[bufferType].storedSettings) === null || _a === void 0 ? void 0 : _a.lockedRepresentations.finish();
}
trackInfo.storedSettings = null;
this.trigger("trackUpdate", {
period: toExposedPeriod(periodObj.period),
trackType: bufferType,
reason: "manual",
});
// The previous event trigger could have had side-effects, so we
// re-check if we're still mostly in the same state
if (this._isDisposed) {
return; // Someone disposed the `TracksStore` on the previous side-effect
}
const newPeriodItem = getPeriodItem(this._storedPeriodInfo, periodObj.period.id);
if (newPeriodItem !== undefined &&
newPeriodItem[bufferType].storedSettings === null) {
(_b = newPeriodItem[bufferType].dispatcher) === null || _b === void 0 ? void 0 : _b.updateTrack(null);
}
if (newPeriodItem !== undefined) {
const hasNoTrackAtAll = ["audio", "video"].every((ttype) => newPeriodItem[ttype].storedSettings === null);
if (hasNoTrackAtAll) {
const err = new MediaError("NO_AUDIO_VIDEO_TRACKS", "No audio and no video tracks are set.");
this.trigger("error", err);
this.dispose();
return;
}
}
}
/**
* Returns an object describing the chosen audio track for the given audio
* Period.
*
* Returns `null` is the the current audio track is disabled or not
* set yet.a pas bcp de marge de manoeuvre j'ai l'impression
*
* Returns `undefined` if the given Period's id is not currently found in the
* `TracksStore`. The cause being most probably that the corresponding
* Period is not available anymore.
* If you're in that case and if still have the corresponding JavaScript
* reference to the wanted Period, you can call `getOldAudioTrack` with it. It
* will try retrieving the choice it made from its cache.
* @param {Object} periodObj - The concerned Period's object
* @returns {Object|null|undefined} - The audio track chosen for this Period.
* `null` if audio tracks were disabled and `undefined` if the Period is not
* known.
*/
getChosenAudioTrack(periodObj, filterPlayableRepresentations) {
return isNullOrUndefined(periodObj.audio.storedSettings)
? null
: toAudioTrack(periodObj.audio.storedSettings.adaptation, filterPlayableRepresentations);
}
/**
* Returns an object describing the chosen text track for the given text
* Period.
*
* Returns null is the the current text track is disabled or not
* set yet.
*
* @param {Object} periodObj - The concerned Period's object
* @returns {Object|null} - The text track chosen for this Period
*/
getChosenTextTrack(periodObj) {
return isNullOrUndefined(periodObj.text.storedSettings)
? null
: toTextTrack(periodObj.text.storedSettings.adaptation);
}
/**
* Returns an object describing the chosen video track for the given video
* Period.
*
* Returns null is the the current video track is disabled or not
* set yet.
*
* @param {Object} periodObj - The concerned Period's object
* @returns {Object|null} - The video track chosen for this Period
*/
getChosenVideoTrack(periodObj, filterPlayableRepresentations) {
if (isNullOrUndefined(periodObj.video.storedSettings)) {
return null;
}
return toVideoTrack(periodObj.video.storedSettings.adaptation, filterPlayableRepresentations);
}
/**
* Returns all available audio tracks for a given Period, as an array of
* objects.
*
* Returns `undefined` if the given Period's id is not known.
*
* @param {Object} periodObj - The concerned Period's object
* @param {boolean} filterPlayableRepresentations - If `true`, only
* representations considered to be "playable" will be included in the
* returned response.
* If `false`, the response should contain all linked representations.
* @returns {Array.<Object>}
*/
getAvailableAudioTracks(periodObj, filterPlayableRepresentations) {
const storedSettings = periodObj.audio.storedSettings;
const currentId = !isNullOrUndefined(storedSettings)
? storedSettings.adaptation.id
: null;
const adaptations = getSupportedAdaptations(periodObj.period, "audio");
return adaptations.map((adaptation) => {
const active = currentId === null ? false : currentId === adaptation.id;
return objectAssign(toAudioTrack(adaptation, filterPlayableRepresentations), {
active,
});
});
}
/**
* Returns all available text tracks for a given Period, as an array of
* objects.
*
* Returns `undefined` if the given Period's id is not known.
*
* @param {Object} periodObj - The concerned Period's object
* @returns {Array.<Object>}
*/
getAvailableTextTracks(periodObj) {
const storedSettings = periodObj.text.storedSettings;
const currentId = !isNullOrUndefined(storedSettings)
? storedSettings.adaptation.id
: null;
const adaptations = getSupportedAdaptations(periodObj.period, "text");
return adaptations.map((adaptation) => {
const active = currentId === null ? false : currentId === adaptation.id;
return objectAssign(toTextTrack(adaptation), { active });
});
}
/**
* Returns all available video tracks for a given Period, as an array of
* objects.
*
* Returns `undefined` if the given Period's id is not known.
*
* @param {Object} periodObj - The concerned Period's object
* @param {boolean} filterPlayableRepresentations - If `true`, only
* representations considered to be "playable" will be included in the
* returned response.
* If `false`, the response should contain all linked representations.
* @returns {Array.<Object>}
*/
getAvailableVideoTracks(periodObj, filterPlayableRepresentations) {
const storedSettings = periodObj.video.storedSettings;
const currentId = isNullOrUndefined(storedSettings)
? undefined
: storedSettings.adaptation.id;
const adaptations = getSupportedAdaptations(periodObj.period, "video");
return adaptations.map((adaptation) => {
const active = currentId === null ? false : currentId === adaptation.id;
const track = toVideoTrack(adaptation, filterPlayableRepresentations);
const trickModeTracks = track.trickModeTracks !== undefined
? track.trickModeTracks.map((trickModeAdaptation) => {
const isActive = currentId === null ? false : currentId === trickModeAdaptation.id;
return objectAssign(trickModeAdaptation, { active: isActive });
})
: [];
const availableTrack = objectAssign(track, { active });
if (trickModeTracks !== undefined) {
availableTrack.trickModeTracks = trickModeTracks;
}
return availableTrack;
});
}
getLockedAudioRepresentations(periodObj) {
const { storedSettings } = periodObj.audio;
if (isNullOrUndefined(storedSettings)) {
return null;
}
const lastLockedSettings = storedSettings.lockedRepresentations.getValue();
return lastLockedSettings === null ? null : lastLockedSettings.representationIds;
}
getLockedVideoRepresentations(periodObj) {
const { storedSettings } = periodObj.video;
if (isNullOrUndefined(storedSettings)) {
return null;
}
const lastLockedSettings = storedSettings.lockedRepresentations.getValue();
return lastLockedSettings === null ? null : lastLockedSettings.representationIds;
}
lockAudioRepresentations(periodObj, lockSettings) {
var _a;
const { storedSettings } = periodObj.audio;
if (isNullOrUndefined(storedSettings)) {
return;
}
const { DEFAULT_AUDIO_REPRESENTATIONS_SWITCHING_MODE } = config.getCurrent();
const filtered = this._getRepresentationsToLock(storedSettings.adaptation, lockSettings.representations);
const switchingMode = (_a = lockSettings.switchingMode) !== null && _a !== void 0 ? _a : DEFAULT_AUDIO_REPRESENTATIONS_SWITCHING_MODE;
storedSettings.lockedRepresentations.setValue({
representationIds: filtered,
switchingMode,
});
}
lockVideoRepresentations(periodObj, lockSettings) {
var _a;
const { storedSettings } = periodObj.video;
if (isNullOrUndefined(storedSettings)) {
return;
}
const { DEFAULT_VIDEO_REPRESENTATIONS_SWITCHING_MODE } = config.getCurrent();
const filtered = this._getRepresentationsToLock(storedSettings.adaptation, lockSettings.representations);
const switchingMode = (_a = lockSettings.switchingMode) !== null && _a !== void 0 ? _a : DEFAULT_VIDEO_REPRESENTATIONS_SWITCHING_MODE;
storedSettings.lockedRepresentations.setValue({
representationIds: filtered,
switchingMode,
});
}
unlockAudioRepresentations(periodObj) {
const { storedSettings } = periodObj.audio;
if (isNullOrUndefined(storedSettings) ||
storedSettings.lockedRepresentations.getValue() === null) {
return;
}
storedSettings.lockedRepresentations.setValue(null);
}
unlockVideoRepresentations(periodObj) {
const { storedSettings } = periodObj.video;
if (isNullOrUndefined(storedSettings) ||
storedSettings.lockedRepresentations.getValue() === null) {
return;
}
storedSettings.lockedRepresentations.setValue(null);
}
dispose() {
this._isDisposed = true;
while (true) {
const lastPeriod = this._storedPeriodInfo.pop();
if (lastPeriod === undefined) {
return;
}
lastPeriod.isRemoved = true;
}
}
_resetVideoTrackChoices(reason) {
var _a;
for (let i = 0; i < this._storedPeriodInfo.length; i++) {
const periodObj = this._storedPeriodInfo[i];
if (!isNullOrUndefined(periodObj.video.storedSettings)) {
const chosenBaseTrack = periodObj.video.storedSettings.adaptationBase;
if (chosenBaseTrack !== null) {
const chosenTrack = getRightVideoTrack(chosenBaseTrack, this._isTrickModeTrackEnabled);
periodObj.video.storedSettings.adaptationBase = chosenBaseTrack;
periodObj.video.storedSettings.adaptation = chosenTrack;
}
}
}
// Clone the current Period list to not be influenced if Periods are removed
// or added while the loop is running.
const sliced = this._storedPeriodInfo.slice();
for (let i = 0; i < sliced.length; i++) {
const period = sliced[i].period;
const videoItem = sliced[i].video;
const storedSettings = videoItem.storedSettings;