in key message`);
- }
- return strToUtf8array(atob(challengeText));
- }
- setupLicenseXHR(xhr, url, keysListItem, licenseChallenge) {
- const licenseXhrSetup = this.config.licenseXhrSetup;
- if (!licenseXhrSetup) {
- xhr.open('POST', url, true);
- return Promise.resolve({
- xhr,
- licenseChallenge
- });
- }
- return Promise.resolve().then(() => {
- if (!keysListItem.decryptdata) {
- throw new Error('Key removed');
- }
- return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge);
- }).catch(error => {
- if (!keysListItem.decryptdata) {
- // Key session removed. Cancel license request.
- throw error;
- }
- // let's try to open before running setup
- xhr.open('POST', url, true);
- return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge);
- }).then(licenseXhrSetupResult => {
- // if licenseXhrSetup did not yet call open, let's do it now
- if (!xhr.readyState) {
- xhr.open('POST', url, true);
- }
- const finalLicenseChallenge = licenseXhrSetupResult ? licenseXhrSetupResult : licenseChallenge;
- return {
- xhr,
- licenseChallenge: finalLicenseChallenge
- };
- });
- }
- requestLicense(keySessionContext, licenseChallenge) {
- const keyLoadPolicy = this.config.keyLoadPolicy.default;
- return new Promise((resolve, reject) => {
- const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem);
- this.log(`Sending license request to URL: ${url}`);
- const xhr = new XMLHttpRequest();
- xhr.responseType = 'arraybuffer';
- xhr.onreadystatechange = () => {
- if (!this.hls || !keySessionContext.mediaKeysSession) {
- return reject(new Error('invalid state'));
- }
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- this._requestLicenseFailureCount = 0;
- let data = xhr.response;
- this.log(`License received ${data instanceof ArrayBuffer ? data.byteLength : data}`);
- const licenseResponseCallback = this.config.licenseResponseCallback;
- if (licenseResponseCallback) {
- try {
- data = licenseResponseCallback.call(this.hls, xhr, url, keySessionContext);
- } catch (error) {
- this.error(error);
- }
- }
- resolve(data);
- } else {
- const retryConfig = keyLoadPolicy.errorRetry;
- const maxNumRetry = retryConfig ? retryConfig.maxNumRetry : 0;
- this._requestLicenseFailureCount++;
- if (this._requestLicenseFailureCount > maxNumRetry || xhr.status >= 400 && xhr.status < 500) {
- reject(new EMEKeyError({
- type: ErrorTypes.KEY_SYSTEM_ERROR,
- details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
- decryptdata: keySessionContext.decryptdata,
- fatal: true,
- networkDetails: xhr,
- response: {
- url,
- data: undefined,
- code: xhr.status,
- text: xhr.statusText
- }
- }, `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`));
- } else {
- const attemptsLeft = maxNumRetry - this._requestLicenseFailureCount + 1;
- this.warn(`Retrying license request, ${attemptsLeft} attempts left`);
- this.requestLicense(keySessionContext, licenseChallenge).then(resolve, reject);
- }
- }
- }
- };
- if (keySessionContext.licenseXhr && keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE) {
- keySessionContext.licenseXhr.abort();
- }
- keySessionContext.licenseXhr = xhr;
- this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge).then(({
- xhr,
- licenseChallenge
- }) => {
- if (keySessionContext.keySystem == KeySystems.PLAYREADY) {
- licenseChallenge = this.unpackPlayReadyKeyMessage(xhr, licenseChallenge);
- }
- xhr.send(licenseChallenge);
- }).catch(reject);
- });
- }
- onDestroying() {
- this.unregisterListeners();
- this._clear();
- }
- onMediaAttached(event, data) {
- if (!this.config.emeEnabled) {
- return;
- }
- const media = data.media;
-
- // keep reference of media
- this.media = media;
- addEventListener(media, 'encrypted', this.onMediaEncrypted);
- addEventListener(media, 'waitingforkey', this.onWaitingForKey);
- const mediaResolved = this.mediaResolved;
- if (mediaResolved) {
- mediaResolved();
- } else {
- this.mediaKeys = media.mediaKeys;
- }
- }
- onMediaDetached() {
- const media = this.media;
- if (media) {
- removeEventListener(media, 'encrypted', this.onMediaEncrypted);
- removeEventListener(media, 'waitingforkey', this.onWaitingForKey);
- this.media = null;
- this.mediaKeys = null;
- }
- }
- _clear() {
- var _media$setMediaKeys;
- this._requestLicenseFailureCount = 0;
- this.keyIdToKeySessionPromise = {};
- this.bannedKeyIds = {};
- const mediaResolved = this.mediaResolved;
- if (mediaResolved) {
- mediaResolved();
- }
- if (!this.mediaKeys && !this.mediaKeySessions.length) {
- return;
- }
- const media = this.media;
- const mediaKeysList = this.mediaKeySessions.slice();
- this.mediaKeySessions = [];
- this.mediaKeys = null;
- LevelKey.clearKeyUriToKeyIdMap();
-
- // Close all sessions and remove media keys from the video element.
- const keySessionCount = mediaKeysList.length;
- EMEController.CDMCleanupPromise = Promise.all(mediaKeysList.map(mediaKeySessionContext => this.removeSession(mediaKeySessionContext)).concat((media == null || (_media$setMediaKeys = media.setMediaKeys(null)) == null ? void 0 : _media$setMediaKeys.catch(error => {
- this.log(`Could not clear media keys: ${error}`);
- if (!this.hls) return;
- this.hls.trigger(Events.ERROR, {
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR,
- fatal: false,
- error: new Error(`Could not clear media keys: ${error}`)
- });
- })) || Promise.resolve())).catch(error => {
- this.log(`Could not close sessions and clear media keys: ${error}`);
- if (!this.hls) return;
- this.hls.trigger(Events.ERROR, {
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,
- fatal: false,
- error: new Error(`Could not close sessions and clear media keys: ${error}`)
- });
- }).then(() => {
- if (keySessionCount) {
- this.log('finished closing key sessions and clearing media keys');
- }
- });
- }
- onManifestLoading() {
- this._clear();
- }
- onManifestLoaded(event, {
- sessionKeys
- }) {
- if (!sessionKeys || !this.config.emeEnabled) {
- return;
- }
- if (!this.keyFormatPromise) {
- const keyFormats = sessionKeys.reduce((formats, sessionKey) => {
- if (formats.indexOf(sessionKey.keyFormat) === -1) {
- formats.push(sessionKey.keyFormat);
- }
- return formats;
- }, []);
- this.log(`Selecting key-system from session-keys ${keyFormats.join(', ')}`);
- this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
- }
- }
- removeSession(mediaKeySessionContext) {
- const {
- mediaKeysSession,
- licenseXhr,
- decryptdata
- } = mediaKeySessionContext;
- if (mediaKeysSession) {
- this.log(`Remove licenses and keys and close session "${mediaKeysSession.sessionId}" keyId: ${arrayToHex((decryptdata == null ? void 0 : decryptdata.keyId) || [])}`);
- if (mediaKeySessionContext._onmessage) {
- mediaKeysSession.removeEventListener('message', mediaKeySessionContext._onmessage);
- mediaKeySessionContext._onmessage = undefined;
- }
- if (mediaKeySessionContext._onkeystatuseschange) {
- mediaKeysSession.removeEventListener('keystatuseschange', mediaKeySessionContext._onkeystatuseschange);
- mediaKeySessionContext._onkeystatuseschange = undefined;
- }
- if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) {
- licenseXhr.abort();
- }
- mediaKeySessionContext.mediaKeysSession = mediaKeySessionContext.decryptdata = mediaKeySessionContext.licenseXhr = undefined;
- const index = this.mediaKeySessions.indexOf(mediaKeySessionContext);
- if (index > -1) {
- this.mediaKeySessions.splice(index, 1);
- }
- const {
- keyStatusTimeouts
- } = mediaKeySessionContext;
- if (keyStatusTimeouts) {
- Object.keys(keyStatusTimeouts).forEach(keyId => self.clearTimeout(keyStatusTimeouts[keyId]));
- }
- const {
- drmSystemOptions
- } = this.config;
- const removePromise = isPersistentSessionType(drmSystemOptions) ? new Promise((resolve, reject) => {
- self.setTimeout(() => reject(new Error(`MediaKeySession.remove() timeout`)), 8000);
- mediaKeysSession.remove().then(resolve).catch(reject);
- }) : Promise.resolve();
- return removePromise.catch(error => {
- this.log(`Could not remove session: ${error}`);
- if (!this.hls) return;
- this.hls.trigger(Events.ERROR, {
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR,
- fatal: false,
- error: new Error(`Could not remove session: ${error}`)
- });
- }).then(() => {
- return mediaKeysSession.close();
- }).catch(error => {
- this.log(`Could not close session: ${error}`);
- if (!this.hls) return;
- this.hls.trigger(Events.ERROR, {
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,
- fatal: false,
- error: new Error(`Could not close session: ${error}`)
- });
- });
- }
- return Promise.resolve();
- }
-}
-EMEController.CDMCleanupPromise = void 0;
-function getKeyIdString(decryptdata) {
- if (!decryptdata) {
- throw new Error('Could not read keyId of undefined decryptdata');
- }
- if (decryptdata.keyId === null) {
- throw new Error('keyId is null');
- }
- return arrayToHex(decryptdata.keyId);
-}
-function getKeyStatus(decryptdata, keyContext) {
- if (decryptdata.keyId && keyContext.mediaKeysSession.keyStatuses.has(decryptdata.keyId)) {
- return keyContext.mediaKeysSession.keyStatuses.get(decryptdata.keyId);
- }
- if (decryptdata.matches(keyContext.decryptdata)) {
- return keyContext.keyStatus;
- }
- return undefined;
-}
-class EMEKeyError extends Error {
- constructor(data, message) {
- super(message);
- this.data = void 0;
- data.error || (data.error = new Error(message));
- this.data = data;
- data.err = data.error;
- }
-}
-function getKeyStatusError(keyStatus, decryptdata) {
- const outputRestricted = keyStatus === 'output-restricted';
- const details = outputRestricted ? ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED : ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR;
- return new EMEKeyError({
- type: ErrorTypes.KEY_SYSTEM_ERROR,
- details,
- fatal: false,
- decryptdata
- }, outputRestricted ? 'HDCP level output restricted' : `key status changed to "${keyStatus}"`);
-}
-
-class FPSController {
- constructor(hls) {
- this.hls = void 0;
- this.isVideoPlaybackQualityAvailable = false;
- this.timer = void 0;
- this.media = null;
- this.lastTime = void 0;
- this.lastDroppedFrames = 0;
- this.lastDecodedFrames = 0;
- // stream controller must be provided as a dependency!
- this.streamController = void 0;
- this.hls = hls;
- this.registerListeners();
- }
- setStreamController(streamController) {
- this.streamController = streamController;
- }
- registerListeners() {
- this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
- this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- }
- unregisterListeners() {
- this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
- this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- }
- destroy() {
- if (this.timer) {
- clearInterval(this.timer);
- }
- this.unregisterListeners();
- this.isVideoPlaybackQualityAvailable = false;
- this.media = null;
- }
- onMediaAttaching(event, data) {
- const config = this.hls.config;
- if (config.capLevelOnFPSDrop) {
- const media = data.media instanceof self.HTMLVideoElement ? data.media : null;
- this.media = media;
- if (media && typeof media.getVideoPlaybackQuality === 'function') {
- this.isVideoPlaybackQualityAvailable = true;
- }
- self.clearInterval(this.timer);
- this.timer = self.setInterval(this.checkFPSInterval.bind(this), config.fpsDroppedMonitoringPeriod);
- }
- }
- onMediaDetaching() {
- this.media = null;
- }
- checkFPS(video, decodedFrames, droppedFrames) {
- const currentTime = performance.now();
- if (decodedFrames) {
- if (this.lastTime) {
- const currentPeriod = currentTime - this.lastTime;
- const currentDropped = droppedFrames - this.lastDroppedFrames;
- const currentDecoded = decodedFrames - this.lastDecodedFrames;
- const droppedFPS = 1000 * currentDropped / currentPeriod;
- const hls = this.hls;
- hls.trigger(Events.FPS_DROP, {
- currentDropped: currentDropped,
- currentDecoded: currentDecoded,
- totalDroppedFrames: droppedFrames
- });
- if (droppedFPS > 0) {
- // hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
- if (currentDropped > hls.config.fpsDroppedMonitoringThreshold * currentDecoded) {
- let currentLevel = hls.currentLevel;
- hls.logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel);
- if (currentLevel > 0 && (hls.autoLevelCapping === -1 || hls.autoLevelCapping >= currentLevel)) {
- currentLevel = currentLevel - 1;
- hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, {
- level: currentLevel,
- droppedLevel: hls.currentLevel
- });
- hls.autoLevelCapping = currentLevel;
- this.streamController.nextLevelSwitch();
- }
- }
- }
- }
- this.lastTime = currentTime;
- this.lastDroppedFrames = droppedFrames;
- this.lastDecodedFrames = decodedFrames;
- }
- }
- checkFPSInterval() {
- const video = this.media;
- if (video) {
- if (this.isVideoPlaybackQualityAvailable) {
- const videoPlaybackQuality = video.getVideoPlaybackQuality();
- this.checkFPS(video, videoPlaybackQuality.totalVideoFrames, videoPlaybackQuality.droppedVideoFrames);
- } else {
- // HTMLVideoElement doesn't include the webkit types
- this.checkFPS(video, video.webkitDecodedFrameCount, video.webkitDroppedFrameCount);
- }
- }
- }
-}
-
-function sendAddTrackEvent(track, videoEl) {
- let event;
- try {
- event = new Event('addtrack');
- } catch (err) {
- // for IE11
- event = document.createEvent('Event');
- event.initEvent('addtrack', false, false);
- }
- event.track = track;
- videoEl.dispatchEvent(event);
-}
-function addCueToTrack(track, cue) {
- // Sometimes there are cue overlaps on segmented vtts so the same
- // cue can appear more than once in different vtt files.
- // This avoid showing duplicated cues with same timecode and text.
- const mode = track.mode;
- if (mode === 'disabled') {
- track.mode = 'hidden';
- }
- if (track.cues && !track.cues.getCueById(cue.id)) {
- try {
- track.addCue(cue);
- if (!track.cues.getCueById(cue.id)) {
- throw new Error(`addCue is failed for: ${cue}`);
- }
- } catch (err) {
- logger.debug(`[texttrack-utils]: ${err}`);
- try {
- const textTrackCue = new self.TextTrackCue(cue.startTime, cue.endTime, cue.text);
- textTrackCue.id = cue.id;
- track.addCue(textTrackCue);
- } catch (err2) {
- logger.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${err2}`);
- }
- }
- }
- if (mode === 'disabled') {
- track.mode = mode;
- }
-}
-function clearCurrentCues(track, enterHandler) {
- // When track.mode is disabled, track.cues will be null.
- // To guarantee the removal of cues, we need to temporarily
- // change the mode to hidden
- const mode = track.mode;
- if (mode === 'disabled') {
- track.mode = 'hidden';
- }
- if (track.cues) {
- for (let i = track.cues.length; i--;) {
- if (enterHandler) {
- track.cues[i].removeEventListener('enter', enterHandler);
- }
- track.removeCue(track.cues[i]);
- }
- }
- if (mode === 'disabled') {
- track.mode = mode;
- }
-}
-function removeCuesInRange(track, start, end, predicate) {
- const mode = track.mode;
- if (mode === 'disabled') {
- track.mode = 'hidden';
- }
- if (track.cues && track.cues.length > 0) {
- const cues = getCuesInRange(track.cues, start, end);
- for (let i = 0; i < cues.length; i++) {
- if (!predicate || predicate(cues[i])) {
- track.removeCue(cues[i]);
- }
- }
- }
- if (mode === 'disabled') {
- track.mode = mode;
- }
-}
-
-// Find first cue starting at or after given time.
-// Modified version of binary search O(log(n)).
-function getFirstCueIndexFromTime(cues, time) {
- // If first cue starts at or after time, start there
- if (time <= cues[0].startTime) {
- return 0;
- }
- // If the last cue ends before time there is no overlap
- const len = cues.length - 1;
- if (time > cues[len].endTime) {
- return -1;
- }
- let left = 0;
- let right = len;
- let mid;
- while (left <= right) {
- mid = Math.floor((right + left) / 2);
- if (time < cues[mid].startTime) {
- right = mid - 1;
- } else if (time > cues[mid].startTime && left < len) {
- left = mid + 1;
- } else {
- // If it's not lower or higher, it must be equal.
- return mid;
- }
- }
- // At this point, left and right have swapped.
- // No direct match was found, left or right element must be the closest. Check which one has the smallest diff.
- return cues[left].startTime - time < time - cues[right].startTime ? left : right;
-}
-function getCuesInRange(cues, start, end) {
- const cuesFound = [];
- const firstCueInRange = getFirstCueIndexFromTime(cues, start);
- if (firstCueInRange > -1) {
- for (let i = firstCueInRange, len = cues.length; i < len; i++) {
- const cue = cues[i];
- if (cue.startTime >= start && cue.endTime <= end) {
- cuesFound.push(cue);
- } else if (cue.startTime > end) {
- return cuesFound;
- }
- }
- }
- return cuesFound;
-}
-function filterSubtitleTracks(textTrackList) {
- const tracks = [];
- for (let i = 0; i < textTrackList.length; i++) {
- const track = textTrackList[i];
- // Edge adds a track without a label; we don't want to use it
- if ((track.kind === 'subtitles' || track.kind === 'captions') && track.label) {
- tracks.push(textTrackList[i]);
- }
- }
- return tracks;
-}
-
-class SubtitleTrackController extends BasePlaylistController {
- constructor(hls) {
- super(hls, 'subtitle-track-controller');
- this.media = null;
- this.tracks = [];
- this.groupIds = null;
- this.tracksInGroup = [];
- this.trackId = -1;
- this.currentTrack = null;
- this.selectDefaultTrack = true;
- this.queuedDefaultTrack = -1;
- this.useTextTrackPolling = false;
- this.subtitlePollingInterval = -1;
- this._subtitleDisplay = true;
- this.asyncPollTrackChange = () => this.pollTrackChange(0);
- this.onTextTracksChanged = () => {
- if (!this.useTextTrackPolling) {
- self.clearInterval(this.subtitlePollingInterval);
- }
- // Media is undefined when switching streams via loadSource()
- if (!this.media || !this.hls.config.renderTextTracksNatively) {
- return;
- }
- let textTrack = null;
- const tracks = filterSubtitleTracks(this.media.textTracks);
- for (let i = 0; i < tracks.length; i++) {
- if (tracks[i].mode === 'hidden') {
- // Do not break in case there is a following track with showing.
- textTrack = tracks[i];
- } else if (tracks[i].mode === 'showing') {
- textTrack = tracks[i];
- break;
- }
- }
-
- // Find internal track index for TextTrack
- const trackId = this.findTrackForTextTrack(textTrack);
- if (this.subtitleTrack !== trackId) {
- this.setSubtitleTrack(trackId);
- }
- };
- this.registerListeners();
- }
- destroy() {
- this.unregisterListeners();
- this.tracks.length = 0;
- this.tracksInGroup.length = 0;
- this.currentTrack = null;
- // @ts-ignore
- this.onTextTracksChanged = this.asyncPollTrackChange = null;
- super.destroy();
- }
- get subtitleDisplay() {
- return this._subtitleDisplay;
- }
- set subtitleDisplay(value) {
- this._subtitleDisplay = value;
- if (this.trackId > -1) {
- this.toggleTrackModes();
- }
- }
- registerListeners() {
- const {
- hls
- } = this;
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
- hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
- hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
- hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
- hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
- hls.on(Events.ERROR, this.onError, this);
- }
- unregisterListeners() {
- const {
- hls
- } = this;
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
- hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
- hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
- hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
- hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
- hls.off(Events.ERROR, this.onError, this);
- }
-
- // Listen for subtitle track change, then extract the current track ID.
- onMediaAttached(event, data) {
- this.media = data.media;
- if (!this.media) {
- return;
- }
- if (this.queuedDefaultTrack > -1) {
- this.subtitleTrack = this.queuedDefaultTrack;
- this.queuedDefaultTrack = -1;
- }
- this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks);
- if (this.useTextTrackPolling) {
- this.pollTrackChange(500);
- } else {
- this.media.textTracks.addEventListener('change', this.asyncPollTrackChange);
- }
- }
- pollTrackChange(timeout) {
- self.clearInterval(this.subtitlePollingInterval);
- this.subtitlePollingInterval = self.setInterval(this.onTextTracksChanged, timeout);
- }
- onMediaDetaching(event, data) {
- const media = this.media;
- if (!media) {
- return;
- }
- const transferringMedia = !!data.transferMedia;
- self.clearInterval(this.subtitlePollingInterval);
- if (!this.useTextTrackPolling) {
- media.textTracks.removeEventListener('change', this.asyncPollTrackChange);
- }
- if (this.trackId > -1) {
- this.queuedDefaultTrack = this.trackId;
- }
-
- // Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled.
- this.subtitleTrack = -1;
- this.media = null;
- if (transferringMedia) {
- return;
- }
- const textTracks = filterSubtitleTracks(media.textTracks);
- // Clear loaded cues on media detachment from tracks
- textTracks.forEach(track => {
- clearCurrentCues(track);
- });
- }
- onManifestLoading() {
- this.tracks = [];
- this.groupIds = null;
- this.tracksInGroup = [];
- this.trackId = -1;
- this.currentTrack = null;
- this.selectDefaultTrack = true;
- }
-
- // Fired whenever a new manifest is loaded.
- onManifestParsed(event, data) {
- this.tracks = data.subtitleTracks;
- }
- onSubtitleTrackLoaded(event, data) {
- const {
- id,
- groupId,
- details
- } = data;
- const trackInActiveGroup = this.tracksInGroup[id];
- if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) {
- this.warn(`Subtitle track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`);
- return;
- }
- const curDetails = trackInActiveGroup.details;
- trackInActiveGroup.details = data.details;
- this.log(`Subtitle track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`);
- if (id === this.trackId) {
- this.playlistLoaded(id, data, curDetails);
- }
- }
- onLevelLoading(event, data) {
- this.switchLevel(data.level);
- }
- onLevelSwitching(event, data) {
- this.switchLevel(data.level);
- }
- switchLevel(levelIndex) {
- const levelInfo = this.hls.levels[levelIndex];
- if (!levelInfo) {
- return;
- }
- const subtitleGroups = levelInfo.subtitleGroups || null;
- const currentGroups = this.groupIds;
- let currentTrack = this.currentTrack;
- if (!subtitleGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (subtitleGroups == null ? void 0 : subtitleGroups.length) || subtitleGroups != null && subtitleGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) {
- this.groupIds = subtitleGroups;
- this.trackId = -1;
- this.currentTrack = null;
- const subtitleTracks = this.tracks.filter(track => !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1);
- if (subtitleTracks.length) {
- // Disable selectDefaultTrack if there are no default tracks
- if (this.selectDefaultTrack && !subtitleTracks.some(track => track.default)) {
- this.selectDefaultTrack = false;
- }
- // track.id should match hls.audioTracks index
- subtitleTracks.forEach((track, i) => {
- track.id = i;
- });
- } else if (!currentTrack && !this.tracksInGroup.length) {
- // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks
- return;
- }
- this.tracksInGroup = subtitleTracks;
-
- // Find preferred track
- const subtitlePreference = this.hls.config.subtitlePreference;
- if (!currentTrack && subtitlePreference) {
- this.selectDefaultTrack = false;
- const groupIndex = findMatchingOption(subtitlePreference, subtitleTracks);
- if (groupIndex > -1) {
- currentTrack = subtitleTracks[groupIndex];
- } else {
- const allIndex = findMatchingOption(subtitlePreference, this.tracks);
- currentTrack = this.tracks[allIndex];
- }
- }
-
- // Select initial track
- let trackId = this.findTrackId(currentTrack);
- if (trackId === -1 && currentTrack) {
- trackId = this.findTrackId(null);
- }
-
- // Dispatch events and load track if needed
- const subtitleTracksUpdated = {
- subtitleTracks
- };
- this.log(`Updating subtitle tracks, ${subtitleTracks.length} track(s) found in "${subtitleGroups == null ? void 0 : subtitleGroups.join(',')}" group-id`);
- this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated);
- if (trackId !== -1 && this.trackId === -1) {
- this.setSubtitleTrack(trackId);
- }
- }
- }
- findTrackId(currentTrack) {
- const tracks = this.tracksInGroup;
- const selectDefault = this.selectDefaultTrack;
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- if (selectDefault && !track.default || !selectDefault && !currentTrack) {
- continue;
- }
- if (!currentTrack || matchesOption(track, currentTrack)) {
- return i;
- }
- }
- if (currentTrack) {
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) {
- return i;
- }
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) {
- return i;
- }
- }
- }
- return -1;
- }
- findTrackForTextTrack(textTrack) {
- if (textTrack) {
- const tracks = this.tracksInGroup;
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- if (subtitleTrackMatchesTextTrack(track, textTrack)) {
- return i;
- }
- }
- }
- return -1;
- }
- onError(event, data) {
- if (data.fatal || !data.context) {
- return;
- }
- if (data.context.type === PlaylistContextType.SUBTITLE_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) {
- this.checkRetry(data);
- }
- }
- get allSubtitleTracks() {
- return this.tracks;
- }
-
- /** get alternate subtitle tracks list from playlist **/
- get subtitleTracks() {
- return this.tracksInGroup;
- }
-
- /** get/set index of the selected subtitle track (based on index in subtitle track lists) **/
- get subtitleTrack() {
- return this.trackId;
- }
- set subtitleTrack(newId) {
- this.selectDefaultTrack = false;
- this.setSubtitleTrack(newId);
- }
- setSubtitleOption(subtitleOption) {
- this.hls.config.subtitlePreference = subtitleOption;
- if (subtitleOption) {
- if (subtitleOption.id === -1) {
- this.setSubtitleTrack(-1);
- return null;
- }
- const allSubtitleTracks = this.allSubtitleTracks;
- this.selectDefaultTrack = false;
- if (allSubtitleTracks.length) {
- // First see if current option matches (no switch op)
- const currentTrack = this.currentTrack;
- if (currentTrack && matchesOption(subtitleOption, currentTrack)) {
- return currentTrack;
- }
- // Find option in current group
- const groupIndex = findMatchingOption(subtitleOption, this.tracksInGroup);
- if (groupIndex > -1) {
- const track = this.tracksInGroup[groupIndex];
- this.setSubtitleTrack(groupIndex);
- return track;
- } else if (currentTrack) {
- // If this is not the initial selection return null
- // option should have matched one in active group
- return null;
- } else {
- // Find the option in all tracks for initial selection
- const allIndex = findMatchingOption(subtitleOption, allSubtitleTracks);
- if (allIndex > -1) {
- return allSubtitleTracks[allIndex];
- }
- }
- }
- }
- return null;
- }
- loadPlaylist(hlsUrlParameters) {
- super.loadPlaylist();
- if (this.shouldLoadPlaylist(this.currentTrack)) {
- this.scheduleLoading(this.currentTrack, hlsUrlParameters);
- }
- }
- loadingPlaylist(currentTrack, hlsUrlParameters) {
- super.loadingPlaylist(currentTrack, hlsUrlParameters);
- const id = currentTrack.id;
- const groupId = currentTrack.groupId;
- const url = this.getUrlWithDirectives(currentTrack.url, hlsUrlParameters);
- const details = currentTrack.details;
- const age = details == null ? void 0 : details.age;
- this.log(`Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`);
- this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, {
- url,
- id,
- groupId,
- deliveryDirectives: hlsUrlParameters || null,
- track: currentTrack
- });
- }
-
- /**
- * Disables the old subtitleTrack and sets current mode on the next subtitleTrack.
- * This operates on the DOM textTracks.
- * A value of -1 will disable all subtitle tracks.
- */
- toggleTrackModes() {
- const {
- media
- } = this;
- if (!media) {
- return;
- }
- const textTracks = filterSubtitleTracks(media.textTracks);
- const currentTrack = this.currentTrack;
- let nextTrack;
- if (currentTrack) {
- nextTrack = textTracks.filter(textTrack => subtitleTrackMatchesTextTrack(currentTrack, textTrack))[0];
- if (!nextTrack) {
- this.warn(`Unable to find subtitle TextTrack with name "${currentTrack.name}" and language "${currentTrack.lang}"`);
- }
- }
- [].slice.call(textTracks).forEach(track => {
- if (track.mode !== 'disabled' && track !== nextTrack) {
- track.mode = 'disabled';
- }
- });
- if (nextTrack) {
- const mode = this.subtitleDisplay ? 'showing' : 'hidden';
- if (nextTrack.mode !== mode) {
- nextTrack.mode = mode;
- }
- }
- }
-
- /**
- * This method is responsible for validating the subtitle index and periodically reloading if live.
- * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track.
- */
- setSubtitleTrack(newId) {
- const tracks = this.tracksInGroup;
-
- // setting this.subtitleTrack will trigger internal logic
- // if media has not been attached yet, it will fail
- // we keep a reference to the default track id
- // and we'll set subtitleTrack when onMediaAttached is triggered
- if (!this.media) {
- this.queuedDefaultTrack = newId;
- return;
- }
-
- // exit if track id as already set or invalid
- if (newId < -1 || newId >= tracks.length || !isFiniteNumber(newId)) {
- this.warn(`Invalid subtitle track id: ${newId}`);
- return;
- }
- this.selectDefaultTrack = false;
- const lastTrack = this.currentTrack;
- const track = tracks[newId] || null;
- this.trackId = newId;
- this.currentTrack = track;
- this.toggleTrackModes();
- if (!track) {
- // switch to -1
- this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
- id: newId
- });
- return;
- }
- const trackLoaded = !!track.details && !track.details.live;
- if (newId === this.trackId && track === lastTrack && trackLoaded) {
- return;
- }
- this.log(`Switching to subtitle-track ${newId}` + (track ? ` "${track.name}" lang:${track.lang} group:${track.groupId}` : ''));
- const {
- id,
- groupId = '',
- name,
- type,
- url
- } = track;
- this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
- id,
- groupId,
- name,
- type,
- url
- });
- const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details);
- this.loadPlaylist(hlsUrlParameters);
- }
-}
-
-/**
- * Generate a random v4 UUID
- *
- * @returns A random v4 UUID
- *
- * @group Utils
- *
- * @beta
- */
-function uuid() {
- try {
- return crypto.randomUUID();
- } catch (error) {
- try {
- const url = URL.createObjectURL(new Blob());
- const uuid = url.toString();
- URL.revokeObjectURL(url);
- return uuid.slice(uuid.lastIndexOf('/') + 1);
- } catch (error) {
- let dt = new Date().getTime();
- const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
- const r = (dt + Math.random() * 16) % 16 | 0;
- dt = Math.floor(dt / 16);
- return (c == 'x' ? r : r & 0x3 | 0x8).toString(16);
- });
- return uuid;
- }
- }
-}
-
-// From https://github.com/darkskyapp/string-hash
-function hash(text) {
- let hash = 5381;
- let i = text.length;
- while (i) {
- hash = hash * 33 ^ text.charCodeAt(--i);
- }
- return (hash >>> 0).toString();
-}
-
-const ALIGNED_END_THRESHOLD_SECONDS = 0.025;
-let TimelineOccupancy = /*#__PURE__*/function (TimelineOccupancy) {
- TimelineOccupancy[TimelineOccupancy["Point"] = 0] = "Point";
- TimelineOccupancy[TimelineOccupancy["Range"] = 1] = "Range";
- return TimelineOccupancy;
-}({});
-function generateAssetIdentifier(interstitial, uri, assetListIndex) {
- return `${interstitial.identifier}-${assetListIndex + 1}-${hash(uri)}`;
-}
-class InterstitialEvent {
- constructor(dateRange, base) {
- this.base = void 0;
- this._duration = null;
- this._timelineStart = null;
- this.appendInPlaceDisabled = void 0;
- this.appendInPlaceStarted = void 0;
- this.dateRange = void 0;
- this.hasPlayed = false;
- this.cumulativeDuration = 0;
- this.resumeOffset = NaN;
- this.playoutLimit = NaN;
- this.restrictions = {
- skip: false,
- jump: false
- };
- this.snapOptions = {
- out: false,
- in: false
- };
- this.assetList = [];
- this.assetListLoader = void 0;
- this.assetListResponse = null;
- this.resumeAnchor = void 0;
- this.error = void 0;
- this.resetOnResume = void 0;
- this.base = base;
- this.dateRange = dateRange;
- this.setDateRange(dateRange);
- }
- setDateRange(dateRange) {
- this.dateRange = dateRange;
- this.resumeOffset = dateRange.attr.optionalFloat('X-RESUME-OFFSET', this.resumeOffset);
- this.playoutLimit = dateRange.attr.optionalFloat('X-PLAYOUT-LIMIT', this.playoutLimit);
- this.restrictions = dateRange.attr.enumeratedStringList('X-RESTRICT', this.restrictions);
- this.snapOptions = dateRange.attr.enumeratedStringList('X-SNAP', this.snapOptions);
- }
- reset() {
- var _this$assetListLoader;
- this.appendInPlaceStarted = false;
- (_this$assetListLoader = this.assetListLoader) == null || _this$assetListLoader.destroy();
- this.assetListLoader = undefined;
- if (!this.supplementsPrimary) {
- this.assetListResponse = null;
- this.assetList = [];
- this._duration = null;
- }
- // `error?` is reset when seeking back over interstitial `startOffset`
- // using `schedule.resetErrorsInRange(start, end)`.
- }
- isAssetPastPlayoutLimit(assetIndex) {
- var _this$assetList$asset;
- if (assetIndex > 0 && assetIndex >= this.assetList.length) {
- return true;
- }
- const playoutLimit = this.playoutLimit;
- if (assetIndex <= 0 || isNaN(playoutLimit)) {
- return false;
- }
- if (playoutLimit === 0) {
- return true;
- }
- const assetOffset = ((_this$assetList$asset = this.assetList[assetIndex]) == null ? void 0 : _this$assetList$asset.startOffset) || 0;
- return assetOffset > playoutLimit;
- }
- findAssetIndex(asset) {
- const index = this.assetList.indexOf(asset);
- return index;
- }
- get identifier() {
- return this.dateRange.id;
- }
- get startDate() {
- return this.dateRange.startDate;
- }
- get startTime() {
- // Primary media timeline start time
- const startTime = this.dateRange.startTime;
- if (this.snapOptions.out) {
- const frag = this.dateRange.tagAnchor;
- if (frag) {
- return getSnapToFragmentTime(startTime, frag);
- }
- }
- return startTime;
- }
- get startOffset() {
- return this.cue.pre ? 0 : this.startTime;
- }
- get startIsAligned() {
- if (this.startTime === 0 || this.snapOptions.out) {
- return true;
- }
- const frag = this.dateRange.tagAnchor;
- if (frag) {
- const startTime = this.dateRange.startTime;
- const snappedStart = getSnapToFragmentTime(startTime, frag);
- return startTime - snappedStart < 0.1;
- }
- return false;
- }
- get resumptionOffset() {
- const resumeOffset = this.resumeOffset;
- const offset = isFiniteNumber(resumeOffset) ? resumeOffset : this.duration;
- return this.cumulativeDuration + offset;
- }
- get resumeTime() {
- // Primary media timeline resumption time
- const resumeTime = this.startOffset + this.resumptionOffset;
- if (this.snapOptions.in) {
- const frag = this.resumeAnchor;
- if (frag) {
- return getSnapToFragmentTime(resumeTime, frag);
- }
- }
- return resumeTime;
- }
- get appendInPlace() {
- if (this.appendInPlaceStarted) {
- return true;
- }
- if (this.appendInPlaceDisabled) {
- return false;
- }
- if (!this.cue.once && !this.cue.pre &&
- // preroll starts at startPosition before startPosition is known (live)
- this.startIsAligned && (isNaN(this.playoutLimit) && isNaN(this.resumeOffset) || this.resumeOffset && this.duration && Math.abs(this.resumeOffset - this.duration) < ALIGNED_END_THRESHOLD_SECONDS)) {
- return true;
- }
- return false;
- }
- set appendInPlace(value) {
- if (this.appendInPlaceStarted) {
- this.resetOnResume = !value;
- return;
- }
- this.appendInPlaceDisabled = !value;
- }
-
- // Extended timeline start time
- get timelineStart() {
- if (this._timelineStart !== null) {
- return this._timelineStart;
- }
- return this.startTime;
- }
- set timelineStart(value) {
- this._timelineStart = value;
- }
- get duration() {
- const playoutLimit = this.playoutLimit;
- let duration;
- if (this._duration !== null) {
- duration = this._duration;
- } else if (this.dateRange.duration) {
- duration = this.dateRange.duration;
- } else {
- duration = this.dateRange.plannedDuration || 0;
- }
- if (!isNaN(playoutLimit) && playoutLimit < duration) {
- duration = playoutLimit;
- }
- return duration;
- }
- set duration(value) {
- this._duration = value;
- }
- get cue() {
- return this.dateRange.cue;
- }
- get timelineOccupancy() {
- if (this.dateRange.attr['X-TIMELINE-OCCUPIES'] === 'RANGE') {
- return TimelineOccupancy.Range;
- }
- return TimelineOccupancy.Point;
- }
- get supplementsPrimary() {
- return this.dateRange.attr['X-TIMELINE-STYLE'] === 'PRIMARY';
- }
- get contentMayVary() {
- return this.dateRange.attr['X-CONTENT-MAY-VARY'] !== 'NO';
- }
- get assetUrl() {
- return this.dateRange.attr['X-ASSET-URI'];
- }
- get assetListUrl() {
- return this.dateRange.attr['X-ASSET-LIST'];
- }
- get baseUrl() {
- return this.base.url;
- }
- get assetListLoaded() {
- return this.assetList.length > 0 || this.assetListResponse !== null;
- }
- toString() {
- return eventToString(this);
- }
-}
-function getSnapToFragmentTime(time, frag) {
- return time - frag.start < frag.duration / 2 && !(Math.abs(time - (frag.start + frag.duration)) < ALIGNED_END_THRESHOLD_SECONDS) ? frag.start : frag.start + frag.duration;
-}
-function getInterstitialUrl(uri, sessionId, baseUrl) {
- const url = new self.URL(uri, baseUrl);
- if (url.protocol !== 'data:') {
- url.searchParams.set('_HLS_primary_id', sessionId);
- }
- return url;
-}
-function getNextAssetIndex(interstitial, assetListIndex) {
- while ((_interstitial$assetLi = interstitial.assetList[++assetListIndex]) != null && _interstitial$assetLi.error) {
- var _interstitial$assetLi;
- } /* no-op */
- return assetListIndex;
-}
-function eventToString(interstitial) {
- return `["${interstitial.identifier}" ${interstitial.cue.pre ? '' : interstitial.cue.post ? '' : ''}${interstitial.timelineStart.toFixed(2)}-${interstitial.resumeTime.toFixed(2)}]`;
-}
-function eventAssetToString(asset) {
- const start = asset.timelineStart;
- const duration = asset.duration || 0;
- return `["${asset.identifier}" ${start.toFixed(2)}-${(start + duration).toFixed(2)}]`;
-}
-
-class HlsAssetPlayer {
- constructor(HlsPlayerClass, userConfig, interstitial, assetItem) {
- this.hls = void 0;
- this.interstitial = void 0;
- this.assetItem = void 0;
- this.tracks = null;
- this.hasDetails = false;
- this.mediaAttached = null;
- this._currentTime = void 0;
- this._bufferedEosTime = void 0;
- this.checkPlayout = () => {
- if (this.reachedPlayout(this.currentTime) && this.hls) {
- this.hls.trigger(Events.PLAYOUT_LIMIT_REACHED, {});
- }
- };
- const hls = this.hls = new HlsPlayerClass(userConfig);
- this.interstitial = interstitial;
- this.assetItem = assetItem;
- const detailsLoaded = () => {
- this.hasDetails = true;
- };
- hls.once(Events.LEVEL_LOADED, detailsLoaded);
- hls.once(Events.AUDIO_TRACK_LOADED, detailsLoaded);
- hls.once(Events.SUBTITLE_TRACK_LOADED, detailsLoaded);
- hls.on(Events.MEDIA_ATTACHING, (name, {
- media
- }) => {
- this.removeMediaListeners();
- this.mediaAttached = media;
- const event = this.interstitial;
- if (event.playoutLimit) {
- media.addEventListener('timeupdate', this.checkPlayout);
- if (this.appendInPlace) {
- hls.on(Events.BUFFER_APPENDED, () => {
- const bufferedEnd = this.bufferedEnd;
- if (this.reachedPlayout(bufferedEnd)) {
- this._bufferedEosTime = bufferedEnd;
- hls.trigger(Events.BUFFERED_TO_END, undefined);
- }
- });
- }
- }
- });
- }
- get appendInPlace() {
- return this.interstitial.appendInPlace;
- }
- loadSource() {
- const hls = this.hls;
- if (!hls) {
- return;
- }
- if (!hls.url) {
- let uri = this.assetItem.uri;
- try {
- uri = getInterstitialUrl(uri, hls.config.primarySessionId || '').href;
- } catch (error) {
- // Ignore error parsing ASSET_URI or adding _HLS_primary_id to it. The
- // issue should surface as an INTERSTITIAL_ASSET_ERROR loading the asset.
- }
- hls.loadSource(uri);
- } else if (hls.levels.length && !hls.started) {
- hls.startLoad(-1, true);
- }
- }
- bufferedInPlaceToEnd(media) {
- var _this$hls;
- if (!this.appendInPlace) {
- return false;
- }
- if ((_this$hls = this.hls) != null && _this$hls.bufferedToEnd) {
- return true;
- }
- if (!media) {
- return false;
- }
- const duration = Math.min(this._bufferedEosTime || Infinity, this.duration);
- const start = this.timelineOffset;
- const bufferInfo = BufferHelper.bufferInfo(media, start, 0);
- const bufferedEnd = this.getAssetTime(bufferInfo.end);
- return bufferedEnd >= duration - 0.02;
- }
- reachedPlayout(time) {
- const interstitial = this.interstitial;
- const playoutLimit = interstitial.playoutLimit;
- return this.startOffset + time >= playoutLimit;
- }
- get destroyed() {
- var _this$hls2;
- return !((_this$hls2 = this.hls) != null && _this$hls2.userConfig);
- }
- get assetId() {
- return this.assetItem.identifier;
- }
- get interstitialId() {
- return this.assetItem.parentIdentifier;
- }
- get media() {
- var _this$hls3;
- return ((_this$hls3 = this.hls) == null ? void 0 : _this$hls3.media) || null;
- }
- get bufferedEnd() {
- const media = this.media || this.mediaAttached;
- if (!media) {
- if (this._bufferedEosTime) {
- return this._bufferedEosTime;
- }
- return this.currentTime;
- }
- const bufferInfo = BufferHelper.bufferInfo(media, media.currentTime, 0.001);
- return this.getAssetTime(bufferInfo.end);
- }
- get currentTime() {
- const media = this.media || this.mediaAttached;
- if (!media) {
- return this._currentTime || 0;
- }
- return this.getAssetTime(media.currentTime);
- }
- get duration() {
- const duration = this.assetItem.duration;
- if (!duration) {
- return 0;
- }
- const playoutLimit = this.interstitial.playoutLimit;
- if (playoutLimit) {
- const assetPlayout = playoutLimit - this.startOffset;
- if (assetPlayout > 0 && assetPlayout < duration) {
- return assetPlayout;
- }
- }
- return duration;
- }
- get remaining() {
- const duration = this.duration;
- if (!duration) {
- return 0;
- }
- return Math.max(0, duration - this.currentTime);
- }
- get startOffset() {
- return this.assetItem.startOffset;
- }
- get timelineOffset() {
- var _this$hls4;
- return ((_this$hls4 = this.hls) == null ? void 0 : _this$hls4.config.timelineOffset) || 0;
- }
- set timelineOffset(value) {
- const timelineOffset = this.timelineOffset;
- if (value !== timelineOffset) {
- const diff = value - timelineOffset;
- if (Math.abs(diff) > 1 / 90000 && this.hls) {
- if (this.hasDetails) {
- throw new Error(`Cannot set timelineOffset after playlists are loaded`);
- }
- this.hls.config.timelineOffset = value;
- }
- }
- }
- getAssetTime(time) {
- const timelineOffset = this.timelineOffset;
- const duration = this.duration;
- return Math.min(Math.max(0, time - timelineOffset), duration);
- }
- removeMediaListeners() {
- const media = this.mediaAttached;
- if (media) {
- this._currentTime = media.currentTime;
- this.bufferSnapShot();
- media.removeEventListener('timeupdate', this.checkPlayout);
- }
- }
- bufferSnapShot() {
- if (this.mediaAttached) {
- var _this$hls5;
- if ((_this$hls5 = this.hls) != null && _this$hls5.bufferedToEnd) {
- this._bufferedEosTime = this.bufferedEnd;
- }
- }
- }
- destroy() {
- this.removeMediaListeners();
- if (this.hls) {
- this.hls.destroy();
- }
- this.hls = null;
- // @ts-ignore
- this.tracks = this.mediaAttached = this.checkPlayout = null;
- }
- attachMedia(data) {
- var _this$hls6;
- this.loadSource();
- (_this$hls6 = this.hls) == null || _this$hls6.attachMedia(data);
- }
- detachMedia() {
- var _this$hls7;
- this.removeMediaListeners();
- this.mediaAttached = null;
- (_this$hls7 = this.hls) == null || _this$hls7.detachMedia();
- }
- resumeBuffering() {
- var _this$hls8;
- (_this$hls8 = this.hls) == null || _this$hls8.resumeBuffering();
- }
- pauseBuffering() {
- var _this$hls9;
- (_this$hls9 = this.hls) == null || _this$hls9.pauseBuffering();
- }
- transferMedia() {
- var _this$hls0;
- this.bufferSnapShot();
- return ((_this$hls0 = this.hls) == null ? void 0 : _this$hls0.transferMedia()) || null;
- }
- resetDetails() {
- const hls = this.hls;
- if (hls && this.hasDetails) {
- hls.stopLoad();
- const deleteDetails = obj => delete obj.details;
- hls.levels.forEach(deleteDetails);
- hls.allAudioTracks.forEach(deleteDetails);
- hls.allSubtitleTracks.forEach(deleteDetails);
- this.hasDetails = false;
- }
- }
- on(event, listener, context) {
- var _this$hls1;
- (_this$hls1 = this.hls) == null || _this$hls1.on(event, listener);
- }
- once(event, listener, context) {
- var _this$hls10;
- (_this$hls10 = this.hls) == null || _this$hls10.once(event, listener);
- }
- off(event, listener, context) {
- var _this$hls11;
- (_this$hls11 = this.hls) == null || _this$hls11.off(event, listener);
- }
- toString() {
- var _this$hls12;
- return `HlsAssetPlayer: ${eventAssetToString(this.assetItem)} ${(_this$hls12 = this.hls) == null ? void 0 : _this$hls12.sessionId} ${this.appendInPlace ? 'append-in-place' : ''}`;
- }
-}
-
-const ABUTTING_THRESHOLD_SECONDS = 0.033;
-class InterstitialsSchedule extends Logger {
- constructor(onScheduleUpdate, logger) {
- super('interstitials-sched', logger);
- this.onScheduleUpdate = void 0;
- this.eventMap = {};
- this.events = null;
- this.items = null;
- this.durations = {
- primary: 0,
- playout: 0,
- integrated: 0
- };
- this.onScheduleUpdate = onScheduleUpdate;
- }
- destroy() {
- this.reset();
- // @ts-ignore
- this.onScheduleUpdate = null;
- }
- reset() {
- this.eventMap = {};
- this.setDurations(0, 0, 0);
- if (this.events) {
- this.events.forEach(interstitial => interstitial.reset());
- }
- this.events = this.items = null;
- }
- resetErrorsInRange(start, end) {
- if (this.events) {
- return this.events.reduce((count, interstitial) => {
- if (start <= interstitial.startOffset && end > interstitial.startOffset) {
- delete interstitial.error;
- return count + 1;
- }
- return count;
- }, 0);
- }
- return 0;
- }
- get duration() {
- const items = this.items;
- return items ? items[items.length - 1].end : 0;
- }
- get length() {
- return this.items ? this.items.length : 0;
- }
- getEvent(identifier) {
- return identifier ? this.eventMap[identifier] || null : null;
- }
- hasEvent(identifier) {
- return identifier in this.eventMap;
- }
- findItemIndex(item, time) {
- if (item.event) {
- // Find Event Item
- return this.findEventIndex(item.event.identifier);
- }
- // Find Primary Item
- let index = -1;
- if (item.nextEvent) {
- index = this.findEventIndex(item.nextEvent.identifier) - 1;
- } else if (item.previousEvent) {
- index = this.findEventIndex(item.previousEvent.identifier) + 1;
- }
- const items = this.items;
- if (items) {
- if (!items[index]) {
- if (time === undefined) {
- time = item.start;
- }
- index = this.findItemIndexAtTime(time);
- }
- // Only return index of a Primary Item
- while (index >= 0 && (_items$index = items[index]) != null && _items$index.event) {
- var _items$index;
- // If index found is an interstitial it is not a valid result as it should have been matched up top
- // decrement until result is negative (not found) or a primary segment
- index--;
- }
- }
- return index;
- }
- findItemIndexAtTime(timelinePos, timelineType) {
- const items = this.items;
- if (items) {
- for (let i = 0; i < items.length; i++) {
- let timeRange = items[i];
- if (timelineType && timelineType !== 'primary') {
- timeRange = timeRange[timelineType];
- }
- if (timelinePos === timeRange.start || timelinePos > timeRange.start && timelinePos < timeRange.end) {
- return i;
- }
- }
- }
- return -1;
- }
- findJumpRestrictedIndex(startIndex, endIndex) {
- const items = this.items;
- if (items) {
- for (let i = startIndex; i <= endIndex; i++) {
- if (!items[i]) {
- break;
- }
- const event = items[i].event;
- if (event != null && event.restrictions.jump && !event.appendInPlace) {
- return i;
- }
- }
- }
- return -1;
- }
- findEventIndex(identifier) {
- const items = this.items;
- if (items) {
- for (let i = items.length; i--;) {
- var _items$i$event;
- if (((_items$i$event = items[i].event) == null ? void 0 : _items$i$event.identifier) === identifier) {
- return i;
- }
- }
- }
- return -1;
- }
- findAssetIndex(event, timelinePos) {
- const assetList = event.assetList;
- const length = assetList.length;
- if (length > 1) {
- for (let i = 0; i < length; i++) {
- const asset = assetList[i];
- if (!asset.error) {
- const timelineStart = asset.timelineStart;
- if (timelinePos === timelineStart || timelinePos > timelineStart && (timelinePos < timelineStart + (asset.duration || 0) || i === length - 1)) {
- return i;
- }
- }
- }
- }
- return 0;
- }
- get assetIdAtEnd() {
- var _this$items;
- const interstitialAtEnd = (_this$items = this.items) == null || (_this$items = _this$items[this.length - 1]) == null ? void 0 : _this$items.event;
- if (interstitialAtEnd) {
- const assetList = interstitialAtEnd.assetList;
- const assetAtEnd = assetList[assetList.length - 1];
- if (assetAtEnd) {
- return assetAtEnd.identifier;
- }
- }
- return null;
- }
- parseInterstitialDateRanges(mediaSelection, enableAppendInPlace) {
- const details = mediaSelection.main.details;
- const {
- dateRanges
- } = details;
- const previousInterstitialEvents = this.events;
- const interstitialEvents = this.parseDateRanges(dateRanges, {
- url: details.url
- }, enableAppendInPlace);
- const ids = Object.keys(dateRanges);
- const removedInterstitials = previousInterstitialEvents ? previousInterstitialEvents.filter(event => !ids.includes(event.identifier)) : [];
- if (interstitialEvents.length) {
- // pre-rolls, post-rolls, and events with the same start time are played in playlist tag order
- // all other events are ordered by start time
- interstitialEvents.sort((a, b) => {
- const aPre = a.cue.pre;
- const aPost = a.cue.post;
- const bPre = b.cue.pre;
- const bPost = b.cue.post;
- if (aPre && !bPre) {
- return -1;
- }
- if (bPre && !aPre) {
- return 1;
- }
- if (aPost && !bPost) {
- return 1;
- }
- if (bPost && !aPost) {
- return -1;
- }
- if (!aPre && !bPre && !aPost && !bPost) {
- const startA = a.startTime;
- const startB = b.startTime;
- if (startA !== startB) {
- return startA - startB;
- }
- }
- return a.dateRange.tagOrder - b.dateRange.tagOrder;
- });
- }
- this.events = interstitialEvents;
-
- // Clear removed DateRanges from buffered list (kills playback of active Interstitials)
- removedInterstitials.forEach(interstitial => {
- this.removeEvent(interstitial);
- });
- this.updateSchedule(mediaSelection, removedInterstitials);
- }
- updateSchedule(mediaSelection, removedInterstitials = [], forceUpdate = false) {
- const events = this.events || [];
- if (events.length || removedInterstitials.length || this.length < 2) {
- const currentItems = this.items;
- const updatedItems = this.parseSchedule(events, mediaSelection);
- const updated = forceUpdate || removedInterstitials.length || (currentItems == null ? void 0 : currentItems.length) !== updatedItems.length || updatedItems.some((item, i) => {
- return Math.abs(item.playout.start - currentItems[i].playout.start) > 0.005 || Math.abs(item.playout.end - currentItems[i].playout.end) > 0.005;
- });
- if (updated) {
- this.items = updatedItems;
- // call interstitials-controller onScheduleUpdated()
- this.onScheduleUpdate(removedInterstitials, currentItems);
- }
- }
- }
- parseDateRanges(dateRanges, baseData, enableAppendInPlace) {
- const interstitialEvents = [];
- const ids = Object.keys(dateRanges);
- for (let i = 0; i < ids.length; i++) {
- const id = ids[i];
- const dateRange = dateRanges[id];
- if (dateRange.isInterstitial) {
- let interstitial = this.eventMap[id];
- if (interstitial) {
- // Update InterstitialEvent already parsed and mapped
- // This retains already loaded duration and loaded asset list info
- interstitial.setDateRange(dateRange);
- } else {
- interstitial = new InterstitialEvent(dateRange, baseData);
- this.eventMap[id] = interstitial;
- if (enableAppendInPlace === false) {
- interstitial.appendInPlace = enableAppendInPlace;
- }
- }
- interstitialEvents.push(interstitial);
- }
- }
- return interstitialEvents;
- }
- parseSchedule(interstitialEvents, mediaSelection) {
- const schedule = [];
- const details = mediaSelection.main.details;
- const primaryDuration = details.live ? Infinity : details.edge;
- let playoutDuration = 0;
-
- // Filter events that have errored from the schedule (Primary fallback)
- interstitialEvents = interstitialEvents.filter(event => !event.error && !(event.cue.once && event.hasPlayed));
- if (interstitialEvents.length) {
- // Update Schedule
- this.resolveOffsets(interstitialEvents, mediaSelection);
-
- // Populate Schedule with Interstitial Event and Primary Segment Items
- let primaryPosition = 0;
- let integratedTime = 0;
- interstitialEvents.forEach((interstitial, i) => {
- const preroll = interstitial.cue.pre;
- const postroll = interstitial.cue.post;
- const previousEvent = interstitialEvents[i - 1] || null;
- const appendInPlace = interstitial.appendInPlace;
- const eventStart = postroll ? primaryDuration : interstitial.startOffset;
- const interstitialDuration = interstitial.duration;
- const timelineDuration = interstitial.timelineOccupancy === TimelineOccupancy.Range ? interstitialDuration : 0;
- const resumptionOffset = interstitial.resumptionOffset;
- const inSameStartTimeSequence = (previousEvent == null ? void 0 : previousEvent.startTime) === eventStart;
- const start = eventStart + interstitial.cumulativeDuration;
- let end = appendInPlace ? start + interstitialDuration : eventStart + resumptionOffset;
- if (preroll || !postroll && eventStart <= 0) {
- // preroll or in-progress midroll
- const integratedStart = integratedTime;
- integratedTime += timelineDuration;
- interstitial.timelineStart = start;
- const playoutStart = playoutDuration;
- playoutDuration += interstitialDuration;
- schedule.push({
- event: interstitial,
- start,
- end,
- playout: {
- start: playoutStart,
- end: playoutDuration
- },
- integrated: {
- start: integratedStart,
- end: integratedTime
- }
- });
- } else if (eventStart <= primaryDuration) {
- if (!inSameStartTimeSequence) {
- const segmentDuration = eventStart - primaryPosition;
- // Do not schedule a primary segment if interstitials are abutting by less than ABUTTING_THRESHOLD_SECONDS
- if (segmentDuration > ABUTTING_THRESHOLD_SECONDS) {
- // primary segment
- const timelineStart = primaryPosition;
- const _integratedStart = integratedTime;
- integratedTime += segmentDuration;
- const _playoutStart = playoutDuration;
- playoutDuration += segmentDuration;
- const primarySegment = {
- previousEvent: interstitialEvents[i - 1] || null,
- nextEvent: interstitial,
- start: timelineStart,
- end: timelineStart + segmentDuration,
- playout: {
- start: _playoutStart,
- end: playoutDuration
- },
- integrated: {
- start: _integratedStart,
- end: integratedTime
- }
- };
- schedule.push(primarySegment);
- } else if (segmentDuration > 0 && previousEvent) {
- // Add previous event `resumeTime` (based on duration or resumeOffset) so that it ends aligned with this one
- previousEvent.cumulativeDuration += segmentDuration;
- schedule[schedule.length - 1].end = eventStart;
- }
- }
- // midroll / postroll
- if (postroll) {
- end = start;
- }
- interstitial.timelineStart = start;
- const integratedStart = integratedTime;
- integratedTime += timelineDuration;
- const playoutStart = playoutDuration;
- playoutDuration += interstitialDuration;
- schedule.push({
- event: interstitial,
- start,
- end,
- playout: {
- start: playoutStart,
- end: playoutDuration
- },
- integrated: {
- start: integratedStart,
- end: integratedTime
- }
- });
- } else {
- // Interstitial starts after end of primary VOD - not included in schedule
- return;
- }
- const resumeTime = interstitial.resumeTime;
- if (postroll || resumeTime > primaryDuration) {
- primaryPosition = primaryDuration;
- } else {
- primaryPosition = resumeTime;
- }
- });
- if (primaryPosition < primaryDuration) {
- var _schedule;
- // last primary segment
- const timelineStart = primaryPosition;
- const integratedStart = integratedTime;
- const segmentDuration = primaryDuration - primaryPosition;
- integratedTime += segmentDuration;
- const playoutStart = playoutDuration;
- playoutDuration += segmentDuration;
- schedule.push({
- previousEvent: ((_schedule = schedule[schedule.length - 1]) == null ? void 0 : _schedule.event) || null,
- nextEvent: null,
- start: primaryPosition,
- end: timelineStart + segmentDuration,
- playout: {
- start: playoutStart,
- end: playoutDuration
- },
- integrated: {
- start: integratedStart,
- end: integratedTime
- }
- });
- }
- this.setDurations(primaryDuration, playoutDuration, integratedTime);
- } else {
- // no interstials - schedule is one primary segment
- const start = 0;
- schedule.push({
- previousEvent: null,
- nextEvent: null,
- start,
- end: primaryDuration,
- playout: {
- start,
- end: primaryDuration
- },
- integrated: {
- start,
- end: primaryDuration
- }
- });
- this.setDurations(primaryDuration, primaryDuration, primaryDuration);
- }
- return schedule;
- }
- setDurations(primary, playout, integrated) {
- this.durations = {
- primary,
- playout,
- integrated
- };
- }
- resolveOffsets(interstitialEvents, mediaSelection) {
- const details = mediaSelection.main.details;
- const primaryDuration = details.live ? Infinity : details.edge;
-
- // First resolve cumulative resumption offsets for Interstitials that start at the same DateTime
- let cumulativeDuration = 0;
- let lastScheduledStart = -1;
- interstitialEvents.forEach((interstitial, i) => {
- const preroll = interstitial.cue.pre;
- const postroll = interstitial.cue.post;
- const eventStart = preroll ? 0 : postroll ? primaryDuration : interstitial.startTime;
- this.updateAssetDurations(interstitial);
-
- // X-RESUME-OFFSET values of interstitials scheduled at the same time are cumulative
- const inSameStartTimeSequence = lastScheduledStart === eventStart;
- if (inSameStartTimeSequence) {
- interstitial.cumulativeDuration = cumulativeDuration;
- } else {
- cumulativeDuration = 0;
- lastScheduledStart = eventStart;
- }
- if (!postroll && interstitial.snapOptions.in) {
- // FIXME: Include audio playlist in snapping
- interstitial.resumeAnchor = findFragmentByPTS(null, details.fragments, interstitial.startOffset + interstitial.resumptionOffset, 0, 0) || undefined;
- }
- // Check if primary fragments align with resumption offset and disable appendInPlace if they do not
- if (interstitial.appendInPlace && !interstitial.appendInPlaceStarted) {
- const alignedSegmentStart = this.primaryCanResumeInPlaceAt(interstitial, mediaSelection);
- if (!alignedSegmentStart) {
- interstitial.appendInPlace = false;
- }
- }
- if (!interstitial.appendInPlace && i + 1 < interstitialEvents.length) {
- // abutting Interstitials must use the same MediaSource strategy, this applies to all whether or not they are back to back:
- const timeBetween = interstitialEvents[i + 1].startTime - interstitialEvents[i].resumeTime;
- if (timeBetween < ABUTTING_THRESHOLD_SECONDS) {
- interstitialEvents[i + 1].appendInPlace = false;
- if (interstitialEvents[i + 1].appendInPlace) {
- this.warn(`Could not change append strategy for abutting event ${interstitial}`);
- }
- }
- }
- // Update cumulativeDuration for next abutting interstitial with the same start date
- const resumeOffset = isFiniteNumber(interstitial.resumeOffset) ? interstitial.resumeOffset : interstitial.duration;
- cumulativeDuration += resumeOffset;
- });
- }
- primaryCanResumeInPlaceAt(interstitial, mediaSelection) {
- const resumeTime = interstitial.resumeTime;
- const resumesInPlaceAt = interstitial.startTime + interstitial.resumptionOffset;
- if (Math.abs(resumeTime - resumesInPlaceAt) > ALIGNED_END_THRESHOLD_SECONDS) {
- this.log(`"${interstitial.identifier}" resumption ${resumeTime} not aligned with estimated timeline end ${resumesInPlaceAt}`);
- return false;
- }
- const playlists = Object.keys(mediaSelection);
- return !playlists.some(playlistType => {
- const details = mediaSelection[playlistType].details;
- const playlistEnd = details.edge;
- if (resumeTime >= playlistEnd) {
- // Live playback - resumption segments are not yet available
- this.log(`"${interstitial.identifier}" resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`);
- // Assume alignment is possible (or reset can take place)
- return false;
- }
- const startFragment = findFragmentByPTS(null, details.fragments, resumeTime);
- if (!startFragment) {
- this.log(`"${interstitial.identifier}" resumption ${resumeTime} does not align with any fragments in ${playlistType} playlist (${details.fragStart}-${details.fragmentEnd})`);
- return true;
- }
- const allowance = playlistType === 'audio' ? 0.175 : 0;
- const alignedWithSegment = Math.abs(startFragment.start - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS + allowance || Math.abs(startFragment.end - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS + allowance;
- if (!alignedWithSegment) {
- this.log(`"${interstitial.identifier}" resumption ${resumeTime} not aligned with ${playlistType} fragment bounds (${startFragment.start}-${startFragment.end} sn: ${startFragment.sn} cc: ${startFragment.cc})`);
- return true;
- }
- return false;
- });
- }
- updateAssetDurations(interstitial) {
- if (!interstitial.assetListLoaded) {
- return;
- }
- const eventStart = interstitial.timelineStart;
- let sumDuration = 0;
- let hasUnknownDuration = false;
- let hasErrors = false;
- for (let i = 0; i < interstitial.assetList.length; i++) {
- const asset = interstitial.assetList[i];
- const timelineStart = eventStart + sumDuration;
- asset.startOffset = sumDuration;
- asset.timelineStart = timelineStart;
- hasUnknownDuration || (hasUnknownDuration = asset.duration === null);
- hasErrors || (hasErrors = !!asset.error);
- const duration = asset.error ? 0 : asset.duration || 0;
- sumDuration += duration;
- }
- // Use the sum of known durations when it is greater than the stated duration
- if (hasUnknownDuration && !hasErrors) {
- interstitial.duration = Math.max(sumDuration, interstitial.duration);
- } else {
- interstitial.duration = sumDuration;
- }
- }
- removeEvent(interstitial) {
- interstitial.reset();
- delete this.eventMap[interstitial.identifier];
- }
-}
-function segmentToString(segment) {
- return `[${segment.event ? '"' + segment.event.identifier + '"' : 'primary'}: ${segment.start.toFixed(2)}-${segment.end.toFixed(2)}]`;
-}
-
-class AssetListLoader {
- constructor(hls) {
- this.hls = void 0;
- this.hls = hls;
- }
- destroy() {
- // @ts-ignore
- this.hls = null;
- }
- loadAssetList(interstitial, hlsStartOffset) {
- const assetListUrl = interstitial.assetListUrl;
- let url;
- try {
- url = getInterstitialUrl(assetListUrl, this.hls.sessionId, interstitial.baseUrl);
- } catch (error) {
- const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, error, assetListUrl);
- this.hls.trigger(Events.ERROR, errorData);
- return;
- }
- if (hlsStartOffset && url.protocol !== 'data:') {
- url.searchParams.set('_HLS_start_offset', '' + hlsStartOffset);
- }
- const config = this.hls.config;
- const Loader = config.loader;
- const loader = new Loader(config);
- const context = {
- responseType: 'json',
- url: url.href
- };
- const loadPolicy = config.interstitialAssetListLoadPolicy.default;
- const loaderConfig = {
- loadPolicy,
- timeout: loadPolicy.maxLoadTimeMs,
- maxRetry: 0,
- retryDelay: 0,
- maxRetryDelay: 0
- };
- const callbacks = {
- onSuccess: (response, stats, context, networkDetails) => {
- const assetListResponse = response.data;
- const assets = assetListResponse == null ? void 0 : assetListResponse.ASSETS;
- if (!Array.isArray(assets)) {
- const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_PARSING_ERROR, new Error(`Invalid interstitial asset list`), context.url, stats, networkDetails);
- this.hls.trigger(Events.ERROR, errorData);
- return;
- }
- interstitial.assetListResponse = assetListResponse;
- this.hls.trigger(Events.ASSET_LIST_LOADED, {
- event: interstitial,
- assetListResponse,
- networkDetails
- });
- },
- onError: (error, context, networkDetails, stats) => {
- const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, new Error(`Error loading X-ASSET-LIST: HTTP status ${error.code} ${error.text} (${context.url})`), context.url, stats, networkDetails);
- this.hls.trigger(Events.ERROR, errorData);
- },
- onTimeout: (stats, context, networkDetails) => {
- const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_TIMEOUT, new Error(`Timeout loading X-ASSET-LIST (${context.url})`), context.url, stats, networkDetails);
- this.hls.trigger(Events.ERROR, errorData);
- }
- };
- loader.load(context, loaderConfig, callbacks);
- this.hls.trigger(Events.ASSET_LIST_LOADING, {
- event: interstitial
- });
- return loader;
- }
- assignAssetListError(interstitial, details, error, url, stats, networkDetails) {
- interstitial.error = error;
- return {
- type: ErrorTypes.NETWORK_ERROR,
- details,
- fatal: false,
- interstitial,
- url,
- error,
- networkDetails,
- stats
- };
- }
-}
-
-function playWithCatch(media) {
- var _media$play;
- media == null || (_media$play = media.play()) == null || _media$play.catch(() => {
- /* no-op */
- });
-}
-function timelineMessage(label, time) {
- return `[${label}] Advancing timeline position to ${time}`;
-}
-class InterstitialsController extends Logger {
- constructor(hls, HlsPlayerClass) {
- super('interstitials', hls.logger);
- this.HlsPlayerClass = void 0;
- this.hls = void 0;
- this.assetListLoader = void 0;
- // Last updated LevelDetails
- this.mediaSelection = null;
- this.altSelection = null;
- // Media and MediaSource/SourceBuffers
- this.media = null;
- this.detachedData = null;
- this.requiredTracks = null;
- // Public Interface for Interstitial playback state and control
- this.manager = null;
- // Interstitial Asset Players
- this.playerQueue = [];
- // Timeline position tracking
- this.bufferedPos = -1;
- this.timelinePos = -1;
- // Schedule
- this.schedule = void 0;
- // Schedule playback and buffering state
- this.playingItem = null;
- this.bufferingItem = null;
- this.waitingItem = null;
- this.endedItem = null;
- this.playingAsset = null;
- this.endedAsset = null;
- this.bufferingAsset = null;
- this.shouldPlay = false;
- this.onPlay = () => {
- this.shouldPlay = true;
- };
- this.onPause = () => {
- this.shouldPlay = false;
- };
- this.onSeeking = () => {
- const currentTime = this.currentTime;
- if (currentTime === undefined || this.playbackDisabled || !this.schedule) {
- return;
- }
- const diff = currentTime - this.timelinePos;
- const roundingError = Math.abs(diff) < 1 / 705600000; // one flick
- if (roundingError) {
- return;
- }
- const backwardSeek = diff <= -0.01;
- if (this.timelinePos === -1 && !this.effectivePlayingItem) {
- this.checkStart();
- }
- this.timelinePos = currentTime;
- this.bufferedPos = currentTime;
-
- // Check if seeking out of an item
- const playingItem = this.playingItem;
- if (!playingItem) {
- this.checkBuffer();
- return;
- }
- if (backwardSeek) {
- const resetCount = this.schedule.resetErrorsInRange(currentTime, currentTime - diff);
- if (resetCount) {
- this.updateSchedule(true);
- }
- }
- this.checkBuffer();
- if (backwardSeek && currentTime < playingItem.start || currentTime >= playingItem.end) {
- var _this$media;
- const playingIndex = this.findItemIndex(playingItem);
- let scheduleIndex = this.schedule.findItemIndexAtTime(currentTime);
- if (scheduleIndex === -1) {
- scheduleIndex = playingIndex + (backwardSeek ? -1 : 1);
- this.log(`seeked ${backwardSeek ? 'back ' : ''}to position not covered by schedule ${currentTime} (resolving from ${playingIndex} to ${scheduleIndex})`);
- }
- if (!this.isInterstitial(playingItem) && (_this$media = this.media) != null && _this$media.paused) {
- this.shouldPlay = false;
- }
- if (!backwardSeek) {
- // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction
- if (scheduleIndex > playingIndex) {
- const jumpIndex = this.schedule.findJumpRestrictedIndex(playingIndex + 1, scheduleIndex);
- if (jumpIndex > playingIndex) {
- this.setSchedulePosition(jumpIndex);
- return;
- }
- }
- }
- this.setSchedulePosition(scheduleIndex);
- return;
- }
- // Check if seeking out of an asset (assumes same item following above check)
- const playingAsset = this.playingAsset;
- if (!playingAsset) {
- // restart Interstitial at end
- if (this.playingLastItem && this.isInterstitial(playingItem)) {
- const restartAsset = playingItem.event.assetList[0];
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (restartAsset) {
- this.endedItem = this.playingItem;
- this.playingItem = null;
- this.setScheduleToAssetAtTime(currentTime, restartAsset);
- }
- }
- return;
- }
- const start = playingAsset.timelineStart;
- const duration = playingAsset.duration || 0;
- if (backwardSeek && currentTime < start || currentTime >= start + duration) {
- var _playingItem$event;
- if ((_playingItem$event = playingItem.event) != null && _playingItem$event.appendInPlace) {
- // Return SourceBuffer(s) to primary player and flush
- this.clearAssetPlayers(playingItem.event, playingItem);
- this.flushFrontBuffer(currentTime);
- }
- this.setScheduleToAssetAtTime(currentTime, playingAsset);
- }
- };
- this.onTimeupdate = () => {
- const currentTime = this.currentTime;
- if (currentTime === undefined || this.playbackDisabled) {
- return;
- }
- if (this.timelinePos === -1 && !this.effectivePlayingItem) {
- this.checkStart();
- }
-
- // Only allow timeupdate to advance primary position, seeking is used for jumping back
- // this prevents primaryPos from being reset to 0 after re-attach
- if (currentTime > this.timelinePos) {
- this.timelinePos = currentTime;
- if (currentTime > this.bufferedPos) {
- this.checkBuffer();
- }
- } else {
- return;
- }
-
- // Check if playback has entered the next item
- const playingItem = this.playingItem;
- if (!playingItem || this.playingLastItem) {
- return;
- }
- if (currentTime >= playingItem.end) {
- this.timelinePos = playingItem.end;
- const playingIndex = this.findItemIndex(playingItem);
- this.setSchedulePosition(playingIndex + 1);
- }
- // Check if playback has entered the next asset
- const playingAsset = this.playingAsset;
- if (!playingAsset) {
- return;
- }
- const end = playingAsset.timelineStart + (playingAsset.duration || 0);
- if (currentTime >= end) {
- this.setScheduleToAssetAtTime(currentTime, playingAsset);
- }
- };
- // Schedule update callback
- this.onScheduleUpdate = (removedInterstitials, previousItems) => {
- const schedule = this.schedule;
- if (!schedule) {
- return;
- }
- const playingItem = this.playingItem;
- const interstitialEvents = schedule.events || [];
- const scheduleItems = schedule.items || [];
- const durations = schedule.durations;
- const removedIds = removedInterstitials.map(interstitial => interstitial.identifier);
- const interstitialsUpdated = !!(interstitialEvents.length || removedIds.length);
- if (interstitialsUpdated || previousItems) {
- this.log(`INTERSTITIALS_UPDATED (${interstitialEvents.length}): ${interstitialEvents}
-Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timelinePos}`);
- }
- if (removedIds.length) {
- this.log(`Removed events ${removedIds}`);
- }
-
- // Update schedule item references
- // Do not replace Interstitial playingItem without a match - used for INTERSTITIAL_ASSET_ENDED and INTERSTITIAL_ENDED
- let updatedPlayingItem = null;
- let updatedBufferingItem = null;
- if (playingItem) {
- updatedPlayingItem = this.updateItem(playingItem, this.timelinePos);
- if (this.itemsMatch(playingItem, updatedPlayingItem)) {
- this.playingItem = updatedPlayingItem;
- } else {
- this.waitingItem = this.endedItem = null;
- }
- }
- // Clear waitingItem if it has been removed from the schedule
- this.waitingItem = this.updateItem(this.waitingItem);
- this.endedItem = this.updateItem(this.endedItem);
- // Do not replace Interstitial bufferingItem without a match - used for transfering media element or source
- const bufferingItem = this.bufferingItem;
- if (bufferingItem) {
- updatedBufferingItem = this.updateItem(bufferingItem, this.bufferedPos);
- if (this.itemsMatch(bufferingItem, updatedBufferingItem)) {
- this.bufferingItem = updatedBufferingItem;
- } else if (bufferingItem.event) {
- // Interstitial removed from schedule (Live -> VOD or other scenario where Start Date is outside the range of VOD Playlist)
- this.bufferingItem = this.playingItem;
- this.clearInterstitial(bufferingItem.event, null);
- }
- }
- removedInterstitials.forEach(interstitial => {
- interstitial.assetList.forEach(asset => {
- this.clearAssetPlayer(asset.identifier, null);
- });
- });
- this.playerQueue.forEach(player => {
- if (player.interstitial.appendInPlace) {
- const timelineStart = player.assetItem.timelineStart;
- const diff = player.timelineOffset - timelineStart;
- if (diff) {
- try {
- player.timelineOffset = timelineStart;
- } catch (e) {
- if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) {
- this.warn(`${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`);
- }
- }
- }
- }
- });
- if (interstitialsUpdated || previousItems) {
- this.hls.trigger(Events.INTERSTITIALS_UPDATED, {
- events: interstitialEvents.slice(0),
- schedule: scheduleItems.slice(0),
- durations,
- removedIds
- });
- if (this.isInterstitial(playingItem) && removedIds.includes(playingItem.event.identifier)) {
- this.warn(`Interstitial "${playingItem.event.identifier}" removed while playing`);
- this.primaryFallback(playingItem.event);
- return;
- }
- if (playingItem) {
- this.trimInPlace(updatedPlayingItem, playingItem);
- }
- if (bufferingItem && updatedBufferingItem !== updatedPlayingItem) {
- this.trimInPlace(updatedBufferingItem, bufferingItem);
- }
-
- // Check if buffered to new Interstitial event boundary
- // (Live update publishes Interstitial with new segment)
- this.checkBuffer();
- }
- };
- this.hls = hls;
- this.HlsPlayerClass = HlsPlayerClass;
- this.assetListLoader = new AssetListLoader(hls);
- this.schedule = new InterstitialsSchedule(this.onScheduleUpdate, hls.logger);
- this.registerListeners();
- }
- registerListeners() {
- const hls = this.hls;
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (hls) {
- hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
- hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
- hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
- hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this);
- hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
- hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this);
- hls.on(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this);
- hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this);
- hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
- hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
- hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this);
- hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this);
- hls.on(Events.ERROR, this.onError, this);
- hls.on(Events.DESTROYING, this.onDestroying, this);
- }
- }
- unregisterListeners() {
- const hls = this.hls;
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (hls) {
- hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
- hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
- hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
- hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this);
- hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
- hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this);
- hls.off(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this);
- hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this);
- hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
- hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
- hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
- hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this);
- hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this);
- hls.off(Events.ERROR, this.onError, this);
- hls.off(Events.DESTROYING, this.onDestroying, this);
- }
- }
- startLoad() {
- // TODO: startLoad - check for waitingItem and retry by resetting schedule
- this.resumeBuffering();
- }
- stopLoad() {
- // TODO: stopLoad - stop all scheule.events[].assetListLoader?.abort() then delete the loaders
- this.pauseBuffering();
- }
- resumeBuffering() {
- var _this$getBufferingPla;
- (_this$getBufferingPla = this.getBufferingPlayer()) == null || _this$getBufferingPla.resumeBuffering();
- }
- pauseBuffering() {
- var _this$getBufferingPla2;
- (_this$getBufferingPla2 = this.getBufferingPlayer()) == null || _this$getBufferingPla2.pauseBuffering();
- }
- destroy() {
- this.unregisterListeners();
- this.stopLoad();
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (this.assetListLoader) {
- this.assetListLoader.destroy();
- }
- this.emptyPlayerQueue();
- this.clearScheduleState();
- if (this.schedule) {
- this.schedule.destroy();
- }
- this.media = this.detachedData = this.mediaSelection = this.requiredTracks = this.altSelection = this.schedule = this.manager = null;
- // @ts-ignore
- this.hls = this.HlsPlayerClass = this.log = null;
- // @ts-ignore
- this.assetListLoader = null;
- // @ts-ignore
- this.onPlay = this.onPause = this.onSeeking = this.onTimeupdate = null;
- // @ts-ignore
- this.onScheduleUpdate = null;
- }
- onDestroying() {
- const media = this.primaryMedia || this.media;
- if (media) {
- this.removeMediaListeners(media);
- }
- }
- removeMediaListeners(media) {
- removeEventListener(media, 'play', this.onPlay);
- removeEventListener(media, 'pause', this.onPause);
- removeEventListener(media, 'seeking', this.onSeeking);
- removeEventListener(media, 'timeupdate', this.onTimeupdate);
- }
- onMediaAttaching(event, data) {
- const media = this.media = data.media;
- addEventListener(media, 'seeking', this.onSeeking);
- addEventListener(media, 'timeupdate', this.onTimeupdate);
- addEventListener(media, 'play', this.onPlay);
- addEventListener(media, 'pause', this.onPause);
- }
- onMediaAttached(event, data) {
- const playingItem = this.effectivePlayingItem;
- const detachedMedia = this.detachedData;
- this.detachedData = null;
- if (playingItem === null) {
- this.checkStart();
- } else if (!detachedMedia) {
- // Resume schedule after detached externally
- this.clearScheduleState();
- const playingIndex = this.findItemIndex(playingItem);
- this.setSchedulePosition(playingIndex);
- }
- }
- clearScheduleState() {
- this.log(`clear schedule state`);
- this.playingItem = this.bufferingItem = this.waitingItem = this.endedItem = this.playingAsset = this.endedAsset = this.bufferingAsset = null;
- }
- onMediaDetaching(event, data) {
- const transferringMedia = !!data.transferMedia;
- const media = this.media;
- this.media = null;
- if (transferringMedia) {
- return;
- }
- if (media) {
- this.removeMediaListeners(media);
- }
- // If detachMedia is called while in an Interstitial, detach the asset player as well and reset the schedule position
- if (this.detachedData) {
- const player = this.getBufferingPlayer();
- if (player) {
- this.log(`Removing schedule state for detachedData and ${player}`);
- this.playingAsset = this.endedAsset = this.bufferingAsset = this.bufferingItem = this.waitingItem = this.detachedData = null;
- player.detachMedia();
- }
- this.shouldPlay = false;
- }
- }
- get interstitialsManager() {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (!this.hls) {
- return null;
- }
- if (this.manager) {
- return this.manager;
- }
- const c = this;
- const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem;
- const getAssetPlayer = asset => asset ? c.getAssetPlayer(asset.identifier) : asset;
- const getMappedTime = (item, timelineType, asset, controllerField, assetPlayerField) => {
- if (item) {
- let time = item[timelineType].start;
- const interstitial = item.event;
- if (interstitial) {
- if (timelineType === 'playout' || interstitial.timelineOccupancy !== TimelineOccupancy.Point) {
- const assetPlayer = getAssetPlayer(asset);
- if ((assetPlayer == null ? void 0 : assetPlayer.interstitial) === interstitial) {
- time += assetPlayer.assetItem.startOffset + assetPlayer[assetPlayerField];
- }
- }
- } else {
- const value = controllerField === 'bufferedPos' ? getBufferedEnd() : c[controllerField];
- time += value - item.start;
- }
- return time;
- }
- return 0;
- };
- const findMappedTime = (primaryTime, timelineType) => {
- var _c$schedule;
- if (primaryTime !== 0 && timelineType !== 'primary' && (_c$schedule = c.schedule) != null && _c$schedule.length) {
- var _c$schedule$items;
- const index = c.schedule.findItemIndexAtTime(primaryTime);
- const item = (_c$schedule$items = c.schedule.items) == null ? void 0 : _c$schedule$items[index];
- if (item) {
- const diff = item[timelineType].start - item.start;
- return primaryTime + diff;
- }
- }
- return primaryTime;
- };
- const getBufferedEnd = () => {
- const value = c.bufferedPos;
- if (value === Number.MAX_VALUE) {
- return getMappedDuration('primary');
- }
- return Math.max(value, 0);
- };
- const getMappedDuration = timelineType => {
- var _c$primaryDetails, _c$schedule2;
- if ((_c$primaryDetails = c.primaryDetails) != null && _c$primaryDetails.live) {
- // return end of last event item or playlist
- return c.primaryDetails.edge;
- }
- return ((_c$schedule2 = c.schedule) == null ? void 0 : _c$schedule2.durations[timelineType]) || 0;
- };
- const seekTo = (time, timelineType) => {
- var _item$event, _c$schedule$items2;
- const item = c.effectivePlayingItem;
- if (item != null && (_item$event = item.event) != null && _item$event.restrictions.skip || !c.schedule) {
- return;
- }
- c.log(`seek to ${time} "${timelineType}"`);
- const playingItem = c.effectivePlayingItem;
- const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType);
- const targetItem = (_c$schedule$items2 = c.schedule.items) == null ? void 0 : _c$schedule$items2[targetIndex];
- const bufferingPlayer = c.getBufferingPlayer();
- const bufferingInterstitial = bufferingPlayer == null ? void 0 : bufferingPlayer.interstitial;
- const appendInPlace = bufferingInterstitial == null ? void 0 : bufferingInterstitial.appendInPlace;
- const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem);
- if (playingItem && (appendInPlace || seekInItem)) {
- // seek in asset player or primary media (appendInPlace)
- const assetPlayer = getAssetPlayer(c.playingAsset);
- const media = (assetPlayer == null ? void 0 : assetPlayer.media) || c.primaryMedia;
- if (media) {
- const currentTime = timelineType === 'primary' ? media.currentTime : getMappedTime(playingItem, timelineType, c.playingAsset, 'timelinePos', 'currentTime');
- const diff = time - currentTime;
- const seekToTime = (appendInPlace ? currentTime : media.currentTime) + diff;
- if (seekToTime >= 0 && (!assetPlayer || appendInPlace || seekToTime <= assetPlayer.duration)) {
- media.currentTime = seekToTime;
- return;
- }
- }
- }
- // seek out of item or asset
- if (targetItem) {
- let seekToTime = time;
- if (timelineType !== 'primary') {
- const primarySegmentStart = targetItem[timelineType].start;
- const diff = time - primarySegmentStart;
- seekToTime = targetItem.start + diff;
- }
- const targetIsPrimary = !c.isInterstitial(targetItem);
- if ((!c.isInterstitial(playingItem) || playingItem.event.appendInPlace) && (targetIsPrimary || targetItem.event.appendInPlace)) {
- const media = c.media || (appendInPlace ? bufferingPlayer == null ? void 0 : bufferingPlayer.media : null);
- if (media) {
- media.currentTime = seekToTime;
- }
- } else if (playingItem) {
- // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction
- const playingIndex = c.findItemIndex(playingItem);
- if (targetIndex > playingIndex) {
- const jumpIndex = c.schedule.findJumpRestrictedIndex(playingIndex + 1, targetIndex);
- if (jumpIndex > playingIndex) {
- c.setSchedulePosition(jumpIndex);
- return;
- }
- }
- let assetIndex = 0;
- if (targetIsPrimary) {
- c.timelinePos = seekToTime;
- c.checkBuffer();
- } else {
- const assetList = targetItem.event.assetList;
- const eventTime = time - (targetItem[timelineType] || targetItem).start;
- for (let i = assetList.length; i--;) {
- const asset = assetList[i];
- if (asset.duration && eventTime >= asset.startOffset && eventTime < asset.startOffset + asset.duration) {
- assetIndex = i;
- break;
- }
- }
- }
- c.setSchedulePosition(targetIndex, assetIndex);
- }
- }
- };
- const getActiveInterstitial = () => {
- const playingItem = c.effectivePlayingItem;
- if (c.isInterstitial(playingItem)) {
- return playingItem;
- }
- const bufferingItem = effectiveBufferingItem();
- if (c.isInterstitial(bufferingItem)) {
- return bufferingItem;
- }
- return null;
- };
- const interstitialPlayer = {
- get bufferedEnd() {
- const interstitialItem = effectiveBufferingItem();
- const bufferingItem = c.bufferingItem;
- if (bufferingItem && bufferingItem === interstitialItem) {
- var _c$bufferingAsset;
- return getMappedTime(bufferingItem, 'playout', c.bufferingAsset, 'bufferedPos', 'bufferedEnd') - bufferingItem.playout.start || ((_c$bufferingAsset = c.bufferingAsset) == null ? void 0 : _c$bufferingAsset.startOffset) || 0;
- }
- return 0;
- },
- get currentTime() {
- const interstitialItem = getActiveInterstitial();
- const playingItem = c.effectivePlayingItem;
- if (playingItem && playingItem === interstitialItem) {
- return getMappedTime(playingItem, 'playout', c.effectivePlayingAsset, 'timelinePos', 'currentTime') - playingItem.playout.start;
- }
- return 0;
- },
- set currentTime(time) {
- const interstitialItem = getActiveInterstitial();
- const playingItem = c.effectivePlayingItem;
- if (playingItem && playingItem === interstitialItem) {
- seekTo(time + playingItem.playout.start, 'playout');
- }
- },
- get duration() {
- const interstitialItem = getActiveInterstitial();
- if (interstitialItem) {
- return interstitialItem.playout.end - interstitialItem.playout.start;
- }
- return 0;
- },
- get assetPlayers() {
- var _getActiveInterstitia;
- const assetList = (_getActiveInterstitia = getActiveInterstitial()) == null ? void 0 : _getActiveInterstitia.event.assetList;
- if (assetList) {
- return assetList.map(asset => c.getAssetPlayer(asset.identifier));
- }
- return [];
- },
- get playingIndex() {
- var _getActiveInterstitia2;
- const interstitial = (_getActiveInterstitia2 = getActiveInterstitial()) == null ? void 0 : _getActiveInterstitia2.event;
- if (interstitial && c.effectivePlayingAsset) {
- return interstitial.findAssetIndex(c.effectivePlayingAsset);
- }
- return -1;
- },
- get scheduleItem() {
- return getActiveInterstitial();
- }
- };
- return this.manager = {
- get events() {
- var _c$schedule3;
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- return ((_c$schedule3 = c.schedule) == null || (_c$schedule3 = _c$schedule3.events) == null ? void 0 : _c$schedule3.slice(0)) || [];
- },
- get schedule() {
- var _c$schedule4;
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- return ((_c$schedule4 = c.schedule) == null || (_c$schedule4 = _c$schedule4.items) == null ? void 0 : _c$schedule4.slice(0)) || [];
- },
- get interstitialPlayer() {
- if (getActiveInterstitial()) {
- return interstitialPlayer;
- }
- return null;
- },
- get playerQueue() {
- return c.playerQueue.slice(0);
- },
- get bufferingAsset() {
- return c.bufferingAsset;
- },
- get bufferingItem() {
- return effectiveBufferingItem();
- },
- get bufferingIndex() {
- const item = effectiveBufferingItem();
- return c.findItemIndex(item);
- },
- get playingAsset() {
- return c.effectivePlayingAsset;
- },
- get playingItem() {
- return c.effectivePlayingItem;
- },
- get playingIndex() {
- const item = c.effectivePlayingItem;
- return c.findItemIndex(item);
- },
- primary: {
- get bufferedEnd() {
- return getBufferedEnd();
- },
- get currentTime() {
- const timelinePos = c.timelinePos;
- return timelinePos > 0 ? timelinePos : 0;
- },
- set currentTime(time) {
- seekTo(time, 'primary');
- },
- get duration() {
- return getMappedDuration('primary');
- },
- get seekableStart() {
- var _c$primaryDetails2;
- return ((_c$primaryDetails2 = c.primaryDetails) == null ? void 0 : _c$primaryDetails2.fragmentStart) || 0;
- }
- },
- integrated: {
- get bufferedEnd() {
- return getMappedTime(effectiveBufferingItem(), 'integrated', c.bufferingAsset, 'bufferedPos', 'bufferedEnd');
- },
- get currentTime() {
- return getMappedTime(c.effectivePlayingItem, 'integrated', c.effectivePlayingAsset, 'timelinePos', 'currentTime');
- },
- set currentTime(time) {
- seekTo(time, 'integrated');
- },
- get duration() {
- return getMappedDuration('integrated');
- },
- get seekableStart() {
- var _c$primaryDetails3;
- return findMappedTime(((_c$primaryDetails3 = c.primaryDetails) == null ? void 0 : _c$primaryDetails3.fragmentStart) || 0, 'integrated');
- }
- },
- skip: () => {
- const item = c.effectivePlayingItem;
- const event = item == null ? void 0 : item.event;
- if (event && !event.restrictions.skip) {
- const index = c.findItemIndex(item);
- if (event.appendInPlace) {
- const time = item.playout.start + item.event.duration;
- seekTo(time + 0.001, 'playout');
- } else {
- c.advanceAfterAssetEnded(event, index, Infinity);
- }
- }
- }
- };
- }
-
- // Schedule getters
- get effectivePlayingItem() {
- return this.waitingItem || this.playingItem || this.endedItem;
- }
- get effectivePlayingAsset() {
- return this.playingAsset || this.endedAsset;
- }
- get playingLastItem() {
- var _this$schedule;
- const playingItem = this.playingItem;
- const items = (_this$schedule = this.schedule) == null ? void 0 : _this$schedule.items;
- if (!this.playbackStarted || !playingItem || !items) {
- return false;
- }
- return this.findItemIndex(playingItem) === items.length - 1;
- }
- get playbackStarted() {
- return this.effectivePlayingItem !== null;
- }
-
- // Media getters and event callbacks
- get currentTime() {
- var _this$bufferingItem, _media;
- if (this.mediaSelection === null) {
- // Do not advance before schedule is known
- return undefined;
- }
- // Ignore currentTime when detached for Interstitial playback with source reset
- const queuedForPlayback = this.waitingItem || this.playingItem;
- if (this.isInterstitial(queuedForPlayback) && !queuedForPlayback.event.appendInPlace) {
- return undefined;
- }
- let media = this.media;
- if (!media && (_this$bufferingItem = this.bufferingItem) != null && (_this$bufferingItem = _this$bufferingItem.event) != null && _this$bufferingItem.appendInPlace) {
- // Observe detached media currentTime when appending in place
- media = this.primaryMedia;
- }
- const currentTime = (_media = media) == null ? void 0 : _media.currentTime;
- if (currentTime === undefined || !isFiniteNumber(currentTime)) {
- return undefined;
- }
- return currentTime;
- }
- get primaryMedia() {
- var _this$detachedData;
- return this.media || ((_this$detachedData = this.detachedData) == null ? void 0 : _this$detachedData.media) || null;
- }
- isInterstitial(item) {
- return !!(item != null && item.event);
- }
- retreiveMediaSource(assetId, toSegment) {
- const player = this.getAssetPlayer(assetId);
- if (player) {
- this.transferMediaFromPlayer(player, toSegment);
- }
- }
- transferMediaFromPlayer(player, toSegment) {
- const appendInPlace = player.interstitial.appendInPlace;
- const playerMedia = player.media;
- if (appendInPlace && playerMedia === this.primaryMedia) {
- this.bufferingAsset = null;
- if (!toSegment || this.isInterstitial(toSegment) && !toSegment.event.appendInPlace) {
- // MediaSource cannot be transfered back to an Interstitial that requires a source reset
- // no-op when toSegment is undefined
- if (toSegment && playerMedia) {
- this.detachedData = {
- media: playerMedia
- };
- return;
- }
- }
- const attachMediaSourceData = player.transferMedia();
- this.log(`transfer MediaSource from ${player} ${stringify(attachMediaSourceData)}`);
- this.detachedData = attachMediaSourceData;
- } else if (toSegment && playerMedia) {
- this.shouldPlay || (this.shouldPlay = !playerMedia.paused);
- }
- }
- transferMediaTo(player, media) {
- var _this$detachedData2, _attachMediaSourceDat;
- if (player.media === media) {
- return;
- }
- let attachMediaSourceData = null;
- const primaryPlayer = this.hls;
- const isAssetPlayer = player !== primaryPlayer;
- const appendInPlace = isAssetPlayer && player.interstitial.appendInPlace;
- const detachedMediaSource = (_this$detachedData2 = this.detachedData) == null ? void 0 : _this$detachedData2.mediaSource;
- let logFromSource;
- if (primaryPlayer.media) {
- if (appendInPlace) {
- attachMediaSourceData = primaryPlayer.transferMedia();
- this.detachedData = attachMediaSourceData;
- }
- logFromSource = `Primary`;
- } else if (detachedMediaSource) {
- const bufferingPlayer = this.getBufferingPlayer();
- if (bufferingPlayer) {
- attachMediaSourceData = bufferingPlayer.transferMedia();
- logFromSource = `${bufferingPlayer}`;
- } else {
- logFromSource = `detached MediaSource`;
- }
- } else {
- logFromSource = `detached media`;
- }
- if (!attachMediaSourceData) {
- if (detachedMediaSource) {
- attachMediaSourceData = this.detachedData;
- this.log(`using detachedData: MediaSource ${stringify(attachMediaSourceData)}`);
- } else if (!this.detachedData || primaryPlayer.media === media) {
- // Keep interstitial media transition consistent
- const playerQueue = this.playerQueue;
- if (playerQueue.length > 1) {
- playerQueue.forEach(queuedPlayer => {
- if (isAssetPlayer && queuedPlayer.interstitial.appendInPlace !== appendInPlace) {
- const interstitial = queuedPlayer.interstitial;
- this.clearInterstitial(queuedPlayer.interstitial, null);
- interstitial.appendInPlace = false; // setter may be a no-op;
- // `appendInPlace` getter may still return `true` after insterstitial streaming has begun in that mode.
- if (interstitial.appendInPlace) {
- this.warn(`Could not change append strategy for queued assets ${interstitial}`);
- }
- }
- });
- }
- this.hls.detachMedia();
- this.detachedData = {
- media
- };
- }
- }
- const transferring = attachMediaSourceData && 'mediaSource' in attachMediaSourceData && ((_attachMediaSourceDat = attachMediaSourceData.mediaSource) == null ? void 0 : _attachMediaSourceDat.readyState) !== 'closed';
- const dataToAttach = transferring && attachMediaSourceData ? attachMediaSourceData : media;
- this.log(`${transferring ? 'transfering MediaSource' : 'attaching media'} to ${isAssetPlayer ? player : 'Primary'} from ${logFromSource} (media.currentTime: ${media.currentTime})`);
- const schedule = this.schedule;
- if (dataToAttach === attachMediaSourceData && schedule) {
- const isAssetAtEndOfSchedule = isAssetPlayer && player.assetId === schedule.assetIdAtEnd;
- // Prevent asset players from marking EoS on transferred MediaSource
- dataToAttach.overrides = {
- duration: schedule.duration,
- endOfStream: !isAssetPlayer || isAssetAtEndOfSchedule,
- cueRemoval: !isAssetPlayer
- };
- }
- player.attachMedia(dataToAttach);
- }
- onInterstitialCueEnter() {
- this.onTimeupdate();
- }
- // Scheduling methods
- checkStart() {
- const schedule = this.schedule;
- const interstitialEvents = schedule == null ? void 0 : schedule.events;
- if (!interstitialEvents || this.playbackDisabled || !this.media) {
- return;
- }
- // Check buffered to pre-roll
- if (this.bufferedPos === -1) {
- this.bufferedPos = 0;
- }
- // Start stepping through schedule when playback begins for the first time and we have a pre-roll
- const timelinePos = this.timelinePos;
- const effectivePlayingItem = this.effectivePlayingItem;
- if (timelinePos === -1) {
- const startPosition = this.hls.startPosition;
- this.timelinePos = startPosition;
- if (interstitialEvents.length === 0) {
- this.setSchedulePosition(0);
- } else if (interstitialEvents[0].cue.pre) {
- this.log(timelineMessage('checkStart (preroll)', startPosition));
- const index = schedule.findEventIndex(interstitialEvents[0].identifier);
- this.setSchedulePosition(index);
- } else if (startPosition >= 0 || !this.primaryLive) {
- this.log(timelineMessage('checkStart', startPosition));
- const start = this.timelinePos = startPosition > 0 ? startPosition : 0;
- const index = schedule.findItemIndexAtTime(start);
- this.setSchedulePosition(index);
- } else if (this.hls.liveSyncPosition === 0) {
- this.setSchedulePosition(0);
- } else {
- this.log('[checkStart] waiting for live start');
- }
- } else if (effectivePlayingItem && !this.playingItem) {
- this.log(timelineMessage('checkStart (playing item)', effectivePlayingItem.start));
- const index = schedule.findItemIndex(effectivePlayingItem);
- this.setSchedulePosition(index);
- }
- }
- advanceAssetBuffering(item, assetItem) {
- const interstitial = item.event;
- const assetListIndex = interstitial.findAssetIndex(assetItem);
- const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex);
- if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) {
- this.bufferedToEvent(item, nextAssetIndex);
- } else if (this.schedule) {
- var _this$schedule$items;
- const nextItem = (_this$schedule$items = this.schedule.items) == null ? void 0 : _this$schedule$items[this.findItemIndex(item) + 1];
- if (nextItem) {
- this.bufferedToItem(nextItem);
- }
- }
- }
- advanceAfterAssetEnded(interstitial, index, assetListIndex) {
- const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex);
- if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) {
- // Advance to next asset list item
- if (interstitial.appendInPlace) {
- const assetItem = interstitial.assetList[nextAssetIndex];
- if (assetItem) {
- this.advanceInPlace(assetItem.timelineStart);
- }
- }
- this.setSchedulePosition(index, nextAssetIndex);
- } else if (this.schedule) {
- // Advance to next schedule segment
- // check if we've reached the end of the program
- const scheduleItems = this.schedule.items;
- if (scheduleItems) {
- const nextIndex = index + 1;
- const scheduleLength = scheduleItems.length;
- if (nextIndex >= scheduleLength) {
- this.setSchedulePosition(-1);
- return;
- }
- const resumptionTime = interstitial.resumeTime;
- if (this.timelinePos < resumptionTime) {
- this.log(timelineMessage('advanceAfterAssetEnded', resumptionTime));
- this.timelinePos = resumptionTime;
- if (interstitial.appendInPlace) {
- this.advanceInPlace(resumptionTime);
- }
- this.checkBuffer(this.bufferedPos < resumptionTime);
- }
- this.setSchedulePosition(nextIndex);
- }
- }
- }
- setScheduleToAssetAtTime(time, playingAsset) {
- const schedule = this.schedule;
- if (!schedule) {
- return;
- }
- const parentIdentifier = playingAsset.parentIdentifier;
- const interstitial = schedule.getEvent(parentIdentifier);
- if (interstitial) {
- const itemIndex = schedule.findEventIndex(parentIdentifier);
- const assetListIndex = schedule.findAssetIndex(interstitial, time);
- this.advanceAfterAssetEnded(interstitial, itemIndex, assetListIndex - 1);
- }
- }
- setSchedulePosition(index, assetListIndex) {
- var _this$schedule2;
- const scheduleItems = (_this$schedule2 = this.schedule) == null ? void 0 : _this$schedule2.items;
- if (!scheduleItems || this.playbackDisabled) {
- return;
- }
- const scheduledItem = index >= 0 ? scheduleItems[index] : null;
- this.log(`setSchedulePosition ${index}, ${assetListIndex} (${scheduledItem ? segmentToString(scheduledItem) : scheduledItem}) pos: ${this.timelinePos}`);
- // Cleanup current item / asset
- const currentItem = this.waitingItem || this.playingItem;
- const playingLastItem = this.playingLastItem;
- if (this.isInterstitial(currentItem)) {
- const interstitial = currentItem.event;
- const playingAsset = this.playingAsset;
- const assetId = playingAsset == null ? void 0 : playingAsset.identifier;
- const player = assetId ? this.getAssetPlayer(assetId) : null;
- if (player && assetId && (!this.eventItemsMatch(currentItem, scheduledItem) || assetListIndex !== undefined && assetId !== interstitial.assetList[assetListIndex].identifier)) {
- var _this$detachedData3;
- const playingAssetListIndex = interstitial.findAssetIndex(playingAsset);
- this.log(`INTERSTITIAL_ASSET_ENDED ${playingAssetListIndex + 1}/${interstitial.assetList.length} ${eventAssetToString(playingAsset)}`);
- this.endedAsset = playingAsset;
- this.playingAsset = null;
- this.hls.trigger(Events.INTERSTITIAL_ASSET_ENDED, {
- asset: playingAsset,
- assetListIndex: playingAssetListIndex,
- event: interstitial,
- schedule: scheduleItems.slice(0),
- scheduleIndex: index,
- player
- });
- if (currentItem !== this.playingItem) {
- // Schedule change occured on INTERSTITIAL_ASSET_ENDED
- if (this.itemsMatch(currentItem, this.playingItem) &&
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- !this.playingAsset // INTERSTITIAL_ASSET_ENDED side-effect
- ) {
- this.advanceAfterAssetEnded(interstitial, this.findItemIndex(this.playingItem), playingAssetListIndex);
- }
- // Navigation occured on INTERSTITIAL_ASSET_ENDED
- return;
- }
- this.retreiveMediaSource(assetId, scheduledItem);
- if (player.media && !((_this$detachedData3 = this.detachedData) != null && _this$detachedData3.mediaSource)) {
- player.detachMedia();
- }
- }
- if (!this.eventItemsMatch(currentItem, scheduledItem)) {
- this.endedItem = currentItem;
- this.playingItem = null;
- this.log(`INTERSTITIAL_ENDED ${interstitial} ${segmentToString(currentItem)}`);
- interstitial.hasPlayed = true;
- this.hls.trigger(Events.INTERSTITIAL_ENDED, {
- event: interstitial,
- schedule: scheduleItems.slice(0),
- scheduleIndex: index
- });
- // Exiting an Interstitial
- if (interstitial.cue.once) {
- var _this$schedule3;
- // Remove interstitial with CUE attribute value of ONCE after it has played
- this.updateSchedule();
- const updatedScheduleItems = (_this$schedule3 = this.schedule) == null ? void 0 : _this$schedule3.items;
- if (scheduledItem && updatedScheduleItems) {
- const updatedIndex = this.findItemIndex(scheduledItem);
- this.advanceSchedule(updatedIndex, updatedScheduleItems, assetListIndex, currentItem, playingLastItem);
- }
- return;
- }
- }
- }
- this.advanceSchedule(index, scheduleItems, assetListIndex, currentItem, playingLastItem);
- }
- advanceSchedule(index, scheduleItems, assetListIndex, currentItem, playedLastItem) {
- const schedule = this.schedule;
- if (!schedule) {
- return;
- }
- const scheduledItem = scheduleItems[index] || null;
- const media = this.primaryMedia;
- // Cleanup out of range Interstitials
- const playerQueue = this.playerQueue;
- if (playerQueue.length) {
- playerQueue.forEach(player => {
- const interstitial = player.interstitial;
- const queuedIndex = schedule.findEventIndex(interstitial.identifier);
- if (queuedIndex < index || queuedIndex > index + 1) {
- this.clearInterstitial(interstitial, scheduledItem);
- }
- });
- }
- // Setup scheduled item
- if (this.isInterstitial(scheduledItem)) {
- this.timelinePos = Math.min(Math.max(this.timelinePos, scheduledItem.start), scheduledItem.end);
- // Handle Interstitial
- const interstitial = scheduledItem.event;
- // find asset index
- if (assetListIndex === undefined) {
- assetListIndex = schedule.findAssetIndex(interstitial, this.timelinePos);
- const assetIndexCandidate = getNextAssetIndex(interstitial, assetListIndex - 1);
- if (interstitial.isAssetPastPlayoutLimit(assetIndexCandidate) || interstitial.appendInPlace && this.timelinePos === scheduledItem.end) {
- this.advanceAfterAssetEnded(interstitial, index, assetListIndex);
- return;
- }
- assetListIndex = assetIndexCandidate;
- }
- // Ensure Interstitial is enqueued
- const waitingItem = this.waitingItem;
- if (!this.assetsBuffered(scheduledItem, media)) {
- this.setBufferingItem(scheduledItem);
- }
- let player = this.preloadAssets(interstitial, assetListIndex);
- if (!this.eventItemsMatch(scheduledItem, waitingItem || currentItem)) {
- this.waitingItem = scheduledItem;
- this.log(`INTERSTITIAL_STARTED ${segmentToString(scheduledItem)} ${interstitial.appendInPlace ? 'append in place' : ''}`);
- this.hls.trigger(Events.INTERSTITIAL_STARTED, {
- event: interstitial,
- schedule: scheduleItems.slice(0),
- scheduleIndex: index
- });
- }
- if (!interstitial.assetListLoaded) {
- // Waiting at end of primary content segment
- // Expect setSchedulePosition to be called again once ASSET-LIST is loaded
- this.log(`Waiting for ASSET-LIST to complete loading ${interstitial}`);
- return;
- }
- if (interstitial.assetListLoader) {
- interstitial.assetListLoader.destroy();
- interstitial.assetListLoader = undefined;
- }
- if (!media) {
- this.log(`Waiting for attachMedia to start Interstitial ${interstitial}`);
- return;
- }
- // Update schedule and asset list position now that it can start
- this.waitingItem = this.endedItem = null;
- this.playingItem = scheduledItem;
-
- // If asset-list is empty or missing asset index, advance to next item
- const assetItem = interstitial.assetList[assetListIndex];
- if (!assetItem) {
- this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0);
- return;
- }
-
- // Start Interstitial Playback
- if (!player) {
- player = this.getAssetPlayer(assetItem.identifier);
- }
- if (player === null || player.destroyed) {
- const assetListLength = interstitial.assetList.length;
- this.warn(`asset ${assetListIndex + 1}/${assetListLength} player destroyed ${interstitial}`);
- player = this.createAssetPlayer(interstitial, assetItem, assetListIndex);
- player.loadSource();
- }
- if (!this.eventItemsMatch(scheduledItem, this.bufferingItem)) {
- if (interstitial.appendInPlace && this.isAssetBuffered(assetItem)) {
- return;
- }
- }
- this.startAssetPlayer(player, assetListIndex, scheduleItems, index, media);
- if (this.shouldPlay) {
- playWithCatch(player.media);
- }
- } else if (scheduledItem) {
- this.resumePrimary(scheduledItem, index, currentItem);
- if (this.shouldPlay) {
- playWithCatch(this.hls.media);
- }
- } else if (playedLastItem && this.isInterstitial(currentItem)) {
- // Maintain playingItem state at end of schedule (setSchedulePosition(-1) called to end program)
- // this allows onSeeking handler to update schedule position
- this.endedItem = null;
- this.playingItem = currentItem;
- if (!currentItem.event.appendInPlace) {
- // Media must be re-attached to resume primary schedule if not sharing source
- this.attachPrimary(schedule.durations.primary, null);
- }
- }
- }
- get playbackDisabled() {
- return this.hls.config.enableInterstitialPlayback === false;
- }
- get primaryDetails() {
- var _this$mediaSelection;
- return (_this$mediaSelection = this.mediaSelection) == null ? void 0 : _this$mediaSelection.main.details;
- }
- get primaryLive() {
- var _this$primaryDetails;
- return !!((_this$primaryDetails = this.primaryDetails) != null && _this$primaryDetails.live);
- }
- resumePrimary(scheduledItem, index, fromItem) {
- var _this$detachedData4, _this$schedule4;
- this.playingItem = scheduledItem;
- this.playingAsset = this.endedAsset = null;
- this.waitingItem = this.endedItem = null;
- this.bufferedToItem(scheduledItem);
- this.log(`resuming ${segmentToString(scheduledItem)}`);
- if (!((_this$detachedData4 = this.detachedData) != null && _this$detachedData4.mediaSource)) {
- let timelinePos = this.timelinePos;
- if (timelinePos < scheduledItem.start || timelinePos >= scheduledItem.end) {
- timelinePos = this.getPrimaryResumption(scheduledItem, index);
- this.log(timelineMessage('resumePrimary', timelinePos));
- this.timelinePos = timelinePos;
- }
- this.attachPrimary(timelinePos, scheduledItem);
- }
- if (!fromItem) {
- return;
- }
- const scheduleItems = (_this$schedule4 = this.schedule) == null ? void 0 : _this$schedule4.items;
- if (!scheduleItems) {
- return;
- }
- this.log(`INTERSTITIALS_PRIMARY_RESUMED ${segmentToString(scheduledItem)}`);
- this.hls.trigger(Events.INTERSTITIALS_PRIMARY_RESUMED, {
- schedule: scheduleItems.slice(0),
- scheduleIndex: index
- });
- this.checkBuffer();
- }
- getPrimaryResumption(scheduledItem, index) {
- const itemStart = scheduledItem.start;
- if (this.primaryLive) {
- const details = this.primaryDetails;
- if (index === 0) {
- return this.hls.startPosition;
- } else if (details && (itemStart < details.fragmentStart || itemStart > details.edge)) {
- return this.hls.liveSyncPosition || -1;
- }
- }
- return itemStart;
- }
- isAssetBuffered(asset) {
- const player = this.getAssetPlayer(asset.identifier);
- if (player != null && player.hls) {
- return player.hls.bufferedToEnd;
- }
- const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0);
- return bufferInfo.end + 1 >= asset.timelineStart + (asset.duration || 0);
- }
- attachPrimary(timelinePos, item, skipSeekToStartPosition) {
- if (item) {
- this.setBufferingItem(item);
- } else {
- this.bufferingItem = this.playingItem;
- }
- this.bufferingAsset = null;
- const media = this.primaryMedia;
- if (!media) {
- return;
- }
- const hls = this.hls;
- if (hls.media) {
- this.checkBuffer();
- } else {
- this.transferMediaTo(hls, media);
- if (skipSeekToStartPosition) {
- this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
- }
- }
- if (!skipSeekToStartPosition) {
- // Set primary position to resume time
- this.log(timelineMessage('attachPrimary', timelinePos));
- this.timelinePos = timelinePos;
- this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
- }
- }
- startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition) {
- var _hls$mainForwardBuffe;
- const hls = this.hls;
- if (!hls.loadingEnabled || !hls.media || Math.abs((((_hls$mainForwardBuffe = hls.mainForwardBufferInfo) == null ? void 0 : _hls$mainForwardBuffe.start) || hls.media.currentTime) - timelinePos) > 0.5) {
- hls.startLoad(timelinePos, skipSeekToStartPosition);
- } else if (!hls.bufferingEnabled) {
- hls.resumeBuffering();
- }
- }
-
- // HLS.js event callbacks
- onManifestLoading() {
- var _this$schedule5;
- this.stopLoad();
- (_this$schedule5 = this.schedule) == null || _this$schedule5.reset();
- this.emptyPlayerQueue();
- this.clearScheduleState();
- this.shouldPlay = false;
- this.bufferedPos = this.timelinePos = -1;
- this.mediaSelection = this.altSelection = this.manager = this.requiredTracks = null;
- // BUFFER_CODECS listener added here for buffer-controller to handle it first where it adds tracks
- this.hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
- this.hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
- }
- onLevelUpdated(event, data) {
- if (data.level === -1 || !this.schedule) {
- // level was removed
- return;
- }
- const main = this.hls.levels[data.level];
- if (!main.details) {
- return;
- }
- const currentSelection = _objectSpread2(_objectSpread2({}, this.mediaSelection || this.altSelection), {}, {
- main
- });
- this.mediaSelection = currentSelection;
- this.schedule.parseInterstitialDateRanges(currentSelection, this.hls.config.interstitialAppendInPlace);
- if (!this.effectivePlayingItem && this.schedule.items) {
- this.checkStart();
- }
- }
- onAudioTrackUpdated(event, data) {
- const audio = this.hls.audioTracks[data.id];
- const previousSelection = this.mediaSelection;
- if (!previousSelection) {
- this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, {
- audio
- });
- return;
- }
- const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, {
- audio
- });
- this.mediaSelection = currentSelection;
- }
- onSubtitleTrackUpdated(event, data) {
- const subtitles = this.hls.subtitleTracks[data.id];
- const previousSelection = this.mediaSelection;
- if (!previousSelection) {
- this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, {
- subtitles
- });
- return;
- }
- const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, {
- subtitles
- });
- this.mediaSelection = currentSelection;
- }
- onAudioTrackSwitching(event, data) {
- const audioOption = getBasicSelectionOption(data);
- this.playerQueue.forEach(({
- hls
- }) => hls && (hls.setAudioOption(data) || hls.setAudioOption(audioOption)));
- }
- onSubtitleTrackSwitch(event, data) {
- const subtitleOption = getBasicSelectionOption(data);
- this.playerQueue.forEach(({
- hls
- }) => hls && (hls.setSubtitleOption(data) || data.id !== -1 && hls.setSubtitleOption(subtitleOption)));
- }
- onBufferCodecs(event, data) {
- const requiredTracks = data.tracks;
- if (requiredTracks) {
- this.requiredTracks = requiredTracks;
- }
- }
- onBufferAppended(event, data) {
- this.checkBuffer();
- }
- onBufferFlushed(event, data) {
- const playingItem = this.playingItem;
- if (playingItem && !this.itemsMatch(playingItem, this.bufferingItem) && !this.isInterstitial(playingItem)) {
- const timelinePos = this.timelinePos;
- this.bufferedPos = timelinePos;
- this.checkBuffer();
- }
- }
- onBufferedToEnd(event) {
- if (!this.schedule) {
- return;
- }
- // Buffered to post-roll
- const interstitialEvents = this.schedule.events;
- if (this.bufferedPos < Number.MAX_VALUE && interstitialEvents) {
- for (let i = 0; i < interstitialEvents.length; i++) {
- const interstitial = interstitialEvents[i];
- if (interstitial.cue.post) {
- var _this$schedule$items2;
- const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier);
- const item = (_this$schedule$items2 = this.schedule.items) == null ? void 0 : _this$schedule$items2[scheduleIndex];
- if (this.isInterstitial(item) && this.eventItemsMatch(item, this.bufferingItem)) {
- this.bufferedToItem(item, 0);
- }
- break;
- }
- }
- this.bufferedPos = Number.MAX_VALUE;
- }
- }
- onMediaEnded(event) {
- const playingItem = this.playingItem;
- if (!this.playingLastItem && playingItem) {
- const playingIndex = this.findItemIndex(playingItem);
- this.setSchedulePosition(playingIndex + 1);
- } else {
- this.shouldPlay = false;
- }
- }
- updateItem(previousItem, time) {
- var _this$schedule6;
- // find item in this.schedule.items;
- const items = (_this$schedule6 = this.schedule) == null ? void 0 : _this$schedule6.items;
- if (previousItem && items) {
- const index = this.findItemIndex(previousItem, time);
- return items[index] || null;
- }
- return null;
- }
- trimInPlace(updatedItem, itemBeforeUpdate) {
- if (this.isInterstitial(updatedItem) && updatedItem.event.appendInPlace && itemBeforeUpdate.end - updatedItem.end > 0.25) {
- updatedItem.event.assetList.forEach((asset, index) => {
- if (updatedItem.event.isAssetPastPlayoutLimit(index)) {
- this.clearAssetPlayer(asset.identifier, null);
- }
- });
- const flushStart = updatedItem.end + 0.25;
- const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, flushStart, 0);
- if (bufferInfo.end > flushStart || (bufferInfo.nextStart || 0) > flushStart) {
- this.log(`trim buffered interstitial ${segmentToString(updatedItem)} (was ${segmentToString(itemBeforeUpdate)})`);
- const skipSeekToStartPosition = true;
- this.attachPrimary(flushStart, null, skipSeekToStartPosition);
- this.flushFrontBuffer(flushStart);
- }
- }
- }
- itemsMatch(a, b) {
- return !!b && (a === b || a.event && b.event && this.eventItemsMatch(a, b) || !a.event && !b.event && this.findItemIndex(a) === this.findItemIndex(b));
- }
- eventItemsMatch(a, b) {
- var _b$event;
- return !!b && (a === b || a.event.identifier === ((_b$event = b.event) == null ? void 0 : _b$event.identifier));
- }
- findItemIndex(item, time) {
- return item && this.schedule ? this.schedule.findItemIndex(item, time) : -1;
- }
- updateSchedule(forceUpdate = false) {
- var _this$schedule7;
- const mediaSelection = this.mediaSelection;
- if (!mediaSelection) {
- return;
- }
- (_this$schedule7 = this.schedule) == null || _this$schedule7.updateSchedule(mediaSelection, [], forceUpdate);
- }
-
- // Schedule buffer control
- checkBuffer(starved) {
- var _this$schedule8;
- const items = (_this$schedule8 = this.schedule) == null ? void 0 : _this$schedule8.items;
- if (!items) {
- return;
- }
- // Find when combined forward buffer change reaches next schedule segment
- const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0);
- if (starved) {
- this.bufferedPos = this.timelinePos;
- }
- starved || (starved = bufferInfo.len < 1);
- this.updateBufferedPos(bufferInfo.end, items, starved);
- }
- updateBufferedPos(bufferEnd, items, bufferIsEmpty) {
- const schedule = this.schedule;
- const bufferingItem = this.bufferingItem;
- if (this.bufferedPos > bufferEnd || !schedule) {
- return;
- }
- if (items.length === 1 && this.itemsMatch(items[0], bufferingItem)) {
- this.bufferedPos = bufferEnd;
- return;
- }
- const playingItem = this.playingItem;
- const playingIndex = this.findItemIndex(playingItem);
- let bufferEndIndex = schedule.findItemIndexAtTime(bufferEnd);
- if (this.bufferedPos < bufferEnd) {
- var _nextItemToBuffer$eve;
- const bufferingIndex = this.findItemIndex(bufferingItem);
- const nextToBufferIndex = Math.min(bufferingIndex + 1, items.length - 1);
- const nextItemToBuffer = items[nextToBufferIndex];
- if (bufferEndIndex === -1 && bufferingItem && bufferEnd >= bufferingItem.end || (_nextItemToBuffer$eve = nextItemToBuffer.event) != null && _nextItemToBuffer$eve.appendInPlace && bufferEnd + 0.01 >= nextItemToBuffer.start) {
- bufferEndIndex = nextToBufferIndex;
- }
- if (this.isInterstitial(bufferingItem)) {
- const interstitial = bufferingItem.event;
- if (nextToBufferIndex - playingIndex > 1 && interstitial.appendInPlace === false) {
- // do not advance buffering item past Interstitial that requires source reset
- return;
- }
- if (interstitial.assetList.length === 0 && interstitial.assetListLoader) {
- // do not advance buffering item past Interstitial loading asset-list
- return;
- }
- }
- this.bufferedPos = bufferEnd;
- if (bufferEndIndex > bufferingIndex && bufferEndIndex > playingIndex) {
- this.bufferedToItem(nextItemToBuffer);
- } else {
- // allow more time than distance from edge for assets to load
- const details = this.primaryDetails;
- if (this.primaryLive && details && bufferEnd > details.edge - details.targetduration && nextItemToBuffer.start < details.edge + this.hls.config.interstitialLiveLookAhead && this.isInterstitial(nextItemToBuffer)) {
- this.preloadAssets(nextItemToBuffer.event, 0);
- }
- }
- } else if (bufferIsEmpty && playingItem && !this.itemsMatch(playingItem, bufferingItem)) {
- if (bufferEndIndex === playingIndex) {
- this.bufferedToItem(playingItem);
- } else if (bufferEndIndex === playingIndex + 1) {
- this.bufferedToItem(items[bufferEndIndex]);
- }
- }
- }
- assetsBuffered(item, media) {
- const assetList = item.event.assetList;
- if (assetList.length === 0) {
- return false;
- }
- return !item.event.assetList.some(asset => {
- const player = this.getAssetPlayer(asset.identifier);
- return !(player != null && player.bufferedInPlaceToEnd(media));
- });
- }
- setBufferingItem(item) {
- const bufferingLast = this.bufferingItem;
- const schedule = this.schedule;
- if (!this.itemsMatch(item, bufferingLast) && schedule) {
- const {
- items,
- events
- } = schedule;
- if (!items || !events) {
- return bufferingLast;
- }
- const isInterstitial = this.isInterstitial(item);
- const bufferingPlayer = this.getBufferingPlayer();
- this.bufferingItem = item;
- this.bufferedPos = Math.max(item.start, Math.min(item.end, this.timelinePos));
- const timeRemaining = bufferingPlayer ? bufferingPlayer.remaining : bufferingLast ? bufferingLast.end - this.timelinePos : 0;
- this.log(`INTERSTITIALS_BUFFERED_TO_BOUNDARY ${segmentToString(item)}` + (bufferingLast ? ` (${timeRemaining.toFixed(2)} remaining)` : ''));
- if (!this.playbackDisabled) {
- if (isInterstitial) {
- const bufferIndex = schedule.findAssetIndex(item.event, this.bufferedPos);
- // primary fragment loading will exit early in base-stream-controller while `bufferingItem` is set to an Interstitial block
- item.event.assetList.forEach((asset, i) => {
- const player = this.getAssetPlayer(asset.identifier);
- if (player) {
- if (i === bufferIndex) {
- player.loadSource();
- }
- player.resumeBuffering();
- }
- });
- } else {
- this.hls.resumeBuffering();
- this.playerQueue.forEach(player => player.pauseBuffering());
- }
- }
- this.hls.trigger(Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, {
- events: events.slice(0),
- schedule: items.slice(0),
- bufferingIndex: this.findItemIndex(item),
- playingIndex: this.findItemIndex(this.playingItem)
- });
- } else if (this.bufferingItem !== item) {
- this.bufferingItem = item;
- }
- return bufferingLast;
- }
- bufferedToItem(item, assetListIndex = 0) {
- const bufferingLast = this.setBufferingItem(item);
- if (this.playbackDisabled) {
- return;
- }
- if (this.isInterstitial(item)) {
- // Ensure asset list is loaded
- this.bufferedToEvent(item, assetListIndex);
- } else if (bufferingLast !== null) {
- // If primary player is detached, it is also stopped, restart loading at primary position
- this.bufferingAsset = null;
- const detachedData = this.detachedData;
- if (detachedData) {
- if (detachedData.mediaSource) {
- const skipSeekToStartPosition = true;
- this.attachPrimary(item.start, item, skipSeekToStartPosition);
- } else {
- this.preloadPrimary(item);
- }
- } else {
- // If not detached seek to resumption point
- this.preloadPrimary(item);
- }
- }
- }
- preloadPrimary(item) {
- const index = this.findItemIndex(item);
- const timelinePos = this.getPrimaryResumption(item, index);
- this.startLoadingPrimaryAt(timelinePos);
- }
- bufferedToEvent(item, assetListIndex) {
- const interstitial = item.event;
- const neverLoaded = interstitial.assetList.length === 0 && !interstitial.assetListLoader;
- const playOnce = interstitial.cue.once;
- if (neverLoaded || !playOnce) {
- // Buffered to Interstitial boundary
- const player = this.preloadAssets(interstitial, assetListIndex);
- if (player != null && player.interstitial.appendInPlace) {
- const media = this.primaryMedia;
- if (media) {
- this.bufferAssetPlayer(player, media);
- }
- }
- }
- }
- preloadAssets(interstitial, assetListIndex) {
- const uri = interstitial.assetUrl;
- const assetListLength = interstitial.assetList.length;
- const neverLoaded = assetListLength === 0 && !interstitial.assetListLoader;
- const playOnce = interstitial.cue.once;
- if (neverLoaded) {
- const timelineStart = interstitial.timelineStart;
- if (interstitial.appendInPlace) {
- var _playingItem$nextEven;
- const playingItem = this.playingItem;
- if (!this.isInterstitial(playingItem) && (playingItem == null || (_playingItem$nextEven = playingItem.nextEvent) == null ? void 0 : _playingItem$nextEven.identifier) === interstitial.identifier) {
- this.flushFrontBuffer(timelineStart + 0.25);
- }
- }
- let hlsStartOffset;
- let liveStartPosition = 0;
- if (!this.playingItem && this.primaryLive) {
- liveStartPosition = this.hls.startPosition;
- if (liveStartPosition === -1) {
- liveStartPosition = this.hls.liveSyncPosition || 0;
- }
- }
- if (liveStartPosition && !(interstitial.cue.pre || interstitial.cue.post)) {
- const startOffset = liveStartPosition - timelineStart;
- if (startOffset > 0) {
- hlsStartOffset = Math.round(startOffset * 1000) / 1000;
- }
- }
- this.log(`Load interstitial asset ${assetListIndex + 1}/${uri ? 1 : assetListLength} ${interstitial}${hlsStartOffset ? ` live-start: ${liveStartPosition} start-offset: ${hlsStartOffset}` : ''}`);
- if (uri) {
- return this.createAsset(interstitial, 0, 0, timelineStart, interstitial.duration, uri);
- }
- const assetListLoader = this.assetListLoader.loadAssetList(interstitial, hlsStartOffset);
- if (assetListLoader) {
- interstitial.assetListLoader = assetListLoader;
- }
- } else if (!playOnce && assetListLength) {
- // Re-buffered to Interstitial boundary, re-create asset player(s)
- for (let i = assetListIndex; i < assetListLength; i++) {
- const _asset = interstitial.assetList[i];
- const playerIndex = this.getAssetPlayerQueueIndex(_asset.identifier);
- if ((playerIndex === -1 || this.playerQueue[playerIndex].destroyed) && !_asset.error) {
- this.createAssetPlayer(interstitial, _asset, i);
- }
- }
- const asset = interstitial.assetList[assetListIndex];
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (asset) {
- const player = this.getAssetPlayer(asset.identifier);
- if (player) {
- player.loadSource();
- }
- return player;
- }
- }
- return null;
- }
- flushFrontBuffer(startOffset) {
- // Force queued flushing of all buffers
- const requiredTracks = this.requiredTracks;
- if (!requiredTracks) {
- return;
- }
- this.log(`Removing front buffer starting at ${startOffset}`);
- const sourceBufferNames = Object.keys(requiredTracks);
- sourceBufferNames.forEach(type => {
- this.hls.trigger(Events.BUFFER_FLUSHING, {
- startOffset,
- endOffset: Infinity,
- type
- });
- });
- }
-
- // Interstitial Asset Player control
- getAssetPlayerQueueIndex(assetId) {
- const playerQueue = this.playerQueue;
- for (let i = 0; i < playerQueue.length; i++) {
- if (assetId === playerQueue[i].assetId) {
- return i;
- }
- }
- return -1;
- }
- getAssetPlayer(assetId) {
- const index = this.getAssetPlayerQueueIndex(assetId);
- return this.playerQueue[index] || null;
- }
- getBufferingPlayer() {
- const {
- playerQueue,
- primaryMedia
- } = this;
- if (primaryMedia) {
- for (let i = 0; i < playerQueue.length; i++) {
- if (playerQueue[i].media === primaryMedia) {
- return playerQueue[i];
- }
- }
- }
- return null;
- }
- createAsset(interstitial, assetListIndex, startOffset, timelineStart, duration, uri) {
- const assetItem = {
- parentIdentifier: interstitial.identifier,
- identifier: generateAssetIdentifier(interstitial, uri, assetListIndex),
- duration,
- startOffset,
- timelineStart,
- uri
- };
- return this.createAssetPlayer(interstitial, assetItem, assetListIndex);
- }
- createAssetPlayer(interstitial, assetItem, assetListIndex) {
- const primary = this.hls;
- const userConfig = primary.userConfig;
- let videoPreference = userConfig.videoPreference;
- const currentLevel = primary.loadLevelObj || primary.levels[primary.currentLevel];
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (videoPreference || currentLevel) {
- videoPreference = _extends({}, videoPreference);
- if (currentLevel.videoCodec) {
- videoPreference.videoCodec = currentLevel.videoCodec;
- }
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (currentLevel.videoRange) {
- videoPreference.allowedVideoRanges = [currentLevel.videoRange];
- }
- }
- const selectedAudio = primary.audioTracks[primary.audioTrack];
- const selectedSubtitle = primary.subtitleTracks[primary.subtitleTrack];
- let startPosition = 0;
- if (this.primaryLive || interstitial.appendInPlace) {
- const timePastStart = this.timelinePos - assetItem.timelineStart;
- if (timePastStart > 1) {
- const duration = assetItem.duration;
- if (duration && timePastStart < duration) {
- startPosition = timePastStart;
- }
- }
- }
- const assetId = assetItem.identifier;
- const playerConfig = _objectSpread2(_objectSpread2({}, userConfig), {}, {
- maxMaxBufferLength: Math.min(180, primary.config.maxMaxBufferLength),
- autoStartLoad: true,
- startFragPrefetch: true,
- primarySessionId: primary.sessionId,
- assetPlayerId: assetId,
- abrEwmaDefaultEstimate: primary.bandwidthEstimate,
- interstitialsController: undefined,
- startPosition,
- liveDurationInfinity: false,
- testBandwidth: false,
- videoPreference,
- audioPreference: selectedAudio || userConfig.audioPreference,
- subtitlePreference: selectedSubtitle || userConfig.subtitlePreference
- });
- // TODO: limit maxMaxBufferLength in asset players to prevent QEE
- if (interstitial.appendInPlace) {
- interstitial.appendInPlaceStarted = true;
- if (assetItem.timelineStart) {
- playerConfig.timelineOffset = assetItem.timelineStart;
- }
- }
- const cmcd = playerConfig.cmcd;
- if (cmcd != null && cmcd.sessionId && cmcd.contentId) {
- playerConfig.cmcd = _extends({}, cmcd, {
- contentId: hash(assetItem.uri)
- });
- }
- if (this.getAssetPlayer(assetId)) {
- this.warn(`Duplicate date range identifier ${interstitial} and asset ${assetId}`);
- }
- const player = new HlsAssetPlayer(this.HlsPlayerClass, playerConfig, interstitial, assetItem);
- this.playerQueue.push(player);
- interstitial.assetList[assetListIndex] = assetItem;
- // Listen for LevelDetails and PTS change to update duration
- let initialDuration = true;
- const updateAssetPlayerDetails = details => {
- if (details.live) {
- var _this$schedule9;
- const error = new Error(`Interstitials MUST be VOD assets ${interstitial}`);
- const errorData = {
- fatal: true,
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
- error
- };
- const scheduleIndex = ((_this$schedule9 = this.schedule) == null ? void 0 : _this$schedule9.findEventIndex(interstitial.identifier)) || -1;
- this.handleAssetItemError(errorData, interstitial, scheduleIndex, assetListIndex, error.message);
- return;
- }
- // Get time at end of last fragment
- const duration = details.edge - details.fragmentStart;
- const currentAssetDuration = assetItem.duration;
- if (initialDuration || currentAssetDuration === null || duration > currentAssetDuration) {
- initialDuration = false;
- this.log(`Interstitial asset "${assetId}" duration change ${currentAssetDuration} > ${duration}`);
- assetItem.duration = duration;
- // Update schedule with new event and asset duration
- this.updateSchedule();
- }
- };
- player.on(Events.LEVEL_UPDATED, (event, {
- details
- }) => updateAssetPlayerDetails(details));
- player.on(Events.LEVEL_PTS_UPDATED, (event, {
- details
- }) => updateAssetPlayerDetails(details));
- player.on(Events.EVENT_CUE_ENTER, () => this.onInterstitialCueEnter());
- const onBufferCodecs = (event, data) => {
- const inQueuPlayer = this.getAssetPlayer(assetId);
- if (inQueuPlayer && data.tracks) {
- inQueuPlayer.off(Events.BUFFER_CODECS, onBufferCodecs);
- inQueuPlayer.tracks = data.tracks;
- const media = this.primaryMedia;
- if (this.bufferingAsset === inQueuPlayer.assetItem && media && !inQueuPlayer.media) {
- this.bufferAssetPlayer(inQueuPlayer, media);
- }
- }
- };
- player.on(Events.BUFFER_CODECS, onBufferCodecs);
- const bufferedToEnd = () => {
- var _this$schedule$items3;
- const inQueuPlayer = this.getAssetPlayer(assetId);
- this.log(`buffered to end of asset ${inQueuPlayer}`);
- if (!inQueuPlayer || !this.schedule) {
- return;
- }
- // Preload at end of asset
- const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier);
- const item = (_this$schedule$items3 = this.schedule.items) == null ? void 0 : _this$schedule$items3[scheduleIndex];
- if (this.isInterstitial(item)) {
- this.advanceAssetBuffering(item, assetItem);
- }
- };
- player.on(Events.BUFFERED_TO_END, bufferedToEnd);
- const endedWithAssetIndex = assetIndex => {
- return () => {
- const inQueuPlayer = this.getAssetPlayer(assetId);
- if (!inQueuPlayer || !this.schedule) {
- return;
- }
- this.shouldPlay = true;
- const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier);
- this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetIndex);
- };
- };
- player.once(Events.MEDIA_ENDED, endedWithAssetIndex(assetListIndex));
- player.once(Events.PLAYOUT_LIMIT_REACHED, endedWithAssetIndex(Infinity));
- player.on(Events.ERROR, (event, data) => {
- if (!this.schedule) {
- return;
- }
- const inQueuPlayer = this.getAssetPlayer(assetId);
- if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) {
- if (inQueuPlayer != null && inQueuPlayer.appendInPlace) {
- this.handleInPlaceStall(interstitial);
- return;
- }
- this.onTimeupdate();
- this.checkBuffer(true);
- return;
- }
- this.handleAssetItemError(data, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, `Asset player error ${data.error} ${interstitial}`);
- });
- player.on(Events.DESTROYING, () => {
- const inQueuPlayer = this.getAssetPlayer(assetId);
- if (!inQueuPlayer || !this.schedule) {
- return;
- }
- const error = new Error(`Asset player destroyed unexpectedly ${assetId}`);
- const errorData = {
- fatal: true,
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
- error
- };
- this.handleAssetItemError(errorData, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, error.message);
- });
- this.log(`INTERSTITIAL_ASSET_PLAYER_CREATED ${eventAssetToString(assetItem)}`);
- this.hls.trigger(Events.INTERSTITIAL_ASSET_PLAYER_CREATED, {
- asset: assetItem,
- assetListIndex,
- event: interstitial,
- player
- });
- return player;
- }
- clearInterstitial(interstitial, toSegment) {
- this.clearAssetPlayers(interstitial, toSegment);
- // Remove asset list and resolved duration
- interstitial.reset();
- }
- clearAssetPlayers(interstitial, toSegment) {
- interstitial.assetList.forEach(asset => {
- this.clearAssetPlayer(asset.identifier, toSegment);
- });
- }
- resetAssetPlayer(assetId) {
- // Reset asset player so that it's timeline can be adjusted without reloading the MVP
- const playerIndex = this.getAssetPlayerQueueIndex(assetId);
- if (playerIndex !== -1) {
- this.log(`reset asset player "${assetId}" after error`);
- const player = this.playerQueue[playerIndex];
- this.transferMediaFromPlayer(player, null);
- player.resetDetails();
- }
- }
- clearAssetPlayer(assetId, toSegment) {
- const playerIndex = this.getAssetPlayerQueueIndex(assetId);
- if (playerIndex !== -1) {
- const player = this.playerQueue[playerIndex];
- this.log(`clear ${player} toSegment: ${toSegment ? segmentToString(toSegment) : toSegment}`);
- this.transferMediaFromPlayer(player, toSegment);
- this.playerQueue.splice(playerIndex, 1);
- player.destroy();
- }
- }
- emptyPlayerQueue() {
- let player;
- while (player = this.playerQueue.pop()) {
- player.destroy();
- }
- this.playerQueue = [];
- }
- startAssetPlayer(player, assetListIndex, scheduleItems, scheduleIndex, media) {
- const {
- interstitial,
- assetItem,
- assetId
- } = player;
- const assetListLength = interstitial.assetList.length;
- const playingAsset = this.playingAsset;
- this.endedAsset = null;
- this.playingAsset = assetItem;
- if (!playingAsset || playingAsset.identifier !== assetId) {
- if (playingAsset) {
- // Exiting another Interstitial asset
- this.clearAssetPlayer(playingAsset.identifier, scheduleItems[scheduleIndex]);
- delete playingAsset.error;
- }
- this.log(`INTERSTITIAL_ASSET_STARTED ${assetListIndex + 1}/${assetListLength} ${eventAssetToString(assetItem)}`);
- this.hls.trigger(Events.INTERSTITIAL_ASSET_STARTED, {
- asset: assetItem,
- assetListIndex,
- event: interstitial,
- schedule: scheduleItems.slice(0),
- scheduleIndex,
- player
- });
- }
-
- // detach media and attach to interstitial player if it does not have another element attached
- this.bufferAssetPlayer(player, media);
- }
- bufferAssetPlayer(player, media) {
- var _this$schedule$items4, _this$detachedData5;
- if (!this.schedule) {
- return;
- }
- const {
- interstitial,
- assetItem
- } = player;
- const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier);
- const item = (_this$schedule$items4 = this.schedule.items) == null ? void 0 : _this$schedule$items4[scheduleIndex];
- if (!item) {
- return;
- }
- player.loadSource();
- this.setBufferingItem(item);
- this.bufferingAsset = assetItem;
- const bufferingPlayer = this.getBufferingPlayer();
- if (bufferingPlayer === player) {
- return;
- }
- const appendInPlaceNext = interstitial.appendInPlace;
- if (appendInPlaceNext && (bufferingPlayer == null ? void 0 : bufferingPlayer.interstitial.appendInPlace) === false) {
- // Media is detached and not available to append in place
- return;
- }
- const activeTracks = (bufferingPlayer == null ? void 0 : bufferingPlayer.tracks) || ((_this$detachedData5 = this.detachedData) == null ? void 0 : _this$detachedData5.tracks) || this.requiredTracks;
- if (appendInPlaceNext && assetItem !== this.playingAsset) {
- // Do not buffer another item if tracks are unknown or incompatible
- if (!player.tracks) {
- this.log(`Waiting for track info before buffering ${player}`);
- return;
- }
- if (activeTracks && !isCompatibleTrackChange(activeTracks, player.tracks)) {
- const error = new Error(`Asset ${eventAssetToString(assetItem)} SourceBuffer tracks ('${Object.keys(player.tracks)}') are not compatible with primary content tracks ('${Object.keys(activeTracks)}')`);
- const errorData = {
- fatal: true,
- type: ErrorTypes.OTHER_ERROR,
- details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR,
- error
- };
- const assetListIndex = interstitial.findAssetIndex(assetItem);
- this.handleAssetItemError(errorData, interstitial, scheduleIndex, assetListIndex, error.message);
- return;
- }
- }
- this.transferMediaTo(player, media);
- }
- handleInPlaceStall(interstitial) {
- const schedule = this.schedule;
- const media = this.primaryMedia;
- if (!schedule || !media) {
- return;
- }
- const currentTime = media.currentTime;
- const foundAssetIndex = schedule.findAssetIndex(interstitial, currentTime);
- const stallingAsset = interstitial.assetList[foundAssetIndex];
- if (stallingAsset) {
- const player = this.getAssetPlayer(stallingAsset.identifier);
- if (player) {
- const assetCurrentTime = player.currentTime || currentTime - stallingAsset.timelineStart;
- const distanceFromEnd = player.duration - assetCurrentTime;
- this.warn(`Stalled at ${assetCurrentTime} of ${assetCurrentTime + distanceFromEnd} in ${player} ${interstitial} (media.currentTime: ${currentTime})`);
- if (assetCurrentTime && (distanceFromEnd / media.playbackRate < 0.5 || player.bufferedInPlaceToEnd(media)) && player.hls) {
- const scheduleIndex = schedule.findEventIndex(interstitial.identifier);
- this.advanceAfterAssetEnded(interstitial, scheduleIndex, foundAssetIndex);
- }
- }
- }
- }
- advanceInPlace(time) {
- const media = this.primaryMedia;
- if (media && media.currentTime < time) {
- media.currentTime = time;
- }
- }
- handleAssetItemError(data, interstitial, scheduleIndex, assetListIndex, errorMessage) {
- if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) {
- return;
- }
- const assetItem = interstitial.assetList[assetListIndex] || null;
- this.warn(`INTERSTITIAL_ASSET_ERROR ${assetItem ? eventAssetToString(assetItem) : assetItem} ${data.error}`);
- if (!this.schedule) {
- return;
- }
- const assetId = (assetItem == null ? void 0 : assetItem.identifier) || '';
- const playerIndex = this.getAssetPlayerQueueIndex(assetId);
- const player = this.playerQueue[playerIndex] || null;
- const items = this.schedule.items;
- const interstitialAssetError = _extends({}, data, {
- fatal: false,
- errorAction: createDoNothingErrorAction(true),
- asset: assetItem,
- assetListIndex,
- event: interstitial,
- schedule: items,
- scheduleIndex,
- player
- });
- this.hls.trigger(Events.INTERSTITIAL_ASSET_ERROR, interstitialAssetError);
- if (!data.fatal) {
- return;
- }
- const playingAsset = this.playingAsset;
- const bufferingAsset = this.bufferingAsset;
- const error = new Error(errorMessage);
- if (assetItem) {
- this.clearAssetPlayer(assetId, null);
- assetItem.error = error;
- }
-
- // If all assets in interstitial fail, mark the interstitial with an error
- if (!interstitial.assetList.some(asset => !asset.error)) {
- interstitial.error = error;
- } else {
- // Reset level details and reload/parse media playlists to align with updated schedule
- for (let i = assetListIndex; i < interstitial.assetList.length; i++) {
- this.resetAssetPlayer(interstitial.assetList[i].identifier);
- }
- }
- this.updateSchedule(true);
- if (interstitial.error) {
- this.primaryFallback(interstitial);
- } else if (playingAsset && playingAsset.identifier === assetId) {
- this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetListIndex);
- } else if (bufferingAsset && bufferingAsset.identifier === assetId && this.isInterstitial(this.bufferingItem)) {
- this.advanceAssetBuffering(this.bufferingItem, bufferingAsset);
- }
- }
- primaryFallback(interstitial) {
- // Fallback to Primary by on current or future events by updating schedule to skip errored interstitials/assets
- const flushStart = interstitial.timelineStart;
- const playingItem = this.effectivePlayingItem;
- let timelinePos = this.timelinePos;
- // Update schedule now that interstitial/assets are flagged with `error` for fallback
- if (playingItem) {
- this.log(`Fallback to primary from event "${interstitial.identifier}" start: ${flushStart} pos: ${timelinePos} playing: ${segmentToString(playingItem)} error: ${interstitial.error}`);
- if (timelinePos === -1) {
- timelinePos = this.hls.startPosition;
- }
- const newPlayingItem = this.updateItem(playingItem, timelinePos);
- if (this.itemsMatch(playingItem, newPlayingItem)) {
- this.clearInterstitial(interstitial, null);
- }
- if (interstitial.appendInPlace) {
- this.attachPrimary(flushStart, null);
- this.flushFrontBuffer(flushStart);
- }
- } else if (timelinePos === -1) {
- this.checkStart();
- return;
- }
- if (!this.schedule) {
- return;
- }
- const scheduleIndex = this.schedule.findItemIndexAtTime(timelinePos);
- this.setSchedulePosition(scheduleIndex);
- }
-
- // Asset List loading
- onAssetListLoaded(event, data) {
- var _this$schedule0, _this$bufferingItem2;
- const interstitial = data.event;
- const interstitialId = interstitial.identifier;
- const assets = data.assetListResponse.ASSETS;
- if (!((_this$schedule0 = this.schedule) != null && _this$schedule0.hasEvent(interstitialId))) {
- // Interstitial with id was removed
- return;
- }
- const eventStart = interstitial.timelineStart;
- const previousDuration = interstitial.duration;
- let sumDuration = 0;
- assets.forEach((asset, assetListIndex) => {
- const duration = parseFloat(asset.DURATION);
- this.createAsset(interstitial, assetListIndex, sumDuration, eventStart + sumDuration, duration, asset.URI);
- sumDuration += duration;
- });
- interstitial.duration = sumDuration;
- this.log(`Loaded asset-list with duration: ${sumDuration} (was: ${previousDuration}) ${interstitial}`);
- const waitingItem = this.waitingItem;
- const waitingForItem = (waitingItem == null ? void 0 : waitingItem.event.identifier) === interstitialId;
-
- // Update schedule now that asset.DURATION(s) are parsed
- this.updateSchedule();
- const bufferingEvent = (_this$bufferingItem2 = this.bufferingItem) == null ? void 0 : _this$bufferingItem2.event;
-
- // If buffer reached Interstitial, start buffering first asset
- if (waitingForItem) {
- var _this$schedule$items5;
- // Advance schedule when waiting for asset list data to play
- const scheduleIndex = this.schedule.findEventIndex(interstitialId);
- const item = (_this$schedule$items5 = this.schedule.items) == null ? void 0 : _this$schedule$items5[scheduleIndex];
- if (item) {
- if (!this.playingItem && this.timelinePos > item.end) {
- // Abandon if new duration is reduced enough to land playback in primary start
- const index = this.schedule.findItemIndexAtTime(this.timelinePos);
- if (index !== scheduleIndex) {
- interstitial.error = new Error(`Interstitial ${assets.length ? 'no longer within playback range' : 'asset-list is empty'} ${this.timelinePos} ${interstitial}`);
- this.log(interstitial.error.message);
- this.updateSchedule(true);
- this.primaryFallback(interstitial);
- return;
- }
- }
- this.setBufferingItem(item);
- }
- this.setSchedulePosition(scheduleIndex);
- } else if ((bufferingEvent == null ? void 0 : bufferingEvent.identifier) === interstitialId) {
- const assetItem = interstitial.assetList[0];
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (assetItem) {
- const player = this.getAssetPlayer(assetItem.identifier);
- if (bufferingEvent.appendInPlace) {
- // If buffering (but not playback) has reached this item transfer media-source
- const media = this.primaryMedia;
- if (player && media) {
- this.bufferAssetPlayer(player, media);
- }
- } else if (player) {
- player.loadSource();
- }
- }
- }
- }
- onError(event, data) {
- if (!this.schedule) {
- return;
- }
- switch (data.details) {
- case ErrorDetails.ASSET_LIST_PARSING_ERROR:
- case ErrorDetails.ASSET_LIST_LOAD_ERROR:
- case ErrorDetails.ASSET_LIST_LOAD_TIMEOUT:
- {
- const interstitial = data.interstitial;
- if (interstitial) {
- this.updateSchedule(true);
- this.primaryFallback(interstitial);
- }
- break;
- }
- case ErrorDetails.BUFFER_STALLED_ERROR:
- {
- const stallingItem = this.endedItem || this.waitingItem || this.playingItem;
- if (this.isInterstitial(stallingItem) && stallingItem.event.appendInPlace) {
- this.handleInPlaceStall(stallingItem.event);
- return;
- }
- this.log(`Primary player stall @${this.timelinePos} bufferedPos: ${this.bufferedPos}`);
- this.onTimeupdate();
- this.checkBuffer(true);
- break;
- }
- }
- }
-}
-
-const TICK_INTERVAL$2 = 500; // how often to tick in ms
-
-class SubtitleStreamController extends BaseStreamController {
- constructor(hls, fragmentTracker, keyLoader) {
- super(hls, fragmentTracker, keyLoader, 'subtitle-stream-controller', PlaylistLevelType.SUBTITLE);
- this.currentTrackId = -1;
- this.tracksBuffered = [];
- this.mainDetails = null;
- this.registerListeners();
- }
- onHandlerDestroying() {
- this.unregisterListeners();
- super.onHandlerDestroying();
- this.mainDetails = null;
- }
- registerListeners() {
- super.registerListeners();
- const {
- hls
- } = this;
- hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
- hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
- hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
- hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
- hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
- hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
- }
- unregisterListeners() {
- super.unregisterListeners();
- const {
- hls
- } = this;
- hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
- hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
- hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
- hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
- hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
- hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
- }
- startLoad(startPosition, skipSeekToStartPosition) {
- this.stopLoad();
- this.state = State.IDLE;
- this.setInterval(TICK_INTERVAL$2);
- this.nextLoadPosition = this.lastCurrentTime = startPosition + this.timelineOffset;
- this.startPosition = skipSeekToStartPosition ? -1 : startPosition;
- this.tick();
- }
- onManifestLoading() {
- super.onManifestLoading();
- this.mainDetails = null;
- }
- onMediaDetaching(event, data) {
- this.tracksBuffered = [];
- super.onMediaDetaching(event, data);
- }
- onLevelLoaded(event, data) {
- this.mainDetails = data.details;
- }
- onSubtitleFragProcessed(event, data) {
- const {
- frag,
- success
- } = data;
- if (!this.fragContextChanged(frag)) {
- if (isMediaFragment(frag)) {
- this.fragPrevious = frag;
- }
- this.state = State.IDLE;
- }
- if (!success) {
- return;
- }
- const buffered = this.tracksBuffered[this.currentTrackId];
- if (!buffered) {
- return;
- }
-
- // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo
- // so we can re-use the logic used to detect how much has been buffered
- let timeRange;
- const fragStart = frag.start;
- for (let i = 0; i < buffered.length; i++) {
- if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) {
- timeRange = buffered[i];
- break;
- }
- }
- const fragEnd = frag.start + frag.duration;
- if (timeRange) {
- timeRange.end = fragEnd;
- } else {
- timeRange = {
- start: fragStart,
- end: fragEnd
- };
- buffered.push(timeRange);
- }
- this.fragmentTracker.fragBuffered(frag);
- this.fragBufferedComplete(frag, null);
- if (this.media) {
- this.tick();
- }
- }
- onBufferFlushing(event, data) {
- const {
- startOffset,
- endOffset
- } = data;
- if (startOffset === 0 && endOffset !== Number.POSITIVE_INFINITY) {
- const endOffsetSubtitles = endOffset - 1;
- if (endOffsetSubtitles <= 0) {
- return;
- }
- data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles);
- this.tracksBuffered.forEach(buffered => {
- for (let i = 0; i < buffered.length;) {
- if (buffered[i].end <= endOffsetSubtitles) {
- buffered.shift();
- continue;
- } else if (buffered[i].start < endOffsetSubtitles) {
- buffered[i].start = endOffsetSubtitles;
- } else {
- break;
- }
- i++;
- }
- });
- this.fragmentTracker.removeFragmentsInRange(startOffset, endOffsetSubtitles, PlaylistLevelType.SUBTITLE);
- }
- }
-
- // If something goes wrong, proceed to next frag, if we were processing one.
- onError(event, data) {
- const frag = data.frag;
- if ((frag == null ? void 0 : frag.type) === PlaylistLevelType.SUBTITLE) {
- if (data.details === ErrorDetails.FRAG_GAP) {
- this.fragmentTracker.fragBuffered(frag, true);
- }
- if (this.fragCurrent) {
- this.fragCurrent.abortRequests();
- }
- if (this.state !== State.STOPPED) {
- this.state = State.IDLE;
- }
- }
- }
-
- // Got all new subtitle levels.
- onSubtitleTracksUpdated(event, {
- subtitleTracks
- }) {
- if (this.levels && subtitleOptionsIdentical(this.levels, subtitleTracks)) {
- this.levels = subtitleTracks.map(mediaPlaylist => new Level(mediaPlaylist));
- return;
- }
- this.tracksBuffered = [];
- this.levels = subtitleTracks.map(mediaPlaylist => {
- const level = new Level(mediaPlaylist);
- this.tracksBuffered[level.id] = [];
- return level;
- });
- this.fragmentTracker.removeFragmentsInRange(0, Number.POSITIVE_INFINITY, PlaylistLevelType.SUBTITLE);
- this.fragPrevious = null;
- this.mediaBuffer = null;
- }
- onSubtitleTrackSwitch(event, data) {
- var _this$levels;
- this.currentTrackId = data.id;
- if (!((_this$levels = this.levels) != null && _this$levels.length) || this.currentTrackId === -1) {
- this.clearInterval();
- return;
- }
-
- // Check if track has the necessary details to load fragments
- const currentTrack = this.levels[this.currentTrackId];
- if (currentTrack != null && currentTrack.details) {
- this.mediaBuffer = this.mediaBufferTimeRanges;
- } else {
- this.mediaBuffer = null;
- }
- if (currentTrack && this.state !== State.STOPPED) {
- this.setInterval(TICK_INTERVAL$2);
- }
- }
-
- // Got a new set of subtitle fragments.
- onSubtitleTrackLoaded(event, data) {
- var _track$details;
- const {
- currentTrackId,
- levels
- } = this;
- const {
- details: newDetails,
- id: trackId
- } = data;
- if (!levels) {
- this.warn(`Subtitle tracks were reset while loading level ${trackId}`);
- return;
- }
- const track = levels[trackId];
- if (trackId >= levels.length || !track) {
- return;
- }
- this.log(`Subtitle track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`);
- this.mediaBuffer = this.mediaBufferTimeRanges;
- let sliding = 0;
- if (newDetails.live || (_track$details = track.details) != null && _track$details.live) {
- if (newDetails.deltaUpdateFailed) {
- return;
- }
- const mainDetails = this.mainDetails;
- if (!mainDetails) {
- this.startFragRequested = false;
- return;
- }
- const mainSlidingStartFragment = mainDetails.fragments[0];
- if (!track.details) {
- if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) {
- alignMediaPlaylistByPDT(newDetails, mainDetails);
- sliding = newDetails.fragmentStart;
- } else if (mainSlidingStartFragment) {
- // line up live playlist with main so that fragments in range are loaded
- sliding = mainSlidingStartFragment.start;
- addSliding(newDetails, sliding);
- }
- } else {
- var _this$levelLastLoaded;
- sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details);
- if (sliding === 0 && mainSlidingStartFragment) {
- // realign with main when there is no overlap with last refresh
- sliding = mainSlidingStartFragment.start;
- addSliding(newDetails, sliding);
- }
- }
- // compute start position if we are aligned with the main playlist
- if (mainDetails && !this.startFragRequested) {
- this.setStartPosition(mainDetails, sliding);
- }
- }
- track.details = newDetails;
- this.levelLastLoaded = track;
- if (trackId !== currentTrackId) {
- return;
- }
- this.hls.trigger(Events.SUBTITLE_TRACK_UPDATED, {
- details: newDetails,
- id: trackId,
- groupId: data.groupId
- });
-
- // trigger handler right now
- this.tick();
-
- // If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload
- if (newDetails.live && !this.fragCurrent && this.media && this.state === State.IDLE) {
- const foundFrag = findFragmentByPTS(null, newDetails.fragments, this.media.currentTime, 0);
- if (!foundFrag) {
- this.warn('Subtitle playlist not aligned with playback');
- track.details = undefined;
- }
- }
- }
- _handleFragmentLoadComplete(fragLoadedData) {
- const {
- frag,
- payload
- } = fragLoadedData;
- const decryptData = frag.decryptdata;
- const hls = this.hls;
- if (this.fragContextChanged(frag)) {
- return;
- }
- // check to see if the payload needs to be decrypted
- if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) {
- const startTime = performance.now();
- // decrypt the subtitles
- this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => {
- hls.trigger(Events.ERROR, {
- type: ErrorTypes.MEDIA_ERROR,
- details: ErrorDetails.FRAG_DECRYPT_ERROR,
- fatal: false,
- error: err,
- reason: err.message,
- frag
- });
- throw err;
- }).then(decryptedData => {
- const endTime = performance.now();
- hls.trigger(Events.FRAG_DECRYPTED, {
- frag,
- payload: decryptedData,
- stats: {
- tstart: startTime,
- tdecrypt: endTime
- }
- });
- }).catch(err => {
- this.warn(`${err.name}: ${err.message}`);
- this.state = State.IDLE;
- });
- }
- }
- doTick() {
- if (!this.media) {
- this.state = State.IDLE;
- return;
- }
- if (this.state === State.IDLE) {
- const {
- currentTrackId,
- levels
- } = this;
- const track = levels == null ? void 0 : levels[currentTrackId];
- if (!track || !levels.length || !track.details) {
- return;
- }
- if (this.waitForLive(track)) {
- return;
- }
- const {
- config
- } = this;
- const currentTime = this.getLoadPosition();
- const bufferedInfo = BufferHelper.bufferedInfo(this.tracksBuffered[this.currentTrackId] || [], currentTime, config.maxBufferHole);
- const {
- end: targetBufferTime,
- len: bufferLen
- } = bufferedInfo;
- const trackDetails = track.details;
- const maxBufLen = this.hls.maxBufferLength + trackDetails.levelTargetDuration;
- if (bufferLen > maxBufLen) {
- return;
- }
- const fragments = trackDetails.fragments;
- const fragLen = fragments.length;
- const end = trackDetails.edge;
- let foundFrag = null;
- const fragPrevious = this.fragPrevious;
- if (targetBufferTime < end) {
- const tolerance = config.maxFragLookUpTolerance;
- const lookupTolerance = targetBufferTime > end - tolerance ? 0 : tolerance;
- foundFrag = findFragmentByPTS(fragPrevious, fragments, Math.max(fragments[0].start, targetBufferTime), lookupTolerance);
- if (!foundFrag && fragPrevious && fragPrevious.start < fragments[0].start) {
- foundFrag = fragments[0];
- }
- } else {
- foundFrag = fragments[fragLen - 1];
- }
- foundFrag = this.filterReplacedPrimary(foundFrag, track.details);
- if (!foundFrag) {
- return;
- }
- // Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment
- const curSNIdx = foundFrag.sn - trackDetails.startSN;
- const prevFrag = fragments[curSNIdx - 1];
- if (prevFrag && prevFrag.cc === foundFrag.cc && this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED) {
- foundFrag = prevFrag;
- }
- if (this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) {
- // only load if fragment is not loaded
- const fragToLoad = this.mapToInitFragWhenRequired(foundFrag);
- if (fragToLoad) {
- this.loadFragment(fragToLoad, track, targetBufferTime);
- }
- }
- }
- }
- loadFragment(frag, level, targetBufferTime) {
- if (!isMediaFragment(frag)) {
- this._loadInitSegment(frag, level);
- } else {
- super.loadFragment(frag, level, targetBufferTime);
- }
- }
- get mediaBufferTimeRanges() {
- return new BufferableInstance(this.tracksBuffered[this.currentTrackId] || []);
- }
-}
-class BufferableInstance {
- constructor(timeranges) {
- this.buffered = void 0;
- const getRange = (name, index, length) => {
- index = index >>> 0;
- if (index > length - 1) {
- throw new DOMException(`Failed to execute '${name}' on 'TimeRanges': The index provided (${index}) is greater than the maximum bound (${length})`);
- }
- return timeranges[index][name];
- };
- this.buffered = {
- get length() {
- return timeranges.length;
- },
- end(index) {
- return getRange('end', index, timeranges.length);
- },
- start(index) {
- return getRange('start', index, timeranges.length);
- }
- };
- }
-}
-
-/**
- *
- * This code was ported from the dash.js project at:
- * https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js
- * https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2
- *
- * The original copyright appears below:
- *
- * The copyright in this software is being made available under the BSD License,
- * included below. This software may be subject to other third party and contributor
- * rights, including patent rights, and no such rights are granted under this license.
- *
- * Copyright (c) 2015-2016, DASH Industry Forum.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- * 2. Neither the name of Dash Industry Forum nor the names of its
- * contributors may be used to endorse or promote products derived from this software
- * without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
- * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-/**
- * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes
- */
-
-const specialCea608CharsCodes = {
- 0x2a: 0xe1,
- // lowercase a, acute accent
- 0x5c: 0xe9,
- // lowercase e, acute accent
- 0x5e: 0xed,
- // lowercase i, acute accent
- 0x5f: 0xf3,
- // lowercase o, acute accent
- 0x60: 0xfa,
- // lowercase u, acute accent
- 0x7b: 0xe7,
- // lowercase c with cedilla
- 0x7c: 0xf7,
- // division symbol
- 0x7d: 0xd1,
- // uppercase N tilde
- 0x7e: 0xf1,
- // lowercase n tilde
- 0x7f: 0x2588,
- // Full block
- // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
- // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F
- // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES
- 0x80: 0xae,
- // Registered symbol (R)
- 0x81: 0xb0,
- // degree sign
- 0x82: 0xbd,
- // 1/2 symbol
- 0x83: 0xbf,
- // Inverted (open) question mark
- 0x84: 0x2122,
- // Trademark symbol (TM)
- 0x85: 0xa2,
- // Cents symbol
- 0x86: 0xa3,
- // Pounds sterling
- 0x87: 0x266a,
- // Music 8'th note
- 0x88: 0xe0,
- // lowercase a, grave accent
- 0x89: 0x20,
- // transparent space (regular)
- 0x8a: 0xe8,
- // lowercase e, grave accent
- 0x8b: 0xe2,
- // lowercase a, circumflex accent
- 0x8c: 0xea,
- // lowercase e, circumflex accent
- 0x8d: 0xee,
- // lowercase i, circumflex accent
- 0x8e: 0xf4,
- // lowercase o, circumflex accent
- 0x8f: 0xfb,
- // lowercase u, circumflex accent
- // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
- // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F
- 0x90: 0xc1,
- // capital letter A with acute
- 0x91: 0xc9,
- // capital letter E with acute
- 0x92: 0xd3,
- // capital letter O with acute
- 0x93: 0xda,
- // capital letter U with acute
- 0x94: 0xdc,
- // capital letter U with diaresis
- 0x95: 0xfc,
- // lowercase letter U with diaeresis
- 0x96: 0x2018,
- // opening single quote
- 0x97: 0xa1,
- // inverted exclamation mark
- 0x98: 0x2a,
- // asterisk
- 0x99: 0x2019,
- // closing single quote
- 0x9a: 0x2501,
- // box drawings heavy horizontal
- 0x9b: 0xa9,
- // copyright sign
- 0x9c: 0x2120,
- // Service mark
- 0x9d: 0x2022,
- // (round) bullet
- 0x9e: 0x201c,
- // Left double quotation mark
- 0x9f: 0x201d,
- // Right double quotation mark
- 0xa0: 0xc0,
- // uppercase A, grave accent
- 0xa1: 0xc2,
- // uppercase A, circumflex
- 0xa2: 0xc7,
- // uppercase C with cedilla
- 0xa3: 0xc8,
- // uppercase E, grave accent
- 0xa4: 0xca,
- // uppercase E, circumflex
- 0xa5: 0xcb,
- // capital letter E with diaresis
- 0xa6: 0xeb,
- // lowercase letter e with diaresis
- 0xa7: 0xce,
- // uppercase I, circumflex
- 0xa8: 0xcf,
- // uppercase I, with diaresis
- 0xa9: 0xef,
- // lowercase i, with diaresis
- 0xaa: 0xd4,
- // uppercase O, circumflex
- 0xab: 0xd9,
- // uppercase U, grave accent
- 0xac: 0xf9,
- // lowercase u, grave accent
- 0xad: 0xdb,
- // uppercase U, circumflex
- 0xae: 0xab,
- // left-pointing double angle quotation mark
- 0xaf: 0xbb,
- // right-pointing double angle quotation mark
- // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
- // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F
- 0xb0: 0xc3,
- // Uppercase A, tilde
- 0xb1: 0xe3,
- // Lowercase a, tilde
- 0xb2: 0xcd,
- // Uppercase I, acute accent
- 0xb3: 0xcc,
- // Uppercase I, grave accent
- 0xb4: 0xec,
- // Lowercase i, grave accent
- 0xb5: 0xd2,
- // Uppercase O, grave accent
- 0xb6: 0xf2,
- // Lowercase o, grave accent
- 0xb7: 0xd5,
- // Uppercase O, tilde
- 0xb8: 0xf5,
- // Lowercase o, tilde
- 0xb9: 0x7b,
- // Open curly brace
- 0xba: 0x7d,
- // Closing curly brace
- 0xbb: 0x5c,
- // Backslash
- 0xbc: 0x5e,
- // Caret
- 0xbd: 0x5f,
- // Underscore
- 0xbe: 0x7c,
- // Pipe (vertical line)
- 0xbf: 0x223c,
- // Tilde operator
- 0xc0: 0xc4,
- // Uppercase A, umlaut
- 0xc1: 0xe4,
- // Lowercase A, umlaut
- 0xc2: 0xd6,
- // Uppercase O, umlaut
- 0xc3: 0xf6,
- // Lowercase o, umlaut
- 0xc4: 0xdf,
- // Esszett (sharp S)
- 0xc5: 0xa5,
- // Yen symbol
- 0xc6: 0xa4,
- // Generic currency sign
- 0xc7: 0x2503,
- // Box drawings heavy vertical
- 0xc8: 0xc5,
- // Uppercase A, ring
- 0xc9: 0xe5,
- // Lowercase A, ring
- 0xca: 0xd8,
- // Uppercase O, stroke
- 0xcb: 0xf8,
- // Lowercase o, strok
- 0xcc: 0x250f,
- // Box drawings heavy down and right
- 0xcd: 0x2513,
- // Box drawings heavy down and left
- 0xce: 0x2517,
- // Box drawings heavy up and right
- 0xcf: 0x251b // Box drawings heavy up and left
-};
-
-/**
- * Utils
- */
-const getCharForByte = byte => String.fromCharCode(specialCea608CharsCodes[byte] || byte);
-const NR_ROWS = 15;
-const NR_COLS = 100;
-// Tables to look up row from PAC data
-const rowsLowCh1 = {
- 0x11: 1,
- 0x12: 3,
- 0x15: 5,
- 0x16: 7,
- 0x17: 9,
- 0x10: 11,
- 0x13: 12,
- 0x14: 14
-};
-const rowsHighCh1 = {
- 0x11: 2,
- 0x12: 4,
- 0x15: 6,
- 0x16: 8,
- 0x17: 10,
- 0x13: 13,
- 0x14: 15
-};
-const rowsLowCh2 = {
- 0x19: 1,
- 0x1a: 3,
- 0x1d: 5,
- 0x1e: 7,
- 0x1f: 9,
- 0x18: 11,
- 0x1b: 12,
- 0x1c: 14
-};
-const rowsHighCh2 = {
- 0x19: 2,
- 0x1a: 4,
- 0x1d: 6,
- 0x1e: 8,
- 0x1f: 10,
- 0x1b: 13,
- 0x1c: 15
-};
-const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent'];
-class CaptionsLogger {
- constructor() {
- this.time = null;
- this.verboseLevel = 0;
- }
- log(severity, msg) {
- if (this.verboseLevel >= severity) {
- const m = typeof msg === 'function' ? msg() : msg;
- logger.log(`${this.time} [${severity}] ${m}`);
- }
- }
-}
-const numArrayToHexArray = function numArrayToHexArray(numArray) {
- const hexArray = [];
- for (let j = 0; j < numArray.length; j++) {
- hexArray.push(numArray[j].toString(16));
- }
- return hexArray;
-};
-class PenState {
- constructor() {
- this.foreground = 'white';
- this.underline = false;
- this.italics = false;
- this.background = 'black';
- this.flash = false;
- }
- reset() {
- this.foreground = 'white';
- this.underline = false;
- this.italics = false;
- this.background = 'black';
- this.flash = false;
- }
- setStyles(styles) {
- const attribs = ['foreground', 'underline', 'italics', 'background', 'flash'];
- for (let i = 0; i < attribs.length; i++) {
- const style = attribs[i];
- if (styles.hasOwnProperty(style)) {
- this[style] = styles[style];
- }
- }
- }
- isDefault() {
- return this.foreground === 'white' && !this.underline && !this.italics && this.background === 'black' && !this.flash;
- }
- equals(other) {
- return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash;
- }
- copy(newPenState) {
- this.foreground = newPenState.foreground;
- this.underline = newPenState.underline;
- this.italics = newPenState.italics;
- this.background = newPenState.background;
- this.flash = newPenState.flash;
- }
- toString() {
- return 'color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics + ', background=' + this.background + ', flash=' + this.flash;
- }
-}
-
-/**
- * Unicode character with styling and background.
- * @constructor
- */
-class StyledUnicodeChar {
- constructor() {
- this.uchar = ' ';
- this.penState = new PenState();
- }
- reset() {
- this.uchar = ' ';
- this.penState.reset();
- }
- setChar(uchar, newPenState) {
- this.uchar = uchar;
- this.penState.copy(newPenState);
- }
- setPenState(newPenState) {
- this.penState.copy(newPenState);
- }
- equals(other) {
- return this.uchar === other.uchar && this.penState.equals(other.penState);
- }
- copy(newChar) {
- this.uchar = newChar.uchar;
- this.penState.copy(newChar.penState);
- }
- isEmpty() {
- return this.uchar === ' ' && this.penState.isDefault();
- }
-}
-
-/**
- * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
- * @constructor
- */
-class Row {
- constructor(logger) {
- this.chars = [];
- this.pos = 0;
- this.currPenState = new PenState();
- this.cueStartTime = null;
- this.logger = void 0;
- for (let i = 0; i < NR_COLS; i++) {
- this.chars.push(new StyledUnicodeChar());
- }
- this.logger = logger;
- }
- equals(other) {
- for (let i = 0; i < NR_COLS; i++) {
- if (!this.chars[i].equals(other.chars[i])) {
- return false;
- }
- }
- return true;
- }
- copy(other) {
- for (let i = 0; i < NR_COLS; i++) {
- this.chars[i].copy(other.chars[i]);
- }
- }
- isEmpty() {
- let empty = true;
- for (let i = 0; i < NR_COLS; i++) {
- if (!this.chars[i].isEmpty()) {
- empty = false;
- break;
- }
- }
- return empty;
- }
-
- /**
- * Set the cursor to a valid column.
- */
- setCursor(absPos) {
- if (this.pos !== absPos) {
- this.pos = absPos;
- }
- if (this.pos < 0) {
- this.logger.log(3, 'Negative cursor position ' + this.pos);
- this.pos = 0;
- } else if (this.pos > NR_COLS) {
- this.logger.log(3, 'Too large cursor position ' + this.pos);
- this.pos = NR_COLS;
- }
- }
-
- /**
- * Move the cursor relative to current position.
- */
- moveCursor(relPos) {
- const newPos = this.pos + relPos;
- if (relPos > 1) {
- for (let i = this.pos + 1; i < newPos + 1; i++) {
- this.chars[i].setPenState(this.currPenState);
- }
- }
- this.setCursor(newPos);
- }
-
- /**
- * Backspace, move one step back and clear character.
- */
- backSpace() {
- this.moveCursor(-1);
- this.chars[this.pos].setChar(' ', this.currPenState);
- }
- insertChar(byte) {
- if (byte >= 0x90) {
- // Extended char
- this.backSpace();
- }
- const char = getCharForByte(byte);
- if (this.pos >= NR_COLS) {
- this.logger.log(0, () => 'Cannot insert ' + byte.toString(16) + ' (' + char + ') at position ' + this.pos + '. Skipping it!');
- return;
- }
- this.chars[this.pos].setChar(char, this.currPenState);
- this.moveCursor(1);
- }
- clearFromPos(startPos) {
- let i;
- for (i = startPos; i < NR_COLS; i++) {
- this.chars[i].reset();
- }
- }
- clear() {
- this.clearFromPos(0);
- this.pos = 0;
- this.currPenState.reset();
- }
- clearToEndOfRow() {
- this.clearFromPos(this.pos);
- }
- getTextString() {
- const chars = [];
- let empty = true;
- for (let i = 0; i < NR_COLS; i++) {
- const char = this.chars[i].uchar;
- if (char !== ' ') {
- empty = false;
- }
- chars.push(char);
- }
- if (empty) {
- return '';
- } else {
- return chars.join('');
- }
- }
- setPenStyles(styles) {
- this.currPenState.setStyles(styles);
- const currChar = this.chars[this.pos];
- currChar.setPenState(this.currPenState);
- }
-}
-
-/**
- * Keep a CEA-608 screen of 32x15 styled characters
- * @constructor
- */
-class CaptionScreen {
- constructor(logger) {
- this.rows = [];
- this.currRow = NR_ROWS - 1;
- this.nrRollUpRows = null;
- this.lastOutputScreen = null;
- this.logger = void 0;
- for (let i = 0; i < NR_ROWS; i++) {
- this.rows.push(new Row(logger));
- }
- this.logger = logger;
- }
- reset() {
- for (let i = 0; i < NR_ROWS; i++) {
- this.rows[i].clear();
- }
- this.currRow = NR_ROWS - 1;
- }
- equals(other) {
- let equal = true;
- for (let i = 0; i < NR_ROWS; i++) {
- if (!this.rows[i].equals(other.rows[i])) {
- equal = false;
- break;
- }
- }
- return equal;
- }
- copy(other) {
- for (let i = 0; i < NR_ROWS; i++) {
- this.rows[i].copy(other.rows[i]);
- }
- }
- isEmpty() {
- let empty = true;
- for (let i = 0; i < NR_ROWS; i++) {
- if (!this.rows[i].isEmpty()) {
- empty = false;
- break;
- }
- }
- return empty;
- }
- backSpace() {
- const row = this.rows[this.currRow];
- row.backSpace();
- }
- clearToEndOfRow() {
- const row = this.rows[this.currRow];
- row.clearToEndOfRow();
- }
-
- /**
- * Insert a character (without styling) in the current row.
- */
- insertChar(char) {
- const row = this.rows[this.currRow];
- row.insertChar(char);
- }
- setPen(styles) {
- const row = this.rows[this.currRow];
- row.setPenStyles(styles);
- }
- moveCursor(relPos) {
- const row = this.rows[this.currRow];
- row.moveCursor(relPos);
- }
- setCursor(absPos) {
- this.logger.log(2, 'setCursor: ' + absPos);
- const row = this.rows[this.currRow];
- row.setCursor(absPos);
- }
- setPAC(pacData) {
- this.logger.log(2, () => 'pacData = ' + stringify(pacData));
- let newRow = pacData.row - 1;
- if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
- newRow = this.nrRollUpRows - 1;
- }
-
- // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
- if (this.nrRollUpRows && this.currRow !== newRow) {
- // clear all rows first
- for (let i = 0; i < NR_ROWS; i++) {
- this.rows[i].clear();
- }
-
- // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
- // topRowIndex - the start of rows to copy (inclusive index)
- const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
- // We only copy if the last position was already shown.
- // We use the cueStartTime value to check this.
- const lastOutputScreen = this.lastOutputScreen;
- if (lastOutputScreen) {
- const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
- const time = this.logger.time;
- if (prevLineTime !== null && time !== null && prevLineTime < time) {
- for (let i = 0; i < this.nrRollUpRows; i++) {
- this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]);
- }
- }
- }
- }
- this.currRow = newRow;
- const row = this.rows[this.currRow];
- if (pacData.indent !== null) {
- const indent = pacData.indent;
- const prevPos = Math.max(indent - 1, 0);
- row.setCursor(pacData.indent);
- pacData.color = row.chars[prevPos].penState.foreground;
- }
- const styles = {
- foreground: pacData.color,
- underline: pacData.underline,
- italics: pacData.italics,
- background: 'black',
- flash: false
- };
- this.setPen(styles);
- }
-
- /**
- * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
- */
- setBkgData(bkgData) {
- this.logger.log(2, () => 'bkgData = ' + stringify(bkgData));
- this.backSpace();
- this.setPen(bkgData);
- this.insertChar(0x20); // Space
- }
- setRollUpRows(nrRows) {
- this.nrRollUpRows = nrRows;
- }
- rollUp() {
- if (this.nrRollUpRows === null) {
- this.logger.log(3, 'roll_up but nrRollUpRows not set yet');
- return; // Not properly setup
- }
- this.logger.log(1, () => this.getDisplayText());
- const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
- const topRow = this.rows.splice(topRowIndex, 1)[0];
- topRow.clear();
- this.rows.splice(this.currRow, 0, topRow);
- this.logger.log(2, 'Rolling up');
- // this.logger.log(VerboseLevel.TEXT, this.get_display_text())
- }
-
- /**
- * Get all non-empty rows with as unicode text.
- */
- getDisplayText(asOneRow) {
- asOneRow = asOneRow || false;
- const displayText = [];
- let text = '';
- let rowNr = -1;
- for (let i = 0; i < NR_ROWS; i++) {
- const rowText = this.rows[i].getTextString();
- if (rowText) {
- rowNr = i + 1;
- if (asOneRow) {
- displayText.push('Row ' + rowNr + ": '" + rowText + "'");
- } else {
- displayText.push(rowText.trim());
- }
- }
- }
- if (displayText.length > 0) {
- if (asOneRow) {
- text = '[' + displayText.join(' | ') + ']';
- } else {
- text = displayText.join('\n');
- }
- }
- return text;
- }
- getTextAndFormat() {
- return this.rows;
- }
-}
-
-// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
-
-class Cea608Channel {
- constructor(channelNumber, outputFilter, logger) {
- this.chNr = void 0;
- this.outputFilter = void 0;
- this.mode = void 0;
- this.verbose = void 0;
- this.displayedMemory = void 0;
- this.nonDisplayedMemory = void 0;
- this.lastOutputScreen = void 0;
- this.currRollUpRow = void 0;
- this.writeScreen = void 0;
- this.cueStartTime = void 0;
- this.logger = void 0;
- this.chNr = channelNumber;
- this.outputFilter = outputFilter;
- this.mode = null;
- this.verbose = 0;
- this.displayedMemory = new CaptionScreen(logger);
- this.nonDisplayedMemory = new CaptionScreen(logger);
- this.lastOutputScreen = new CaptionScreen(logger);
- this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
- this.writeScreen = this.displayedMemory;
- this.mode = null;
- this.cueStartTime = null; // Keeps track of where a cue started.
- this.logger = logger;
- }
- reset() {
- this.mode = null;
- this.displayedMemory.reset();
- this.nonDisplayedMemory.reset();
- this.lastOutputScreen.reset();
- this.outputFilter.reset();
- this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
- this.writeScreen = this.displayedMemory;
- this.mode = null;
- this.cueStartTime = null;
- }
- getHandler() {
- return this.outputFilter;
- }
- setHandler(newHandler) {
- this.outputFilter = newHandler;
- }
- setPAC(pacData) {
- this.writeScreen.setPAC(pacData);
- }
- setBkgData(bkgData) {
- this.writeScreen.setBkgData(bkgData);
- }
- setMode(newMode) {
- if (newMode === this.mode) {
- return;
- }
- this.mode = newMode;
- this.logger.log(2, () => 'MODE=' + newMode);
- if (this.mode === 'MODE_POP-ON') {
- this.writeScreen = this.nonDisplayedMemory;
- } else {
- this.writeScreen = this.displayedMemory;
- this.writeScreen.reset();
- }
- if (this.mode !== 'MODE_ROLL-UP') {
- this.displayedMemory.nrRollUpRows = null;
- this.nonDisplayedMemory.nrRollUpRows = null;
- }
- this.mode = newMode;
- }
- insertChars(chars) {
- for (let i = 0; i < chars.length; i++) {
- this.writeScreen.insertChar(chars[i]);
- }
- const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
- this.logger.log(2, () => screen + ': ' + this.writeScreen.getDisplayText(true));
- if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
- this.logger.log(1, () => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true));
- this.outputDataUpdate();
- }
- }
- ccRCL() {
- // Resume Caption Loading (switch mode to Pop On)
- this.logger.log(2, 'RCL - Resume Caption Loading');
- this.setMode('MODE_POP-ON');
- }
- ccBS() {
- // BackSpace
- this.logger.log(2, 'BS - BackSpace');
- if (this.mode === 'MODE_TEXT') {
- return;
- }
- this.writeScreen.backSpace();
- if (this.writeScreen === this.displayedMemory) {
- this.outputDataUpdate();
- }
- }
- ccAOF() {
- // Reserved (formerly Alarm Off)
- }
- ccAON() {
- // Reserved (formerly Alarm On)
- }
- ccDER() {
- // Delete to End of Row
- this.logger.log(2, 'DER- Delete to End of Row');
- this.writeScreen.clearToEndOfRow();
- this.outputDataUpdate();
- }
- ccRU(nrRows) {
- // Roll-Up Captions-2,3,or 4 Rows
- this.logger.log(2, 'RU(' + nrRows + ') - Roll Up');
- this.writeScreen = this.displayedMemory;
- this.setMode('MODE_ROLL-UP');
- this.writeScreen.setRollUpRows(nrRows);
- }
- ccFON() {
- // Flash On
- this.logger.log(2, 'FON - Flash On');
- this.writeScreen.setPen({
- flash: true
- });
- }
- ccRDC() {
- // Resume Direct Captioning (switch mode to PaintOn)
- this.logger.log(2, 'RDC - Resume Direct Captioning');
- this.setMode('MODE_PAINT-ON');
- }
- ccTR() {
- // Text Restart in text mode (not supported, however)
- this.logger.log(2, 'TR');
- this.setMode('MODE_TEXT');
- }
- ccRTD() {
- // Resume Text Display in Text mode (not supported, however)
- this.logger.log(2, 'RTD');
- this.setMode('MODE_TEXT');
- }
- ccEDM() {
- // Erase Displayed Memory
- this.logger.log(2, 'EDM - Erase Displayed Memory');
- this.displayedMemory.reset();
- this.outputDataUpdate(true);
- }
- ccCR() {
- // Carriage Return
- this.logger.log(2, 'CR - Carriage Return');
- this.writeScreen.rollUp();
- this.outputDataUpdate(true);
- }
- ccENM() {
- // Erase Non-Displayed Memory
- this.logger.log(2, 'ENM - Erase Non-displayed Memory');
- this.nonDisplayedMemory.reset();
- }
- ccEOC() {
- // End of Caption (Flip Memories)
- this.logger.log(2, 'EOC - End Of Caption');
- if (this.mode === 'MODE_POP-ON') {
- const tmp = this.displayedMemory;
- this.displayedMemory = this.nonDisplayedMemory;
- this.nonDisplayedMemory = tmp;
- this.writeScreen = this.nonDisplayedMemory;
- this.logger.log(1, () => 'DISP: ' + this.displayedMemory.getDisplayText());
- }
- this.outputDataUpdate(true);
- }
- ccTO(nrCols) {
- // Tab Offset 1,2, or 3 columns
- this.logger.log(2, 'TO(' + nrCols + ') - Tab Offset');
- this.writeScreen.moveCursor(nrCols);
- }
- ccMIDROW(secondByte) {
- // Parse MIDROW command
- const styles = {
- flash: false
- };
- styles.underline = secondByte % 2 === 1;
- styles.italics = secondByte >= 0x2e;
- if (!styles.italics) {
- const colorIndex = Math.floor(secondByte / 2) - 0x10;
- const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta'];
- styles.foreground = colors[colorIndex];
- } else {
- styles.foreground = 'white';
- }
- this.logger.log(2, 'MIDROW: ' + stringify(styles));
- this.writeScreen.setPen(styles);
- }
- outputDataUpdate(dispatch = false) {
- const time = this.logger.time;
- if (time === null) {
- return;
- }
- if (this.outputFilter) {
- if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) {
- // Start of a new cue
- this.cueStartTime = time;
- } else {
- if (!this.displayedMemory.equals(this.lastOutputScreen)) {
- this.outputFilter.newCue(this.cueStartTime, time, this.lastOutputScreen);
- if (dispatch && this.outputFilter.dispatchCue) {
- this.outputFilter.dispatchCue();
- }
- this.cueStartTime = this.displayedMemory.isEmpty() ? null : time;
- }
- }
- this.lastOutputScreen.copy(this.displayedMemory);
- }
- }
- cueSplitAtTime(t) {
- if (this.outputFilter) {
- if (!this.displayedMemory.isEmpty()) {
- if (this.outputFilter.newCue) {
- this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory);
- }
- this.cueStartTime = t;
- }
- }
- }
-}
-
-// Will be 1 or 2 when parsing captions
-
-class Cea608Parser {
- constructor(field, out1, out2) {
- this.channels = void 0;
- this.currentChannel = 0;
- this.cmdHistory = createCmdHistory();
- this.logger = void 0;
- const logger = this.logger = new CaptionsLogger();
- this.channels = [null, new Cea608Channel(field, out1, logger), new Cea608Channel(field + 1, out2, logger)];
- }
- getHandler(channel) {
- return this.channels[channel].getHandler();
- }
- setHandler(channel, newHandler) {
- this.channels[channel].setHandler(newHandler);
- }
-
- /**
- * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
- */
- addData(time, byteList) {
- this.logger.time = time;
- for (let i = 0; i < byteList.length; i += 2) {
- const a = byteList[i] & 0x7f;
- const b = byteList[i + 1] & 0x7f;
- let cmdFound = false;
- let charsFound = null;
- if (a === 0 && b === 0) {
- continue;
- } else {
- this.logger.log(3, () => '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')');
- }
- const cmdHistory = this.cmdHistory;
- const isControlCode = a >= 0x10 && a <= 0x1f;
- if (isControlCode) {
- // Skip redundant control codes
- if (hasCmdRepeated(a, b, cmdHistory)) {
- setLastCmd(null, null, cmdHistory);
- this.logger.log(3, () => 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped');
- continue;
- }
- setLastCmd(a, b, this.cmdHistory);
- cmdFound = this.parseCmd(a, b);
- if (!cmdFound) {
- cmdFound = this.parseMidrow(a, b);
- }
- if (!cmdFound) {
- cmdFound = this.parsePAC(a, b);
- }
- if (!cmdFound) {
- cmdFound = this.parseBackgroundAttributes(a, b);
- }
- } else {
- setLastCmd(null, null, cmdHistory);
- }
- if (!cmdFound) {
- charsFound = this.parseChars(a, b);
- if (charsFound) {
- const currChNr = this.currentChannel;
- if (currChNr && currChNr > 0) {
- const channel = this.channels[currChNr];
- channel.insertChars(charsFound);
- } else {
- this.logger.log(2, 'No channel found yet. TEXT-MODE?');
- }
- }
- }
- if (!cmdFound && !charsFound) {
- this.logger.log(2, () => "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]]));
- }
- }
- }
-
- /**
- * Parse Command.
- * @returns True if a command was found
- */
- parseCmd(a, b) {
- const cond1 = (a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) && b >= 0x20 && b <= 0x2f;
- const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23;
- if (!(cond1 || cond2)) {
- return false;
- }
- const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2;
- const channel = this.channels[chNr];
- if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) {
- if (b === 0x20) {
- channel.ccRCL();
- } else if (b === 0x21) {
- channel.ccBS();
- } else if (b === 0x22) {
- channel.ccAOF();
- } else if (b === 0x23) {
- channel.ccAON();
- } else if (b === 0x24) {
- channel.ccDER();
- } else if (b === 0x25) {
- channel.ccRU(2);
- } else if (b === 0x26) {
- channel.ccRU(3);
- } else if (b === 0x27) {
- channel.ccRU(4);
- } else if (b === 0x28) {
- channel.ccFON();
- } else if (b === 0x29) {
- channel.ccRDC();
- } else if (b === 0x2a) {
- channel.ccTR();
- } else if (b === 0x2b) {
- channel.ccRTD();
- } else if (b === 0x2c) {
- channel.ccEDM();
- } else if (b === 0x2d) {
- channel.ccCR();
- } else if (b === 0x2e) {
- channel.ccENM();
- } else if (b === 0x2f) {
- channel.ccEOC();
- }
- } else {
- // a == 0x17 || a == 0x1F
- channel.ccTO(b - 0x20);
- }
- this.currentChannel = chNr;
- return true;
- }
-
- /**
- * Parse midrow styling command
- */
- parseMidrow(a, b) {
- let chNr = 0;
- if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) {
- if (a === 0x11) {
- chNr = 1;
- } else {
- chNr = 2;
- }
- if (chNr !== this.currentChannel) {
- this.logger.log(0, 'Mismatch channel in midrow parsing');
- return false;
- }
- const channel = this.channels[chNr];
- if (!channel) {
- return false;
- }
- channel.ccMIDROW(b);
- this.logger.log(3, () => 'MIDROW (' + numArrayToHexArray([a, b]) + ')');
- return true;
- }
- return false;
- }
-
- /**
- * Parse Preable Access Codes (Table 53).
- * @returns {Boolean} Tells if PAC found
- */
- parsePAC(a, b) {
- let row;
- const case1 = (a >= 0x11 && a <= 0x17 || a >= 0x19 && a <= 0x1f) && b >= 0x40 && b <= 0x7f;
- const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f;
- if (!(case1 || case2)) {
- return false;
- }
- const chNr = a <= 0x17 ? 1 : 2;
- if (b >= 0x40 && b <= 0x5f) {
- row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a];
- } else {
- // 0x60 <= b <= 0x7F
- row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a];
- }
- const channel = this.channels[chNr];
- if (!channel) {
- return false;
- }
- channel.setPAC(this.interpretPAC(row, b));
- this.currentChannel = chNr;
- return true;
- }
-
- /**
- * Interpret the second byte of the pac, and return the information.
- * @returns pacData with style parameters
- */
- interpretPAC(row, byte) {
- let pacIndex;
- const pacData = {
- color: null,
- italics: false,
- indent: null,
- underline: false,
- row: row
- };
- if (byte > 0x5f) {
- pacIndex = byte - 0x60;
- } else {
- pacIndex = byte - 0x40;
- }
- pacData.underline = (pacIndex & 1) === 1;
- if (pacIndex <= 0xd) {
- pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)];
- } else if (pacIndex <= 0xf) {
- pacData.italics = true;
- pacData.color = 'white';
- } else {
- pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4;
- }
- return pacData; // Note that row has zero offset. The spec uses 1.
- }
-
- /**
- * Parse characters.
- * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
- */
- parseChars(a, b) {
- let channelNr;
- let charCodes = null;
- let charCode1 = null;
- if (a >= 0x19) {
- channelNr = 2;
- charCode1 = a - 8;
- } else {
- channelNr = 1;
- charCode1 = a;
- }
- if (charCode1 >= 0x11 && charCode1 <= 0x13) {
- // Special character
- let oneCode;
- if (charCode1 === 0x11) {
- oneCode = b + 0x50;
- } else if (charCode1 === 0x12) {
- oneCode = b + 0x70;
- } else {
- oneCode = b + 0x90;
- }
- this.logger.log(2, () => "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr);
- charCodes = [oneCode];
- } else if (a >= 0x20 && a <= 0x7f) {
- charCodes = b === 0 ? [a] : [a, b];
- }
- if (charCodes) {
- this.logger.log(3, () => 'Char codes = ' + numArrayToHexArray(charCodes).join(','));
- }
- return charCodes;
- }
-
- /**
- * Parse extended background attributes as well as new foreground color black.
- * @returns True if background attributes are found
- */
- parseBackgroundAttributes(a, b) {
- const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f;
- const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f;
- if (!(case1 || case2)) {
- return false;
- }
- let index;
- const bkgData = {};
- if (a === 0x10 || a === 0x18) {
- index = Math.floor((b - 0x20) / 2);
- bkgData.background = backgroundColors[index];
- if (b % 2 === 1) {
- bkgData.background = bkgData.background + '_semi';
- }
- } else if (b === 0x2d) {
- bkgData.background = 'transparent';
- } else {
- bkgData.foreground = 'black';
- if (b === 0x2f) {
- bkgData.underline = true;
- }
- }
- const chNr = a <= 0x17 ? 1 : 2;
- const channel = this.channels[chNr];
- channel.setBkgData(bkgData);
- return true;
- }
-
- /**
- * Reset state of parser and its channels.
- */
- reset() {
- for (let i = 0; i < Object.keys(this.channels).length; i++) {
- const channel = this.channels[i];
- if (channel) {
- channel.reset();
- }
- }
- setLastCmd(null, null, this.cmdHistory);
- }
-
- /**
- * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
- */
- cueSplitAtTime(t) {
- for (let i = 0; i < this.channels.length; i++) {
- const channel = this.channels[i];
- if (channel) {
- channel.cueSplitAtTime(t);
- }
- }
- }
-}
-function setLastCmd(a, b, cmdHistory) {
- cmdHistory.a = a;
- cmdHistory.b = b;
-}
-function hasCmdRepeated(a, b, cmdHistory) {
- return cmdHistory.a === a && cmdHistory.b === b;
-}
-function createCmdHistory() {
- return {
- a: null,
- b: null
- };
-}
-
-/**
- * Copyright 2013 vtt.js Contributors
- *
- * 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.
- */
-
-var VTTCue = (function () {
- if (optionalSelf != null && optionalSelf.VTTCue) {
- return self.VTTCue;
- }
- const AllowedDirections = ['', 'lr', 'rl'];
- const AllowedAlignments = ['start', 'middle', 'end', 'left', 'right'];
- function isAllowedValue(allowed, value) {
- if (typeof value !== 'string') {
- return false;
- }
- // necessary for assuring the generic conforms to the Array interface
- if (!Array.isArray(allowed)) {
- return false;
- }
- // reset the type so that the next narrowing works well
- const lcValue = value.toLowerCase();
- // use the allow list to narrow the type to a specific subset of strings
- if (~allowed.indexOf(lcValue)) {
- return lcValue;
- }
- return false;
- }
- function findDirectionSetting(value) {
- return isAllowedValue(AllowedDirections, value);
- }
- function findAlignSetting(value) {
- return isAllowedValue(AllowedAlignments, value);
- }
- function extend(obj, ...rest) {
- let i = 1;
- for (; i < arguments.length; i++) {
- const cobj = arguments[i];
- for (const p in cobj) {
- obj[p] = cobj[p];
- }
- }
- return obj;
- }
- function VTTCue(startTime, endTime, text) {
- const cue = this;
- const baseObj = {
- enumerable: true
- };
- /**
- * Shim implementation specific properties. These properties are not in
- * the spec.
- */
-
- // Lets us know when the VTTCue's data has changed in such a way that we need
- // to recompute its display state. This lets us compute its display state
- // lazily.
- cue.hasBeenReset = false;
-
- /**
- * VTTCue and TextTrackCue properties
- * http://dev.w3.org/html5/webvtt/#vttcue-interface
- */
-
- let _id = '';
- let _pauseOnExit = false;
- let _startTime = startTime;
- let _endTime = endTime;
- let _text = text;
- let _region = null;
- let _vertical = '';
- let _snapToLines = true;
- let _line = 'auto';
- let _lineAlign = 'start';
- let _position = 50;
- let _positionAlign = 'middle';
- let _size = 50;
- let _align = 'middle';
- Object.defineProperty(cue, 'id', extend({}, baseObj, {
- get: function () {
- return _id;
- },
- set: function (value) {
- _id = '' + value;
- }
- }));
- Object.defineProperty(cue, 'pauseOnExit', extend({}, baseObj, {
- get: function () {
- return _pauseOnExit;
- },
- set: function (value) {
- _pauseOnExit = !!value;
- }
- }));
- Object.defineProperty(cue, 'startTime', extend({}, baseObj, {
- get: function () {
- return _startTime;
- },
- set: function (value) {
- if (typeof value !== 'number') {
- throw new TypeError('Start time must be set to a number.');
- }
- _startTime = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'endTime', extend({}, baseObj, {
- get: function () {
- return _endTime;
- },
- set: function (value) {
- if (typeof value !== 'number') {
- throw new TypeError('End time must be set to a number.');
- }
- _endTime = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'text', extend({}, baseObj, {
- get: function () {
- return _text;
- },
- set: function (value) {
- _text = '' + value;
- this.hasBeenReset = true;
- }
- }));
-
- // todo: implement VTTRegion polyfill?
- Object.defineProperty(cue, 'region', extend({}, baseObj, {
- get: function () {
- return _region;
- },
- set: function (value) {
- _region = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'vertical', extend({}, baseObj, {
- get: function () {
- return _vertical;
- },
- set: function (value) {
- const setting = findDirectionSetting(value);
- // Have to check for false because the setting an be an empty string.
- if (setting === false) {
- throw new SyntaxError('An invalid or illegal string was specified.');
- }
- _vertical = setting;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'snapToLines', extend({}, baseObj, {
- get: function () {
- return _snapToLines;
- },
- set: function (value) {
- _snapToLines = !!value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'line', extend({}, baseObj, {
- get: function () {
- return _line;
- },
- set: function (value) {
- if (typeof value !== 'number' && value !== 'auto') {
- throw new SyntaxError('An invalid number or illegal string was specified.');
- }
- _line = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'lineAlign', extend({}, baseObj, {
- get: function () {
- return _lineAlign;
- },
- set: function (value) {
- const setting = findAlignSetting(value);
- if (!setting) {
- throw new SyntaxError('An invalid or illegal string was specified.');
- }
- _lineAlign = setting;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'position', extend({}, baseObj, {
- get: function () {
- return _position;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error('Position must be between 0 and 100.');
- }
- _position = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'positionAlign', extend({}, baseObj, {
- get: function () {
- return _positionAlign;
- },
- set: function (value) {
- const setting = findAlignSetting(value);
- if (!setting) {
- throw new SyntaxError('An invalid or illegal string was specified.');
- }
- _positionAlign = setting;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'size', extend({}, baseObj, {
- get: function () {
- return _size;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error('Size must be between 0 and 100.');
- }
- _size = value;
- this.hasBeenReset = true;
- }
- }));
- Object.defineProperty(cue, 'align', extend({}, baseObj, {
- get: function () {
- return _align;
- },
- set: function (value) {
- const setting = findAlignSetting(value);
- if (!setting) {
- throw new SyntaxError('An invalid or illegal string was specified.');
- }
- _align = setting;
- this.hasBeenReset = true;
- }
- }));
-
- /**
- * Other