Buckets:
ktongue/docker_container / simsite /frontend /node_modules /hls.js /src /controller /interstitials-controller.ts
| import { createDoNothingErrorAction } from './error-controller'; | |
| import { HlsAssetPlayer } from './interstitial-player'; | |
| import { | |
| type InterstitialScheduleEventItem, | |
| type InterstitialScheduleItem, | |
| type InterstitialSchedulePrimaryItem, | |
| InterstitialsSchedule, | |
| segmentToString, | |
| type TimelineType, | |
| } from './interstitials-schedule'; | |
| import { ErrorDetails, ErrorTypes } from '../errors'; | |
| import { Events } from '../events'; | |
| import { AssetListLoader } from '../loader/interstitial-asset-list'; | |
| import { | |
| ALIGNED_END_THRESHOLD_SECONDS, | |
| eventAssetToString, | |
| generateAssetIdentifier, | |
| getNextAssetIndex, | |
| type InterstitialAssetId, | |
| type InterstitialAssetItem, | |
| type InterstitialEvent, | |
| type InterstitialEventWithAssetList, | |
| TimelineOccupancy, | |
| } from '../loader/interstitial-event'; | |
| import { BufferHelper } from '../utils/buffer-helper'; | |
| import { | |
| addEventListener, | |
| removeEventListener, | |
| } from '../utils/event-listener-helper'; | |
| import { hash } from '../utils/hash'; | |
| import { Logger } from '../utils/logger'; | |
| import { isCompatibleTrackChange } from '../utils/mediasource-helper'; | |
| import { getBasicSelectionOption } from '../utils/rendition-helper'; | |
| import { stringify } from '../utils/safe-json-stringify'; | |
| import type { | |
| HlsAssetPlayerConfig, | |
| InterstitialPlayer, | |
| } from './interstitial-player'; | |
| import type Hls from '../hls'; | |
| import type { LevelDetails } from '../loader/level-details'; | |
| import type { SourceBufferName } from '../types/buffer'; | |
| import type { NetworkComponentAPI } from '../types/component-api'; | |
| import type { | |
| AssetListLoadedData, | |
| AudioTrackSwitchingData, | |
| AudioTrackUpdatedData, | |
| BufferAppendedData, | |
| BufferCodecsData, | |
| BufferFlushedData, | |
| ErrorData, | |
| LevelUpdatedData, | |
| MediaAttachedData, | |
| MediaAttachingData, | |
| MediaDetachingData, | |
| SubtitleTrackSwitchData, | |
| SubtitleTrackUpdatedData, | |
| } from '../types/events'; | |
| import type { MediaPlaylist, MediaSelection } from '../types/media-playlist'; | |
| export interface InterstitialsManager { | |
| events: InterstitialEvent[]; | |
| schedule: InterstitialScheduleItem[]; | |
| interstitialPlayer: InterstitialPlayer | null; | |
| playerQueue: HlsAssetPlayer[]; | |
| bufferingAsset: InterstitialAssetItem | null; | |
| bufferingItem: InterstitialScheduleItem | null; | |
| bufferingIndex: number; | |
| playingAsset: InterstitialAssetItem | null; | |
| playingItem: InterstitialScheduleItem | null; | |
| playingIndex: number; | |
| primary: PlayheadTimes; | |
| integrated: PlayheadTimes; | |
| skip: () => void; | |
| } | |
| export type PlayheadTimes = { | |
| bufferedEnd: number; | |
| currentTime: number; | |
| duration: number; | |
| seekableStart: number; | |
| }; | |
| function playWithCatch(media: HTMLMediaElement | null) { | |
| (media?.play() as Promise<void> | undefined)?.catch(() => { | |
| /* no-op */ | |
| }); | |
| } | |
| function timelineMessage(label: string, time: number) { | |
| return `[${label}] Advancing timeline position to ${time}`; | |
| } | |
| export default class InterstitialsController | |
| extends Logger | |
| implements NetworkComponentAPI | |
| { | |
| private readonly HlsPlayerClass: typeof Hls; | |
| private readonly hls: Hls; | |
| private readonly assetListLoader: AssetListLoader; | |
| // Last updated LevelDetails | |
| private mediaSelection: MediaSelection | null = null; | |
| private altSelection: { | |
| audio?: MediaPlaylist; | |
| subtitles?: MediaPlaylist; | |
| } | null = null; | |
| // Media and MediaSource/SourceBuffers | |
| private media: HTMLMediaElement | null = null; | |
| private detachedData: MediaAttachingData | null = null; | |
| private requiredTracks: Partial<BufferCodecsData> | null = null; | |
| // Public Interface for Interstitial playback state and control | |
| private manager: InterstitialsManager | null = null; | |
| // Interstitial Asset Players | |
| private playerQueue: HlsAssetPlayer[] = []; | |
| // Timeline position tracking | |
| private bufferedPos: number = -1; | |
| private timelinePos: number = -1; | |
| // Schedule | |
| private schedule: InterstitialsSchedule | null; | |
| // Schedule playback and buffering state | |
| private playingItem: InterstitialScheduleItem | null = null; | |
| private bufferingItem: InterstitialScheduleItem | null = null; | |
| private waitingItem: InterstitialScheduleEventItem | null = null; | |
| private endedItem: InterstitialScheduleItem | null = null; | |
| private playingAsset: InterstitialAssetItem | null = null; | |
| private endedAsset: InterstitialAssetItem | null = null; | |
| private bufferingAsset: InterstitialAssetItem | null = null; | |
| private shouldPlay: boolean = false; | |
| constructor(hls: Hls, HlsPlayerClass: typeof Hls) { | |
| super('interstitials', hls.logger); | |
| this.hls = hls; | |
| this.HlsPlayerClass = HlsPlayerClass; | |
| this.assetListLoader = new AssetListLoader(hls); | |
| this.schedule = new InterstitialsSchedule( | |
| this.onScheduleUpdate, | |
| hls.logger, | |
| ); | |
| this.registerListeners(); | |
| } | |
| private 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); | |
| } | |
| } | |
| private 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() { | |
| this.getBufferingPlayer()?.resumeBuffering(); | |
| } | |
| pauseBuffering() { | |
| this.getBufferingPlayer()?.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; | |
| } | |
| private onDestroying() { | |
| const media = this.primaryMedia || this.media; | |
| if (media) { | |
| this.removeMediaListeners(media); | |
| } | |
| } | |
| private removeMediaListeners(media: HTMLMediaElement) { | |
| removeEventListener(media, 'play', this.onPlay); | |
| removeEventListener(media, 'pause', this.onPause); | |
| removeEventListener(media, 'seeking', this.onSeeking); | |
| removeEventListener(media, 'timeupdate', this.onTimeupdate); | |
| } | |
| private onMediaAttaching( | |
| event: Events.MEDIA_ATTACHING, | |
| data: MediaAttachingData, | |
| ) { | |
| 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); | |
| } | |
| private onMediaAttached( | |
| event: Events.MEDIA_ATTACHED, | |
| data: MediaAttachedData, | |
| ) { | |
| 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); | |
| } | |
| } | |
| private clearScheduleState() { | |
| this.log(`clear schedule state`); | |
| this.playingItem = | |
| this.bufferingItem = | |
| this.waitingItem = | |
| this.endedItem = | |
| this.playingAsset = | |
| this.endedAsset = | |
| this.bufferingAsset = | |
| null; | |
| } | |
| private onMediaDetaching( | |
| event: Events.MEDIA_DETACHING, | |
| data: MediaDetachingData, | |
| ) { | |
| 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; | |
| } | |
| } | |
| public get interstitialsManager(): InterstitialsManager | null { | |
| // 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: InterstitialAssetItem | null) => | |
| asset ? c.getAssetPlayer(asset.identifier) : asset; | |
| const getMappedTime = ( | |
| item: InterstitialScheduleItem | null, | |
| timelineType: TimelineType, | |
| asset: InterstitialAssetItem | null, | |
| controllerField: 'bufferedPos' | 'timelinePos', | |
| assetPlayerField: 'bufferedEnd' | 'currentTime', | |
| ): number => { | |
| if (item) { | |
| let time = ( | |
| item[timelineType] as { | |
| start: number; | |
| end: number; | |
| } | |
| ).start; | |
| const interstitial = item.event; | |
| if (interstitial) { | |
| if ( | |
| timelineType === 'playout' || | |
| interstitial.timelineOccupancy !== TimelineOccupancy.Point | |
| ) { | |
| const assetPlayer = getAssetPlayer(asset); | |
| if (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: number, | |
| timelineType: TimelineType, | |
| ): number => { | |
| if ( | |
| primaryTime !== 0 && | |
| timelineType !== 'primary' && | |
| c.schedule?.length | |
| ) { | |
| const index = c.schedule.findItemIndexAtTime(primaryTime); | |
| const item = c.schedule.items?.[index]; | |
| if (item) { | |
| const diff = item[timelineType].start - item.start; | |
| return primaryTime + diff; | |
| } | |
| } | |
| return primaryTime; | |
| }; | |
| const getBufferedEnd = (): number => { | |
| const value = c.bufferedPos; | |
| if (value === Number.MAX_VALUE) { | |
| return getMappedDuration('primary'); | |
| } | |
| return Math.max(value, 0); | |
| }; | |
| const getMappedDuration = (timelineType: TimelineType): number => { | |
| if (c.primaryDetails?.live) { | |
| // return end of last event item or playlist | |
| return c.primaryDetails.edge; | |
| } | |
| return c.schedule?.durations[timelineType] || 0; | |
| }; | |
| const seekTo = (time: number, timelineType: TimelineType) => { | |
| const item = c.effectivePlayingItem; | |
| if (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.items?.[targetIndex]; | |
| const bufferingPlayer = c.getBufferingPlayer(); | |
| const bufferingInterstitial = bufferingPlayer?.interstitial; | |
| const appendInPlace = 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?.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?.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: InterstitialPlayer = { | |
| get bufferedEnd() { | |
| const interstitialItem = effectiveBufferingItem(); | |
| const bufferingItem = c.bufferingItem; | |
| if (bufferingItem && bufferingItem === interstitialItem) { | |
| return ( | |
| getMappedTime( | |
| bufferingItem, | |
| 'playout', | |
| c.bufferingAsset, | |
| 'bufferedPos', | |
| 'bufferedEnd', | |
| ) - bufferingItem.playout.start || | |
| 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: number) { | |
| 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() { | |
| const assetList = getActiveInterstitial()?.event.assetList; | |
| if (assetList) { | |
| return assetList.map((asset) => c.getAssetPlayer(asset.identifier)); | |
| } | |
| return []; | |
| }, | |
| get playingIndex() { | |
| const interstitial = getActiveInterstitial()?.event; | |
| if (interstitial && c.effectivePlayingAsset) { | |
| return interstitial.findAssetIndex(c.effectivePlayingAsset); | |
| } | |
| return -1; | |
| }, | |
| get scheduleItem() { | |
| return getActiveInterstitial(); | |
| }, | |
| }; | |
| return (this.manager = { | |
| get events() { | |
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | |
| return c.schedule?.events?.slice(0) || []; | |
| }, | |
| get schedule() { | |
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | |
| return c.schedule?.items?.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: number) { | |
| seekTo(time, 'primary'); | |
| }, | |
| get duration() { | |
| return getMappedDuration('primary'); | |
| }, | |
| get seekableStart() { | |
| return c.primaryDetails?.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: number) { | |
| seekTo(time, 'integrated'); | |
| }, | |
| get duration() { | |
| return getMappedDuration('integrated'); | |
| }, | |
| get seekableStart() { | |
| return findMappedTime( | |
| c.primaryDetails?.fragmentStart || 0, | |
| 'integrated', | |
| ); | |
| }, | |
| }, | |
| skip: () => { | |
| const item = c.effectivePlayingItem; | |
| const event = 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 | |
| private get effectivePlayingItem(): InterstitialScheduleItem | null { | |
| return this.waitingItem || this.playingItem || this.endedItem; | |
| } | |
| private get effectivePlayingAsset(): InterstitialAssetItem | null { | |
| return this.playingAsset || this.endedAsset; | |
| } | |
| private get playingLastItem(): boolean { | |
| const playingItem = this.playingItem; | |
| const items = this.schedule?.items; | |
| if (!this.playbackStarted || !playingItem || !items) { | |
| return false; | |
| } | |
| return this.findItemIndex(playingItem) === items.length - 1; | |
| } | |
| private get playbackStarted(): boolean { | |
| return this.effectivePlayingItem !== null; | |
| } | |
| // Media getters and event callbacks | |
| private get currentTime(): number | undefined { | |
| 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?.event?.appendInPlace) { | |
| // Observe detached media currentTime when appending in place | |
| media = this.primaryMedia; | |
| } | |
| const currentTime = media?.currentTime; | |
| if (currentTime === undefined || !Number.isFinite(currentTime)) { | |
| return undefined; | |
| } | |
| return currentTime; | |
| } | |
| private get primaryMedia(): HTMLMediaElement | null { | |
| return this.media || this.detachedData?.media || null; | |
| } | |
| private isInterstitial( | |
| item: InterstitialScheduleItem | null | undefined, | |
| ): item is InterstitialScheduleEventItem { | |
| return !!item?.event; | |
| } | |
| private retreiveMediaSource( | |
| assetId: InterstitialAssetId, | |
| toSegment: InterstitialScheduleItem | null, | |
| ) { | |
| const player = this.getAssetPlayer(assetId); | |
| if (player) { | |
| this.transferMediaFromPlayer(player, toSegment); | |
| } | |
| } | |
| private transferMediaFromPlayer( | |
| player: HlsAssetPlayer, | |
| toSegment: InterstitialScheduleItem | null | undefined, | |
| ) { | |
| 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 ||= !playerMedia.paused; | |
| } | |
| } | |
| private transferMediaTo( | |
| player: Hls | HlsAssetPlayer, | |
| media: HTMLMediaElement, | |
| ) { | |
| if (player.media === media) { | |
| return; | |
| } | |
| let attachMediaSourceData: MediaAttachingData | null = null; | |
| const primaryPlayer = this.hls; | |
| const isAssetPlayer = player !== primaryPlayer; | |
| const appendInPlace = | |
| isAssetPlayer && (player as HlsAssetPlayer).interstitial.appendInPlace; | |
| const detachedMediaSource = this.detachedData?.mediaSource; | |
| let logFromSource: string; | |
| 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 as boolean) { | |
| this.warn( | |
| `Could not change append strategy for queued assets ${interstitial}`, | |
| ); | |
| } | |
| } | |
| }); | |
| } | |
| this.hls.detachMedia(); | |
| this.detachedData = { media }; | |
| } | |
| } | |
| const transferring = | |
| attachMediaSourceData && | |
| 'mediaSource' in attachMediaSourceData && | |
| attachMediaSourceData.mediaSource?.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 as HlsAssetPlayer).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); | |
| } | |
| private onPlay = () => { | |
| this.shouldPlay = true; | |
| }; | |
| private onPause = () => { | |
| this.shouldPlay = false; | |
| }; | |
| private 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; | |
| 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 | |
| ) { | |
| 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?.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 | |
| ) { | |
| if (playingItem.event?.appendInPlace) { | |
| // Return SourceBuffer(s) to primary player and flush | |
| this.clearAssetPlayers(playingItem.event, playingItem); | |
| this.flushFrontBuffer(currentTime); | |
| } | |
| this.setScheduleToAssetAtTime(currentTime, playingAsset); | |
| } | |
| }; | |
| private onInterstitialCueEnter() { | |
| this.onTimeupdate(); | |
| } | |
| private onTimeupdate = () => { | |
| const currentTime = this.currentTime; | |
| if (currentTime === undefined || this.playbackDisabled) { | |
| return; | |
| } | |
| // 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); | |
| } | |
| }; | |
| // Scheduling methods | |
| private checkStart() { | |
| const schedule = this.schedule; | |
| const interstitialEvents = 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.log(timelineMessage('checkStart', startPosition)); | |
| this.timelinePos = startPosition; | |
| if (interstitialEvents.length && interstitialEvents[0].cue.pre) { | |
| const index = schedule.findEventIndex(interstitialEvents[0].identifier); | |
| this.setSchedulePosition(index); | |
| } else if (startPosition >= 0 || !this.primaryLive) { | |
| const start = (this.timelinePos = | |
| startPosition > 0 ? startPosition : 0); | |
| const index = schedule.findItemIndexAtTime(start); | |
| this.setSchedulePosition(index); | |
| } | |
| } else if (effectivePlayingItem && !this.playingItem) { | |
| const index = schedule.findItemIndex(effectivePlayingItem); | |
| this.setSchedulePosition(index); | |
| } | |
| } | |
| private advanceAssetBuffering( | |
| item: InterstitialScheduleEventItem, | |
| assetItem: InterstitialAssetItem, | |
| ) { | |
| 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) { | |
| const nextItem = this.schedule.items?.[this.findItemIndex(item) + 1]; | |
| if (nextItem) { | |
| this.bufferedToItem(nextItem); | |
| } | |
| } | |
| } | |
| private advanceAfterAssetEnded( | |
| interstitial: InterstitialEvent, | |
| index: number, | |
| assetListIndex: number, | |
| ) { | |
| const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); | |
| if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { | |
| // Advance to next asset list item | |
| if (interstitial.appendInPlace) { | |
| const assetItem = interstitial.assetList[nextAssetIndex] as | |
| | InterstitialAssetItem | |
| | undefined; | |
| 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); | |
| } | |
| } | |
| } | |
| private setScheduleToAssetAtTime( | |
| time: number, | |
| playingAsset: InterstitialAssetItem, | |
| ) { | |
| 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); | |
| } | |
| } | |
| private setSchedulePosition(index: number, assetListIndex?: number) { | |
| const scheduleItems = this.schedule?.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?.identifier; | |
| const player = assetId ? this.getAssetPlayer(assetId) : null; | |
| if ( | |
| player && | |
| assetId && | |
| (!this.eventItemsMatch(currentItem, scheduledItem) || | |
| (assetListIndex !== undefined && | |
| assetId !== interstitial.assetList[assetListIndex].identifier)) | |
| ) { | |
| 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.detachedData?.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) { | |
| // Remove interstitial with CUE attribute value of ONCE after it has played | |
| this.updateSchedule(); | |
| const updatedScheduleItems = this.schedule?.items; | |
| if (scheduledItem && updatedScheduleItems) { | |
| const updatedIndex = this.findItemIndex(scheduledItem); | |
| this.advanceSchedule( | |
| updatedIndex, | |
| updatedScheduleItems, | |
| assetListIndex, | |
| currentItem, | |
| playingLastItem, | |
| ); | |
| } | |
| return; | |
| } | |
| } | |
| } | |
| this.advanceSchedule( | |
| index, | |
| scheduleItems, | |
| assetListIndex, | |
| currentItem, | |
| playingLastItem, | |
| ); | |
| } | |
| private advanceSchedule( | |
| index: number, | |
| scheduleItems: InterstitialScheduleItem[], | |
| assetListIndex: number | undefined, | |
| currentItem: InterstitialScheduleItem | null, | |
| playedLastItem: boolean, | |
| ) { | |
| 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] as | |
| | InterstitialAssetItem | |
| | undefined; | |
| 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); | |
| } | |
| } | |
| } | |
| private get playbackDisabled(): boolean { | |
| return this.hls.config.enableInterstitialPlayback === false; | |
| } | |
| private get primaryDetails(): LevelDetails | undefined { | |
| return this.mediaSelection?.main.details; | |
| } | |
| private get primaryLive(): boolean { | |
| return !!this.primaryDetails?.live; | |
| } | |
| private resumePrimary( | |
| scheduledItem: InterstitialSchedulePrimaryItem, | |
| index: number, | |
| fromItem: InterstitialScheduleItem | null, | |
| ) { | |
| this.playingItem = scheduledItem; | |
| this.playingAsset = this.endedAsset = null; | |
| this.waitingItem = this.endedItem = null; | |
| this.bufferedToItem(scheduledItem); | |
| this.log(`resuming ${segmentToString(scheduledItem)}`); | |
| if (!this.detachedData?.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.schedule?.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(); | |
| } | |
| private getPrimaryResumption( | |
| scheduledItem: InterstitialSchedulePrimaryItem, | |
| index: number, | |
| ): number { | |
| 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; | |
| } | |
| private isAssetBuffered(asset: InterstitialAssetItem): boolean { | |
| const player = this.getAssetPlayer(asset.identifier); | |
| if (player?.hls) { | |
| return player.hls.bufferedToEnd; | |
| } | |
| const bufferInfo = BufferHelper.bufferInfo( | |
| this.primaryMedia, | |
| this.timelinePos, | |
| 0, | |
| ); | |
| return bufferInfo.end + 1 >= asset.timelineStart + (asset.duration || 0); | |
| } | |
| private attachPrimary( | |
| timelinePos: number, | |
| item: InterstitialSchedulePrimaryItem | null, | |
| skipSeekToStartPosition?: boolean, | |
| ) { | |
| 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); | |
| } | |
| } | |
| private startLoadingPrimaryAt( | |
| timelinePos: number, | |
| skipSeekToStartPosition?: boolean, | |
| ) { | |
| const hls = this.hls; | |
| if ( | |
| !hls.loadingEnabled || | |
| !hls.media || | |
| Math.abs( | |
| (hls.mainForwardBufferInfo?.start || hls.media.currentTime) - | |
| timelinePos, | |
| ) > 0.5 | |
| ) { | |
| hls.startLoad(timelinePos, skipSeekToStartPosition); | |
| } else if (!hls.bufferingEnabled) { | |
| hls.resumeBuffering(); | |
| } | |
| } | |
| // HLS.js event callbacks | |
| private onManifestLoading() { | |
| this.stopLoad(); | |
| this.schedule?.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); | |
| } | |
| private onLevelUpdated(event: Events.LEVEL_UPDATED, data: LevelUpdatedData) { | |
| if (data.level === -1 || !this.schedule) { | |
| // level was removed | |
| return; | |
| } | |
| const main = this.hls.levels[data.level]; | |
| if (!main.details) { | |
| return; | |
| } | |
| const currentSelection = { | |
| ...(this.mediaSelection || this.altSelection), | |
| main, | |
| }; | |
| this.mediaSelection = currentSelection; | |
| this.schedule.parseInterstitialDateRanges( | |
| currentSelection, | |
| this.hls.config.interstitialAppendInPlace, | |
| ); | |
| if (!this.effectivePlayingItem && this.schedule.items) { | |
| this.checkStart(); | |
| } | |
| } | |
| private onAudioTrackUpdated( | |
| event: Events.AUDIO_TRACK_UPDATED, | |
| data: AudioTrackUpdatedData, | |
| ) { | |
| const audio = this.hls.audioTracks[data.id]; | |
| const previousSelection = this.mediaSelection; | |
| if (!previousSelection) { | |
| this.altSelection = { ...this.altSelection, audio }; | |
| return; | |
| } | |
| const currentSelection = { ...previousSelection, audio }; | |
| this.mediaSelection = currentSelection; | |
| } | |
| private onSubtitleTrackUpdated( | |
| event: Events.SUBTITLE_TRACK_UPDATED, | |
| data: SubtitleTrackUpdatedData, | |
| ) { | |
| const subtitles = this.hls.subtitleTracks[data.id]; | |
| const previousSelection = this.mediaSelection; | |
| if (!previousSelection) { | |
| this.altSelection = { ...this.altSelection, subtitles }; | |
| return; | |
| } | |
| const currentSelection = { ...previousSelection, subtitles }; | |
| this.mediaSelection = currentSelection; | |
| } | |
| private onAudioTrackSwitching( | |
| event: Events.AUDIO_TRACK_SWITCHING, | |
| data: AudioTrackSwitchingData, | |
| ) { | |
| const audioOption = getBasicSelectionOption(data); | |
| this.playerQueue.forEach( | |
| ({ hls }) => | |
| hls && (hls.setAudioOption(data) || hls.setAudioOption(audioOption)), | |
| ); | |
| } | |
| private onSubtitleTrackSwitch( | |
| event: Events.SUBTITLE_TRACK_SWITCH, | |
| data: SubtitleTrackSwitchData, | |
| ) { | |
| const subtitleOption = getBasicSelectionOption(data); | |
| this.playerQueue.forEach( | |
| ({ hls }) => | |
| hls && | |
| (hls.setSubtitleOption(data) || | |
| (data.id !== -1 && hls.setSubtitleOption(subtitleOption))), | |
| ); | |
| } | |
| private onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData) { | |
| const requiredTracks = data.tracks; | |
| if (requiredTracks) { | |
| this.requiredTracks = requiredTracks; | |
| } | |
| } | |
| private onBufferAppended( | |
| event: Events.BUFFER_APPENDED, | |
| data: BufferAppendedData, | |
| ) { | |
| this.checkBuffer(); | |
| } | |
| private onBufferFlushed( | |
| event: Events.BUFFER_FLUSHED, | |
| data: BufferFlushedData, | |
| ) { | |
| const playingItem = this.playingItem; | |
| if ( | |
| playingItem && | |
| !this.itemsMatch(playingItem, this.bufferingItem) && | |
| !this.isInterstitial(playingItem) | |
| ) { | |
| const timelinePos = this.timelinePos; | |
| this.bufferedPos = timelinePos; | |
| this.checkBuffer(); | |
| } | |
| } | |
| private onBufferedToEnd(event: Events.BUFFERED_TO_END) { | |
| 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) { | |
| const scheduleIndex = this.schedule.findEventIndex( | |
| interstitial.identifier, | |
| ); | |
| const item = this.schedule.items?.[scheduleIndex]; | |
| if ( | |
| this.isInterstitial(item) && | |
| this.eventItemsMatch(item, this.bufferingItem) | |
| ) { | |
| this.bufferedToItem(item, 0); | |
| } | |
| break; | |
| } | |
| } | |
| this.bufferedPos = Number.MAX_VALUE; | |
| } | |
| } | |
| private onMediaEnded(event: Events.MEDIA_ENDED) { | |
| const playingItem = this.playingItem; | |
| if (!this.playingLastItem && playingItem) { | |
| const playingIndex = this.findItemIndex(playingItem); | |
| this.setSchedulePosition(playingIndex + 1); | |
| } else { | |
| this.shouldPlay = false; | |
| } | |
| } | |
| // Schedule update callback | |
| private onScheduleUpdate = ( | |
| removedInterstitials: InterstitialEvent[], | |
| previousItems: InterstitialScheduleItem[] | null, | |
| ) => { | |
| 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: InterstitialScheduleItem | null = null; | |
| let updatedBufferingItem: InterstitialScheduleItem | null = 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(); | |
| } | |
| }; | |
| private updateItem<T extends InterstitialScheduleItem>( | |
| previousItem: T | null, | |
| time?: number, | |
| ): T | null { | |
| // find item in this.schedule.items; | |
| const items = this.schedule?.items; | |
| if (previousItem && items) { | |
| const index = this.findItemIndex(previousItem, time); | |
| return (items[index] as T | undefined) || null; | |
| } | |
| return null; | |
| } | |
| private trimInPlace( | |
| updatedItem: InterstitialScheduleItem | null, | |
| itemBeforeUpdate: InterstitialScheduleItem, | |
| ) { | |
| 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); | |
| } | |
| } | |
| } | |
| private itemsMatch( | |
| a: InterstitialScheduleItem, | |
| b: InterstitialScheduleItem | null | undefined, | |
| ): boolean { | |
| return ( | |
| !!b && | |
| (a === b || | |
| (a.event && b.event && this.eventItemsMatch(a, b)) || | |
| (!a.event && | |
| !b.event && | |
| this.findItemIndex(a) === this.findItemIndex(b))) | |
| ); | |
| } | |
| private eventItemsMatch( | |
| a: InterstitialScheduleEventItem, | |
| b: InterstitialScheduleItem | null | undefined, | |
| ): boolean { | |
| return !!b && (a === b || a.event.identifier === b.event?.identifier); | |
| } | |
| private findItemIndex( | |
| item: InterstitialScheduleItem | null, | |
| time?: number, | |
| ): number { | |
| return item && this.schedule ? this.schedule.findItemIndex(item, time) : -1; | |
| } | |
| private updateSchedule(forceUpdate: boolean = false) { | |
| const mediaSelection = this.mediaSelection; | |
| if (!mediaSelection) { | |
| return; | |
| } | |
| this.schedule?.updateSchedule(mediaSelection, [], forceUpdate); | |
| } | |
| // Schedule buffer control | |
| private checkBuffer(starved?: boolean) { | |
| const items = this.schedule?.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 ||= bufferInfo.len < 1; | |
| this.updateBufferedPos(bufferInfo.end, items, starved); | |
| } | |
| private updateBufferedPos( | |
| bufferEnd: number, | |
| items: InterstitialScheduleItem[], | |
| bufferIsEmpty?: boolean, | |
| ) { | |
| 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) { | |
| 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.event?.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]); | |
| } | |
| } | |
| } | |
| private assetsBuffered( | |
| item: InterstitialScheduleEventItem, | |
| media: HTMLMediaElement | null, | |
| ): boolean { | |
| 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?.bufferedInPlaceToEnd(media); | |
| }); | |
| } | |
| private setBufferingItem( | |
| item: InterstitialScheduleItem, | |
| ): InterstitialScheduleItem | null { | |
| 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; | |
| } | |
| private bufferedToItem( | |
| item: InterstitialScheduleItem, | |
| assetListIndex: number = 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); | |
| } | |
| } | |
| } | |
| private preloadPrimary(item: InterstitialSchedulePrimaryItem) { | |
| const index = this.findItemIndex(item); | |
| const timelinePos = this.getPrimaryResumption(item, index); | |
| this.startLoadingPrimaryAt(timelinePos); | |
| } | |
| private bufferedToEvent( | |
| item: InterstitialScheduleEventItem, | |
| assetListIndex: number, | |
| ) { | |
| 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?.interstitial.appendInPlace) { | |
| const media = this.primaryMedia; | |
| if (media) { | |
| this.bufferAssetPlayer(player, media); | |
| } | |
| } | |
| } | |
| } | |
| private preloadAssets( | |
| interstitial: InterstitialEvent, | |
| assetListIndex: number, | |
| ): HlsAssetPlayer | null { | |
| 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) { | |
| const playingItem = this.playingItem; | |
| if ( | |
| !this.isInterstitial(playingItem) && | |
| playingItem?.nextEvent?.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 as InterstitialEventWithAssetList, | |
| 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; | |
| } | |
| private flushFrontBuffer(startOffset: number) { | |
| // 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: SourceBufferName) => { | |
| this.hls.trigger(Events.BUFFER_FLUSHING, { | |
| startOffset, | |
| endOffset: Infinity, | |
| type, | |
| }); | |
| }); | |
| } | |
| // Interstitial Asset Player control | |
| private getAssetPlayerQueueIndex(assetId: InterstitialAssetId): number { | |
| const playerQueue = this.playerQueue; | |
| for (let i = 0; i < playerQueue.length; i++) { | |
| if (assetId === playerQueue[i].assetId) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| private getAssetPlayer(assetId: InterstitialAssetId): HlsAssetPlayer | null { | |
| const index = this.getAssetPlayerQueueIndex(assetId); | |
| return this.playerQueue[index] || null; | |
| } | |
| private getBufferingPlayer(): HlsAssetPlayer | null { | |
| const { playerQueue, primaryMedia } = this; | |
| if (primaryMedia) { | |
| for (let i = 0; i < playerQueue.length; i++) { | |
| if (playerQueue[i].media === primaryMedia) { | |
| return playerQueue[i]; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| private createAsset( | |
| interstitial: InterstitialEvent, | |
| assetListIndex: number, | |
| startOffset: number, | |
| timelineStart: number, | |
| duration: number, | |
| uri: string, | |
| ): HlsAssetPlayer { | |
| const assetItem: InterstitialAssetItem = { | |
| parentIdentifier: interstitial.identifier, | |
| identifier: generateAssetIdentifier(interstitial, uri, assetListIndex), | |
| duration, | |
| startOffset, | |
| timelineStart, | |
| uri, | |
| }; | |
| return this.createAssetPlayer(interstitial, assetItem, assetListIndex); | |
| } | |
| private createAssetPlayer( | |
| interstitial: InterstitialEvent, | |
| assetItem: InterstitialAssetItem, | |
| assetListIndex: number, | |
| ): HlsAssetPlayer { | |
| 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 = Object.assign({}, 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: HlsAssetPlayerConfig = { | |
| ...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 as MediaPlaylist | undefined) || | |
| userConfig.audioPreference, | |
| subtitlePreference: | |
| (selectedSubtitle as MediaPlaylist | undefined) || | |
| 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?.sessionId && cmcd.contentId) { | |
| playerConfig.cmcd = Object.assign({}, 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: LevelDetails) => { | |
| if (details.live) { | |
| const error = new Error( | |
| `Interstitials MUST be VOD assets ${interstitial}`, | |
| ); | |
| const errorData: ErrorData = { | |
| fatal: true, | |
| type: ErrorTypes.OTHER_ERROR, | |
| details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, | |
| error, | |
| }; | |
| const scheduleIndex = | |
| this.schedule?.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: Events.BUFFER_CODECS, | |
| data: BufferCodecsData, | |
| ) => { | |
| 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 = () => { | |
| 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.items?.[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: Events.ERROR, data: ErrorData) => { | |
| if (!this.schedule) { | |
| return; | |
| } | |
| const inQueuPlayer = this.getAssetPlayer(assetId); | |
| if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { | |
| if (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: 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; | |
| } | |
| private clearInterstitial( | |
| interstitial: InterstitialEvent, | |
| toSegment: InterstitialScheduleItem | null, | |
| ) { | |
| this.clearAssetPlayers(interstitial, toSegment); | |
| // Remove asset list and resolved duration | |
| interstitial.reset(); | |
| } | |
| private clearAssetPlayers( | |
| interstitial: InterstitialEvent, | |
| toSegment: InterstitialScheduleItem | null, | |
| ) { | |
| interstitial.assetList.forEach((asset) => { | |
| this.clearAssetPlayer(asset.identifier, toSegment); | |
| }); | |
| } | |
| private resetAssetPlayer(assetId: InterstitialAssetId) { | |
| // 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(); | |
| } | |
| } | |
| private clearAssetPlayer( | |
| assetId: InterstitialAssetId, | |
| toSegment: InterstitialScheduleItem | null, | |
| ) { | |
| 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(); | |
| } | |
| } | |
| private emptyPlayerQueue() { | |
| let player: HlsAssetPlayer | undefined; | |
| while ((player = this.playerQueue.pop())) { | |
| player.destroy(); | |
| } | |
| this.playerQueue = []; | |
| } | |
| private startAssetPlayer( | |
| player: HlsAssetPlayer, | |
| assetListIndex: number, | |
| scheduleItems: InterstitialScheduleItem[], | |
| scheduleIndex: number, | |
| media: HTMLMediaElement, | |
| ) { | |
| 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); | |
| } | |
| private bufferAssetPlayer(player: HlsAssetPlayer, media: HTMLMediaElement) { | |
| if (!this.schedule) { | |
| return; | |
| } | |
| const { interstitial, assetItem } = player; | |
| const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); | |
| const item = this.schedule.items?.[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?.interstitial.appendInPlace === false | |
| ) { | |
| // Media is detached and not available to append in place | |
| return; | |
| } | |
| const activeTracks = | |
| bufferingPlayer?.tracks || | |
| this.detachedData?.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: 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); | |
| } | |
| private handleInPlaceStall(interstitial: InterstitialEvent) { | |
| 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] as | |
| | InterstitialAssetItem | |
| | undefined; | |
| 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, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| private advanceInPlace(time: number) { | |
| const media = this.primaryMedia; | |
| if (media && media.currentTime < time) { | |
| media.currentTime = time; | |
| } | |
| } | |
| private handleAssetItemError( | |
| data: ErrorData, | |
| interstitial: InterstitialEvent, | |
| scheduleIndex: number, | |
| assetListIndex: number, | |
| errorMessage: string, | |
| ) { | |
| if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { | |
| return; | |
| } | |
| const assetItem = (interstitial.assetList[assetListIndex] || | |
| null) as InterstitialAssetItem | null; | |
| this.warn( | |
| `INTERSTITIAL_ASSET_ERROR ${assetItem ? eventAssetToString(assetItem) : assetItem} ${data.error}`, | |
| ); | |
| if (!this.schedule) { | |
| return; | |
| } | |
| const assetId = assetItem?.identifier || ''; | |
| const playerIndex = this.getAssetPlayerQueueIndex(assetId); | |
| const player = this.playerQueue[playerIndex] || null; | |
| const items = this.schedule.items; | |
| const interstitialAssetError = Object.assign({}, 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); | |
| } | |
| } | |
| private primaryFallback(interstitial: InterstitialEvent) { | |
| // 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 | |
| private onAssetListLoaded( | |
| event: Events.ASSET_LIST_LOADED, | |
| data: AssetListLoadedData, | |
| ) { | |
| const interstitial = data.event; | |
| const interstitialId = interstitial.identifier; | |
| const assets = data.assetListResponse.ASSETS; | |
| if (!this.schedule?.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?.event.identifier === interstitialId; | |
| // Update schedule now that asset.DURATION(s) are parsed | |
| this.updateSchedule(); | |
| const bufferingEvent = this.bufferingItem?.event; | |
| // If buffer reached Interstitial, start buffering first asset | |
| if (waitingForItem) { | |
| // Advance schedule when waiting for asset list data to play | |
| const scheduleIndex = this.schedule.findEventIndex(interstitialId); | |
| const item = this.schedule.items?.[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?.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(); | |
| } | |
| } | |
| } | |
| } | |
| private onError(event: Events.ERROR, data: ErrorData) { | |
| 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; | |
| } | |
| } | |
| } | |
| } | |
Xet Storage Details
- Size:
- 95.8 kB
- Xet hash:
- f5ba3b328115f66769cb742abd54ac6b0ecd3dfc87e9ab51ce4643c977e051bb
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.