Skip to content

Commit

Permalink
Merge pull request video-dev#3556 from video-dev/bugfix/back-buffer-f…
Browse files Browse the repository at this point in the history
…lush-aborts-loading

Prevent back-buffer eviction callback from resetting active segment requests
  • Loading branch information
robwalch authored Mar 4, 2021
2 parents 064c336 + a4307e9 commit c5698be
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 155 deletions.
22 changes: 7 additions & 15 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,22 +669,14 @@ class AudioStreamController
}
}

onBufferFlushed(event: Events.BUFFER_FLUSHED, { type }: BufferFlushedData) {
/* after successful buffer flushing, filter flushed fragments from bufferedFrags
use mediaBuffered instead of media (so that we will check against video.buffered ranges in case of alt audio track)
*/
const media = this.mediaBuffer ? this.mediaBuffer : this.media;
if (media && type === ElementaryStreamTypes.AUDIO) {
// filter fragments potentially evicted from buffer. this is to avoid memleak on live streams
this.fragmentTracker.detectEvictedFragments(
ElementaryStreamTypes.AUDIO,
BufferHelper.getBuffered(media)
);
private onBufferFlushed(
event: Events.BUFFER_FLUSHED,
{ type }: BufferFlushedData
) {
if (type === ElementaryStreamTypes.AUDIO) {
const media = this.mediaBuffer ? this.mediaBuffer : this.media;
this.afterBufferFlushed(media, type);
}
// reset reference to frag
this.fragPrevious = null;
// move to IDLE once flush complete. this should trigger new fragment loading
this.state = State.IDLE;
}

private _handleTransmuxComplete(transmuxResult: TransmuxerResult) {
Expand Down
209 changes: 117 additions & 92 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import TaskLoop from '../task-loop';
import { FragmentState } from './fragment-tracker';
import { BufferHelper } from '../utils/buffer-helper';
import { Bufferable, BufferHelper } from '../utils/buffer-helper';
import { logger } from '../utils/logger';
import { Events } from '../events';
import { ErrorDetails } from '../errors';
Expand Down Expand Up @@ -211,7 +211,7 @@ export default class BaseStreamController
const fragStartOffset = fragCurrent.start - tolerance;
const fragEndOffset =
fragCurrent.start + fragCurrent.duration + tolerance;
// check if we seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
// check if the seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
if (fragCurrent.loader) {
this.log(
Expand Down Expand Up @@ -294,43 +294,44 @@ export default class BaseStreamController
this._handleFragmentLoadProgress(data);
};

this._doFragLoad(
frag,
levelDetails,
targetBufferTime,
progressCallback
).then((data) => {
if (!data) {
// if we're here we probably needed to backtrack or are waiting for more parts
return;
}
this.fragLoadError = 0;
if (this.fragContextChanged(frag)) {
if (
this.state === State.FRAG_LOADING ||
this.state === State.BACKTRACKING
) {
this.fragmentTracker.removeFragment(frag);
this.state = State.IDLE;
this._doFragLoad(frag, levelDetails, targetBufferTime, progressCallback)
.then((data) => {
if (!data) {
// if we're here we probably needed to backtrack or are waiting for more parts
return;
}
this.fragLoadError = 0;
if (this.fragContextChanged(frag)) {
if (
this.state === State.FRAG_LOADING ||
this.state === State.BACKTRACKING
) {
this.fragmentTracker.removeFragment(frag);
this.state = State.IDLE;
}
return;
}
return;
}

if ('payload' in data) {
this.log(`Loaded fragment ${frag.sn} of level ${frag.level}`);
this.hls.trigger(Events.FRAG_LOADED, data);
if ('payload' in data) {
this.log(`Loaded fragment ${frag.sn} of level ${frag.level}`);
this.hls.trigger(Events.FRAG_LOADED, data);

// Tracker backtrack must be called after onFragLoaded to update the fragment entity state to BACKTRACKED
// This happens after handleTransmuxComplete when the worker or progressive is disabled
if (this.state === State.BACKTRACKING) {
this.fragmentTracker.backtrack(frag, data);
return;
// Tracker backtrack must be called after onFragLoaded to update the fragment entity state to BACKTRACKED
// This happens after handleTransmuxComplete when the worker or progressive is disabled
if (this.state === State.BACKTRACKING) {
this.fragmentTracker.backtrack(frag, data);
this.resetFragmentLoading(frag);
return;
}
}
}

// Pass through the whole payload; controllers not implementing progressive loading receive data from this callback
this._handleFragmentLoadComplete(data);
});
// Pass through the whole payload; controllers not implementing progressive loading receive data from this callback
this._handleFragmentLoadComplete(data);
})
.catch((reason) => {
this.warn(reason);
this.resetFragmentLoading(frag);
});
}

protected flushMainBuffer(
Expand Down Expand Up @@ -432,6 +433,7 @@ export default class BaseStreamController
})
.catch((reason) => {
this.warn(reason);
this.resetFragmentLoading(frag);
});
}

Expand Down Expand Up @@ -595,15 +597,11 @@ export default class BaseStreamController
}

protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) {
if (this.state !== State.PARSING) {
this.warn(
`State is expected to be PARSING on transmuxer flush, but is ${this.state}.`
);
return;
}

const context = this.getCurrentContext(chunkMeta);
if (!context) {
if (!context || this.state !== State.PARSING) {
if (!this.fragCurrent) {
this.state = State.IDLE;
}
return;
}
const { frag, part, level } = context;
Expand All @@ -612,9 +610,7 @@ export default class BaseStreamController
if (part) {
part.stats.parsing.end = now;
}
this.updateLevelTiming(frag, level, chunkMeta.partial);
this.state = State.PARSED;
this.hls.trigger(Events.FRAG_PARSED, { frag, part });
this.updateLevelTiming(frag, part, level, chunkMeta.partial);
}

protected getCurrentContext(
Expand Down Expand Up @@ -1074,19 +1070,18 @@ export default class BaseStreamController

private handleFragLoadAborted(frag: Fragment, part: Part | undefined) {
if (this.transmuxer && frag.sn !== 'initSegment') {
this.log(
`Fragment ${frag.sn} of level ${frag.level} was aborted, flushing transmuxer`
);
this.transmuxer.flush(
new ChunkMetadata(
frag.level,
frag.sn,
frag.stats.chunkCount + 1,
0,
part ? part.index : -1,
true
)
this.warn(
`Fragment ${frag.sn}${part ? ' part' + part.index : ''} of level ${
frag.level
} was aborted`
);
this.resetFragmentLoading(frag);
}
}

protected resetFragmentLoading(frag: Fragment) {
if (!this.fragCurrent || !this.fragContextChanged(frag)) {
this.state = State.IDLE;
}
}

Expand Down Expand Up @@ -1147,6 +1142,16 @@ export default class BaseStreamController
}
}

protected afterBufferFlushed(media: Bufferable, type: SourceBufferName) {
if (!media) {
return;
}
// After successful buffer flushing, filter flushed fragments from bufferedFrags use mediaBuffered instead of media
// (so that we will check against video.buffered ranges in case of alt audio track)
const bufferedTimeRanges = BufferHelper.getBuffered(media);
this.fragmentTracker.detectEvictedFragments(type, bufferedTimeRanges);
}

protected resetLiveStartWhenNotLoaded(level: number): boolean {
// if loadedmetadata is not set, it means that we are emergency switch down on first frag
// in that case, reset startFragRequested flag
Expand All @@ -1165,45 +1170,65 @@ export default class BaseStreamController
return false;
}

private updateLevelTiming(frag: Fragment, level: Level, partial: boolean) {
private updateLevelTiming(
frag: Fragment,
part: Part | null,
level: Level,
partial: boolean
) {
const details = level.details as LevelDetails;
console.assert(!!details, 'level.details must be defined');
Object.keys(frag.elementaryStreams).forEach((type) => {
const info = frag.elementaryStreams[type];
if (info) {
const parsedDuration = info.endPTS - info.startPTS;
if (parsedDuration <= 0) {
// Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0.
// The new transmuxer will be configured with a time offset matching the next fragment start, preventing the timeline from shifting.
this.warn(
`Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration}) resetting transmuxer to fallback to playlist timing`
);
if (this.transmuxer) {
this.transmuxer.destroy();
this.transmuxer = null;
const parsed = Object.keys(frag.elementaryStreams).reduce(
(result, type) => {
const info = frag.elementaryStreams[type];
if (info) {
const parsedDuration = info.endPTS - info.startPTS;
if (parsedDuration <= 0) {
// Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0.
// The new transmuxer will be configured with a time offset matching the next fragment start,
// preventing the timeline from shifting.
this.warn(
`Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration}) resetting transmuxer to fallback to playlist timing`
);
if (this.transmuxer) {
this.transmuxer.destroy();
this.transmuxer = null;
}
return result || false;
}
const drift = partial
? 0
: LevelHelper.updateFragPTSDTS(
details,
frag,
info.startPTS,
info.endPTS,
info.startDTS,
info.endDTS
);
this.hls.trigger(Events.LEVEL_PTS_UPDATED, {
details,
level,
drift,
type,
frag,
start: info.startPTS,
end: info.endPTS,
});
return true;
}
const drift = partial
? 0
: LevelHelper.updateFragPTSDTS(
details,
frag,
info.startPTS,
info.endPTS,
info.startDTS,
info.endDTS
);
this.hls.trigger(Events.LEVEL_PTS_UPDATED, {
details,
level,
drift,
type,
frag,
start: info.startPTS,
end: info.endPTS,
});
}
});
return result;
},
false
);
if (parsed) {
this.state = State.PARSED;
this.hls.trigger(Events.FRAG_PARSED, { frag, part });
} else {
this.fragCurrent = null;
this.fragPrevious = null;
this.state = State.IDLE;
}
}

set state(nextState) {
Expand Down
20 changes: 15 additions & 5 deletions src/controller/buffer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import type {
MediaAttachingData,
ManifestParsedData,
BufferCodecsData,
LevelPTSUpdatedData,
BufferEOSData,
BufferFlushingData,
FragParsedData,
FragChangedData,
} from '../types/events';
import type { ComponentAPI } from '../types/component-api';
import type Hls from '../hls';
Expand Down Expand Up @@ -84,6 +84,7 @@ export default class BufferController implements ComponentAPI {
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.on(Events.FRAG_PARSED, this.onFragParsed, this);
hls.on(Events.FRAG_CHANGED, this.onFragChanged, this);
}

protected unregisterListeners() {
Expand All @@ -98,6 +99,7 @@ export default class BufferController implements ComponentAPI {
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.off(Events.FRAG_PARSED, this.onFragParsed, this);
hls.off(Events.FRAG_CHANGED, this.onFragChanged, this);
}

private _initSourceBuffer() {
Expand Down Expand Up @@ -482,6 +484,9 @@ export default class BufferController implements ComponentAPI {
}

this.blockBuffers(onUnblocked, buffersAppendedTo);
}

private onFragChanged(event: Events.FRAG_CHANGED, data: FragChangedData) {
this.flushBackBuffer();
}

Expand Down Expand Up @@ -547,8 +552,11 @@ export default class BufferController implements ComponentAPI {
}

const currentTime = media.currentTime;
const targetDuration = details.levelTargetDuration;
const maxBackBufferLength = Math.max(backBufferLength, targetDuration);
const targetBackBufferPosition =
currentTime - Math.max(backBufferLength, details.levelTargetDuration);
Math.floor(currentTime / targetDuration) * targetDuration -
maxBackBufferLength;
sourceBufferTypes.forEach((type: SourceBufferName) => {
const sb = sourceBuffer[type];
if (sb) {
Expand All @@ -563,9 +571,11 @@ export default class BufferController implements ComponentAPI {
});

// Support for deprecated event:
hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
bufferEnd: targetBackBufferPosition,
});
if (details.live) {
hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
bufferEnd: targetBackBufferPosition,
});
}

hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 0,
Expand Down
Loading

0 comments on commit c5698be

Please sign in to comment.