Buckets:
ktongue/docker_container / simsite /frontend /node_modules /hls.js /src /controller /gap-controller.ts
| import { State } from './base-stream-controller'; | |
| import { ErrorDetails, ErrorTypes } from '../errors'; | |
| import { Events } from '../events'; | |
| import TaskLoop from '../task-loop'; | |
| import { PlaylistLevelType } from '../types/loader'; | |
| import { BufferHelper } from '../utils/buffer-helper'; | |
| import { | |
| addEventListener, | |
| removeEventListener, | |
| } from '../utils/event-listener-helper'; | |
| import { stringify } from '../utils/safe-json-stringify'; | |
| import type { InFlightData } from './base-stream-controller'; | |
| import type { InFlightFragments } from '../hls'; | |
| import type Hls from '../hls'; | |
| import type { FragmentTracker } from './fragment-tracker'; | |
| import type { Fragment, MediaFragment, Part } from '../loader/fragment'; | |
| import type { SourceBufferName } from '../types/buffer'; | |
| import type { | |
| BufferAppendedData, | |
| MediaAttachedData, | |
| MediaDetachingData, | |
| } from '../types/events'; | |
| import type { ErrorData } from '../types/events'; | |
| import type { BufferInfo } from '../utils/buffer-helper'; | |
| export const MAX_START_GAP_JUMP = 2.0; | |
| export const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1; | |
| export const SKIP_BUFFER_RANGE_START = 0.05; | |
| const TICK_INTERVAL = 100; | |
| export default class GapController extends TaskLoop { | |
| private hls: Hls | null; | |
| private fragmentTracker: FragmentTracker | null; | |
| private media: HTMLMediaElement | null = null; | |
| private mediaSource?: MediaSource; | |
| private nudgeRetry: number = 0; | |
| private stallReported: boolean = false; | |
| private stalled: number | null = null; | |
| private moved: boolean = false; | |
| private seeking: boolean = false; | |
| private buffered: Partial<Record<SourceBufferName, TimeRanges>> = {}; | |
| private lastCurrentTime: number = 0; | |
| public ended: number = 0; | |
| public waiting: number = 0; | |
| constructor(hls: Hls, fragmentTracker: FragmentTracker) { | |
| super('gap-controller', hls.logger); | |
| this.hls = hls; | |
| this.fragmentTracker = fragmentTracker; | |
| this.registerListeners(); | |
| } | |
| private registerListeners() { | |
| const { hls } = this; | |
| if (hls) { | |
| hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); | |
| hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); | |
| hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); | |
| } | |
| } | |
| private unregisterListeners() { | |
| const { hls } = this; | |
| if (hls) { | |
| hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); | |
| hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); | |
| hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); | |
| } | |
| } | |
| public destroy() { | |
| super.destroy(); | |
| this.unregisterListeners(); | |
| this.media = this.hls = this.fragmentTracker = null; | |
| this.mediaSource = undefined; | |
| } | |
| private onMediaAttached( | |
| event: Events.MEDIA_ATTACHED, | |
| data: MediaAttachedData, | |
| ) { | |
| this.setInterval(TICK_INTERVAL); | |
| this.mediaSource = data.mediaSource; | |
| const media = (this.media = data.media); | |
| addEventListener(media, 'playing', this.onMediaPlaying); | |
| addEventListener(media, 'waiting', this.onMediaWaiting); | |
| addEventListener(media, 'ended', this.onMediaEnded); | |
| } | |
| private onMediaDetaching( | |
| event: Events.MEDIA_DETACHING, | |
| data: MediaDetachingData, | |
| ) { | |
| this.clearInterval(); | |
| const { media } = this; | |
| if (media) { | |
| removeEventListener(media, 'playing', this.onMediaPlaying); | |
| removeEventListener(media, 'waiting', this.onMediaWaiting); | |
| removeEventListener(media, 'ended', this.onMediaEnded); | |
| this.media = null; | |
| } | |
| this.mediaSource = undefined; | |
| } | |
| private onBufferAppended( | |
| event: Events.BUFFER_APPENDED, | |
| data: BufferAppendedData, | |
| ) { | |
| this.buffered = data.timeRanges; | |
| } | |
| private onMediaPlaying = () => { | |
| this.ended = 0; | |
| this.waiting = 0; | |
| }; | |
| private onMediaWaiting = () => { | |
| if (this.media?.seeking) { | |
| return; | |
| } | |
| this.waiting = self.performance.now(); | |
| this.tick(); | |
| }; | |
| private onMediaEnded = () => { | |
| if (this.hls) { | |
| // ended is set when triggering MEDIA_ENDED so that we do not trigger it again on stall or on tick with media.ended | |
| this.ended = this.media?.currentTime || 1; | |
| this.hls.trigger(Events.MEDIA_ENDED, { | |
| stalled: false, | |
| }); | |
| } | |
| }; | |
| public get hasBuffered(): boolean { | |
| return Object.keys(this.buffered).length > 0; | |
| } | |
| public tick() { | |
| if (!this.media?.readyState || !this.hasBuffered) { | |
| return; | |
| } | |
| const currentTime = this.media.currentTime; | |
| this.poll(currentTime, this.lastCurrentTime); | |
| this.lastCurrentTime = currentTime; | |
| } | |
| /** | |
| * Checks if the playhead is stuck within a gap, and if so, attempts to free it. | |
| * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range). | |
| * | |
| * @param lastCurrentTime - Previously read playhead position | |
| */ | |
| public poll(currentTime: number, lastCurrentTime: number) { | |
| const config = this.hls?.config; | |
| if (!config) { | |
| return; | |
| } | |
| const media = this.media; | |
| if (!media) { | |
| return; | |
| } | |
| const { seeking } = media; | |
| const seeked = this.seeking && !seeking; | |
| const beginSeek = !this.seeking && seeking; | |
| const pausedEndedOrHalted = | |
| (media.paused && !seeking) || media.ended || media.playbackRate === 0; | |
| this.seeking = seeking; | |
| // The playhead is moving, no-op | |
| if (currentTime !== lastCurrentTime) { | |
| if (lastCurrentTime) { | |
| this.ended = 0; | |
| } | |
| this.moved = true; | |
| if (!seeking) { | |
| this.nudgeRetry = 0; | |
| // When crossing between buffered video time ranges, but not audio, flush pipeline with seek (Chrome) | |
| if ( | |
| config.nudgeOnVideoHole && | |
| !pausedEndedOrHalted && | |
| currentTime > lastCurrentTime | |
| ) { | |
| this.nudgeOnVideoHole(currentTime, lastCurrentTime); | |
| } | |
| } | |
| if (this.waiting === 0) { | |
| this.stallResolved(currentTime); | |
| } | |
| return; | |
| } | |
| // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek | |
| if (beginSeek || seeked) { | |
| if (seeked) { | |
| this.stallResolved(currentTime); | |
| } | |
| return; | |
| } | |
| // The playhead should not be moving | |
| if (pausedEndedOrHalted) { | |
| this.nudgeRetry = 0; | |
| this.stallResolved(currentTime); | |
| // Fire MEDIA_ENDED to workaround event not being dispatched by browser | |
| if (!this.ended && media.ended && this.hls) { | |
| this.ended = currentTime || 1; | |
| this.hls.trigger(Events.MEDIA_ENDED, { | |
| stalled: false, | |
| }); | |
| } | |
| return; | |
| } | |
| if (!BufferHelper.getBuffered(media).length) { | |
| this.nudgeRetry = 0; | |
| return; | |
| } | |
| // Resolve stalls at buffer holes using the main buffer, whose ranges are the intersections of the A/V sourcebuffers | |
| const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); | |
| const nextStart = bufferInfo.nextStart || 0; | |
| const fragmentTracker = this.fragmentTracker; | |
| if (seeking && fragmentTracker && this.hls) { | |
| // Is there a fragment loading/parsing/appending before currentTime? | |
| const inFlightDependency = getInFlightDependency( | |
| this.hls.inFlightFragments, | |
| currentTime, | |
| ); | |
| // Waiting for seeking in a buffered range to complete | |
| const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP; | |
| // Next buffered range is too far ahead to jump to while still seeking | |
| const noBufferHole = | |
| !nextStart || | |
| inFlightDependency || | |
| (nextStart - currentTime > MAX_START_GAP_JUMP && | |
| !fragmentTracker.getPartialFragment(currentTime)); | |
| if (hasEnoughBuffer || noBufferHole) { | |
| return; | |
| } | |
| // Reset moved state when seeking to a point in or before a gap/hole | |
| this.moved = false; | |
| } | |
| // Skip start gaps if we haven't played, but the last poll detected the start of a stall | |
| // The addition poll gives the browser a chance to jump the gap for us | |
| const levelDetails = this.hls?.latestLevelDetails; | |
| if (!this.moved && this.stalled !== null && fragmentTracker) { | |
| // There is no playable buffer (seeked, waiting for buffer) | |
| const isBuffered = bufferInfo.len > 0; | |
| if (!isBuffered && !nextStart) { | |
| return; | |
| } | |
| // Jump start gaps within jump threshold | |
| const startJump = | |
| Math.max(nextStart, bufferInfo.start || 0) - currentTime; | |
| // When joining a live stream with audio tracks, account for live playlist window sliding by allowing | |
| // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment | |
| // that begins over 1 target duration after the video start position. | |
| const isLive = !!levelDetails?.live; | |
| const maxStartGapJump = isLive | |
| ? levelDetails!.targetduration * 2 | |
| : MAX_START_GAP_JUMP; | |
| const appended = appendedFragAtPosition(currentTime, fragmentTracker); | |
| if (startJump > 0 && (startJump <= maxStartGapJump || appended)) { | |
| if (!media.paused) { | |
| this._trySkipBufferHole(appended); | |
| } | |
| return; | |
| } | |
| } | |
| // Start tracking stall time | |
| const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs; | |
| const tnow = self.performance.now(); | |
| const tWaiting = this.waiting; | |
| let stalled = this.stalled; | |
| if (stalled === null) { | |
| // Use time of recent "waiting" event | |
| if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) { | |
| stalled = this.stalled = tWaiting; | |
| } else { | |
| this.stalled = tnow; | |
| return; | |
| } | |
| } | |
| const stalledDuration = tnow - stalled; | |
| if ( | |
| !seeking && | |
| (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && | |
| this.hls | |
| ) { | |
| // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream | |
| if ( | |
| this.mediaSource?.readyState === 'ended' && | |
| !levelDetails?.live && | |
| Math.abs(currentTime - (levelDetails?.edge || 0)) < 1 | |
| ) { | |
| if (this.ended) { | |
| return; | |
| } | |
| this.ended = currentTime || 1; | |
| this.hls.trigger(Events.MEDIA_ENDED, { | |
| stalled: true, | |
| }); | |
| return; | |
| } | |
| // Report stalling after trying to fix | |
| this._reportStall(bufferInfo); | |
| if (!this.media || (!this.hls as any)) { | |
| return; | |
| } | |
| } | |
| const bufferedWithHoles = BufferHelper.bufferInfo( | |
| media, | |
| currentTime, | |
| config.maxBufferHole, | |
| ); | |
| this._tryFixBufferStall(bufferedWithHoles, stalledDuration, currentTime); | |
| } | |
| private stallResolved(currentTime: number) { | |
| const stalled = this.stalled; | |
| if (stalled && this.hls) { | |
| this.stalled = null; | |
| // The playhead is now moving, but was previously stalled | |
| if (this.stallReported) { | |
| const stalledDuration = self.performance.now() - stalled; | |
| this.log( | |
| `playback not stuck anymore @${currentTime}, after ${Math.round( | |
| stalledDuration, | |
| )}ms`, | |
| ); | |
| this.stallReported = false; | |
| this.waiting = 0; | |
| this.hls.trigger(Events.STALL_RESOLVED, {}); | |
| } | |
| } | |
| } | |
| private nudgeOnVideoHole(currentTime: number, lastCurrentTime: number) { | |
| // Chrome will play one second past a hole in video buffered time ranges without rendering any video from the subsequent range and then stall as long as audio is buffered: | |
| // https://github.com/video-dev/hls.js/issues/5631 | |
| // https://issues.chromium.org/issues/40280613#comment10 | |
| // Detect the potential for this situation and proactively seek to flush the video pipeline once the playhead passes the start of the video hole. | |
| // When there are audio and video buffers and currentTime is past the end of the first video buffered range... | |
| const videoSourceBuffered = this.buffered.video; | |
| if ( | |
| this.hls && | |
| this.media && | |
| this.fragmentTracker && | |
| this.buffered.audio?.length && | |
| videoSourceBuffered && | |
| videoSourceBuffered.length > 1 && | |
| currentTime > videoSourceBuffered.end(0) | |
| ) { | |
| // and audio is buffered at the playhead | |
| const audioBufferInfo = BufferHelper.bufferedInfo( | |
| BufferHelper.timeRangesToArray(this.buffered.audio), | |
| currentTime, | |
| 0, | |
| ); | |
| if (audioBufferInfo.len > 1 && lastCurrentTime >= audioBufferInfo.start) { | |
| const videoTimes = BufferHelper.timeRangesToArray(videoSourceBuffered); | |
| const lastBufferedIndex = BufferHelper.bufferedInfo( | |
| videoTimes, | |
| lastCurrentTime, | |
| 0, | |
| ).bufferedIndex; | |
| // nudge when crossing into another video buffered range (hole). | |
| if ( | |
| lastBufferedIndex > -1 && | |
| lastBufferedIndex < videoTimes.length - 1 | |
| ) { | |
| const bufferedIndex = BufferHelper.bufferedInfo( | |
| videoTimes, | |
| currentTime, | |
| 0, | |
| ).bufferedIndex; | |
| const holeStart = videoTimes[lastBufferedIndex].end; | |
| const holeEnd = videoTimes[lastBufferedIndex + 1].start; | |
| if ( | |
| (bufferedIndex === -1 || bufferedIndex > lastBufferedIndex) && | |
| holeEnd - holeStart < 1 && // `maxBufferHole` may be too small and setting it to 0 should not disable this feature | |
| currentTime - holeStart < 2 | |
| ) { | |
| const error = new Error( | |
| `nudging playhead to flush pipeline after video hole. currentTime: ${currentTime} hole: ${holeStart} -> ${holeEnd} buffered index: ${bufferedIndex}`, | |
| ); | |
| this.warn(error.message); | |
| // Magic number to flush the pipeline without interuption to audio playback: | |
| this.media.currentTime += 0.000001; | |
| let frag: MediaFragment | Part | null | undefined = | |
| appendedFragAtPosition(currentTime, this.fragmentTracker); | |
| if (frag && 'fragment' in frag) { | |
| frag = frag.fragment; | |
| } else if (!frag) { | |
| frag = undefined; | |
| } | |
| const bufferInfo = BufferHelper.bufferInfo( | |
| this.media, | |
| currentTime, | |
| 0, | |
| ); | |
| this.hls.trigger(Events.ERROR, { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, | |
| fatal: false, | |
| error, | |
| reason: error.message, | |
| frag, | |
| buffer: bufferInfo.len, | |
| bufferInfo, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Detects and attempts to fix known buffer stalling issues. | |
| * @param bufferInfo - The properties of the current buffer. | |
| * @param stalledDurationMs - The amount of time Hls.js has been stalling for. | |
| * @private | |
| */ | |
| private _tryFixBufferStall( | |
| bufferInfo: BufferInfo, | |
| stalledDurationMs: number, | |
| currentTime: number, | |
| ) { | |
| const { fragmentTracker, media } = this; | |
| const config = this.hls?.config; | |
| if (!media || !fragmentTracker || !config) { | |
| return; | |
| } | |
| const levelDetails = this.hls?.latestLevelDetails; | |
| const appended = appendedFragAtPosition(currentTime, fragmentTracker); | |
| if ( | |
| appended || | |
| (levelDetails?.live && currentTime < levelDetails.fragmentStart) | |
| ) { | |
| // Try to skip over the buffer hole caused by a partial fragment | |
| // This method isn't limited by the size of the gap between buffered ranges | |
| const targetTime = this._trySkipBufferHole(appended); | |
| // we return here in this case, meaning | |
| // the branch below only executes when we haven't seeked to a new position | |
| if (targetTime || !this.media) { | |
| return; | |
| } | |
| } | |
| // if we haven't had to skip over a buffer hole of a partial fragment | |
| // we may just have to "nudge" the playlist as the browser decoding/rendering engine | |
| // needs to cross some sort of threshold covering all source-buffers content | |
| // to start playing properly. | |
| const bufferedRanges = bufferInfo.buffered; | |
| const adjacentTraversal = this.adjacentTraversal(bufferInfo, currentTime); | |
| if ( | |
| ((bufferedRanges && | |
| bufferedRanges.length > 1 && | |
| bufferInfo.len > config.maxBufferHole) || | |
| (bufferInfo.nextStart && | |
| (bufferInfo.nextStart - currentTime < config.maxBufferHole || | |
| adjacentTraversal))) && | |
| (stalledDurationMs > config.highBufferWatchdogPeriod * 1000 || | |
| this.waiting) | |
| ) { | |
| this.warn('Trying to nudge playhead over buffer-hole'); | |
| // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds | |
| // We only try to jump the hole if it's under the configured size | |
| this._tryNudgeBuffer(bufferInfo); | |
| } | |
| } | |
| private adjacentTraversal(bufferInfo: BufferInfo, currentTime: number) { | |
| const fragmentTracker = this.fragmentTracker; | |
| const nextStart = bufferInfo.nextStart; | |
| if (fragmentTracker && nextStart) { | |
| const current = fragmentTracker.getFragAtPos( | |
| currentTime, | |
| PlaylistLevelType.MAIN, | |
| ); | |
| const next = fragmentTracker.getFragAtPos( | |
| nextStart, | |
| PlaylistLevelType.MAIN, | |
| ); | |
| if (current && next) { | |
| return next.sn - current.sn < 2; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period. | |
| * @param bufferLen - The playhead distance from the end of the current buffer segment. | |
| * @private | |
| */ | |
| private _reportStall(bufferInfo: BufferInfo) { | |
| const { hls, media, stallReported, stalled } = this; | |
| if (!stallReported && stalled !== null && media && hls) { | |
| // Report stalled error once | |
| this.stallReported = true; | |
| const error = new Error( | |
| `Playback stalling at @${ | |
| media.currentTime | |
| } due to low buffer (${stringify(bufferInfo)})`, | |
| ); | |
| this.warn(error.message); | |
| hls.trigger(Events.ERROR, { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.BUFFER_STALLED_ERROR, | |
| fatal: false, | |
| error, | |
| buffer: bufferInfo.len, | |
| bufferInfo, | |
| stalled: { start: stalled }, | |
| }); | |
| } | |
| } | |
| /** | |
| * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments | |
| * @param appended - The fragment or part found at the current time (where playback is stalling). | |
| * @private | |
| */ | |
| private _trySkipBufferHole(appended: MediaFragment | Part | null): number { | |
| const { fragmentTracker, media } = this; | |
| const config = this.hls?.config; | |
| if (!media || !fragmentTracker || !config) { | |
| return 0; | |
| } | |
| // Check if currentTime is between unbuffered regions of partial fragments | |
| const currentTime = media.currentTime; | |
| const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); | |
| const startTime = | |
| currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart; | |
| if (startTime && this.hls) { | |
| const bufferStarved = bufferInfo.len <= config.maxBufferHole; | |
| const waiting = | |
| bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3; | |
| const gapLength = startTime - currentTime; | |
| if (gapLength > 0 && (bufferStarved || waiting)) { | |
| // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial | |
| if (gapLength > config.maxBufferHole) { | |
| let startGap = false; | |
| if (currentTime === 0) { | |
| const startFrag = fragmentTracker.getAppendedFrag( | |
| 0, | |
| PlaylistLevelType.MAIN, | |
| ); | |
| if (startFrag && startTime < startFrag.end) { | |
| startGap = true; | |
| } | |
| } | |
| if (!startGap && appended) { | |
| // Do not seek when selected variant playlist is unloaded | |
| if (!this.hls.loadLevelObj?.details) { | |
| return 0; | |
| } | |
| // Do not seek when required fragments are inflight or appending | |
| const inFlightDependency = getInFlightDependency( | |
| this.hls.inFlightFragments, | |
| startTime, | |
| ); | |
| if (inFlightDependency) { | |
| return 0; | |
| } | |
| // Do not seek if we can't walk tracked fragments to end of gap | |
| let moreToLoad = false; | |
| let pos = appended.end; | |
| while (pos < startTime) { | |
| const provisioned = appendedFragAtPosition(pos, fragmentTracker); | |
| if (provisioned) { | |
| pos += provisioned.duration; | |
| } else { | |
| moreToLoad = true; | |
| break; | |
| } | |
| } | |
| if (moreToLoad) { | |
| return 0; | |
| } | |
| } | |
| } | |
| const targetTime = Math.max( | |
| startTime + SKIP_BUFFER_RANGE_START, | |
| currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS, | |
| ); | |
| this.warn( | |
| `skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`, | |
| ); | |
| this.moved = true; | |
| media.currentTime = targetTime; | |
| if (!appended?.gap) { | |
| const error = new Error( | |
| `fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`, | |
| ); | |
| const errorData: ErrorData = { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, | |
| fatal: false, | |
| error, | |
| reason: error.message, | |
| buffer: bufferInfo.len, | |
| bufferInfo, | |
| }; | |
| if (appended) { | |
| if ('fragment' in appended) { | |
| errorData.part = appended; | |
| } else { | |
| errorData.frag = appended; | |
| } | |
| } | |
| this.hls.trigger(Events.ERROR, errorData); | |
| } | |
| return targetTime; | |
| } | |
| } | |
| return 0; | |
| } | |
| /** | |
| * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount. | |
| * @private | |
| */ | |
| private _tryNudgeBuffer(bufferInfo: BufferInfo) { | |
| const { hls, media, nudgeRetry } = this; | |
| const config = hls?.config; | |
| if (!media || !config) { | |
| return 0; | |
| } | |
| const currentTime = media.currentTime; | |
| this.nudgeRetry++; | |
| if (nudgeRetry < config.nudgeMaxRetry) { | |
| const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset; | |
| // playback stalled in buffered area ... let's nudge currentTime to try to overcome this | |
| const error = new Error( | |
| `Nudging 'currentTime' from ${currentTime} to ${targetTime}`, | |
| ); | |
| this.warn(error.message); | |
| media.currentTime = targetTime; | |
| hls.trigger(Events.ERROR, { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.BUFFER_NUDGE_ON_STALL, | |
| error, | |
| fatal: false, | |
| buffer: bufferInfo.len, | |
| bufferInfo, | |
| }); | |
| } else { | |
| const error = new Error( | |
| `Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`, | |
| ); | |
| this.error(error.message); | |
| hls.trigger(Events.ERROR, { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.BUFFER_STALLED_ERROR, | |
| error, | |
| fatal: true, | |
| buffer: bufferInfo.len, | |
| bufferInfo, | |
| }); | |
| } | |
| } | |
| } | |
| function getInFlightDependency( | |
| inFlightFragments: InFlightFragments, | |
| currentTime: number, | |
| ): Fragment | null { | |
| const main = inFlight(inFlightFragments.main); | |
| if (main && main.start <= currentTime) { | |
| return main; | |
| } | |
| const audio = inFlight(inFlightFragments.audio); | |
| if (audio && audio.start <= currentTime) { | |
| return audio; | |
| } | |
| return null; | |
| } | |
| function inFlight(inFlightData: InFlightData | undefined): Fragment | null { | |
| if (!inFlightData) { | |
| return null; | |
| } | |
| switch (inFlightData.state) { | |
| case State.IDLE: | |
| case State.STOPPED: | |
| case State.ENDED: | |
| case State.ERROR: | |
| return null; | |
| } | |
| return inFlightData.frag; | |
| } | |
| function appendedFragAtPosition(pos: number, fragmentTracker: FragmentTracker) { | |
| return ( | |
| fragmentTracker.getAppendedFrag(pos, PlaylistLevelType.MAIN) || | |
| fragmentTracker.getPartialFragment(pos) | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 24.2 kB
- Xet hash:
- 782a9d4eccc677b17cbf4c7cabd3dbe9edbe7874b85e28635324f7245eb54cb0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.