Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1608,7 +1608,7 @@ export default class BaseStreamController
}
let programFrag = this.filterReplacedPrimary(frag, levelDetails);
if (!programFrag && frag) {
programFrag = getNextFrag(levelDetails, frag.sn, this.loadingParts);
programFrag = getNextFrag(levelDetails, frag.sn);
if (programFrag) {
this.nextLoadPosition = programFrag.start;
}
Expand Down
9 changes: 7 additions & 2 deletions src/controller/buffer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,11 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
const key = appendProgressKey(frag);
const progress = this.fragmentAppendProgress[key];
delete this.fragmentAppendProgress[key];
const cycle = progress?.stats === frag.stats ? progress : undefined;
if (!progress) {
// Tracking miss
return;
}
const cycle = progress.stats === frag.stats ? progress : undefined;
Comment on lines +1409 to +1413

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early return also disables the "count parsed fragments that produced no append operations" protection from #7941

I think that checking buffering stats should fix the same false positive (appends were queued but displaced before completing by transfer/end-of-stream), and the original tests should then pass unmodified:

-    if (!progress) {
-      // Tracking miss
-      return;
-    }
-    const cycle = progress.stats === frag.stats ? progress : undefined;
+    const cycle = progress?.stats === frag.stats ? progress : undefined;
     if (cycle?.errored) {
       // Counted by the append-error path
       return;
     }
+    const fragBuffering = frag.stats.buffering;
+    if (fragBuffering.start > 0 && fragBuffering.first === 0) {
+      // Queued appends never completed (displaced by end-of-stream or transfer)
+      return;
+    }

(A per-SourceBuffer in-flight counter would potentially be even more precise, but I didn't want to suggest introducing new stuff if you think the buffering stats we already have are enough)

if (cycle?.errored) {
// Counted by the append-error path
return;
Expand Down Expand Up @@ -2441,5 +2445,6 @@ function isFragmentFullyBuffered(
coverage: number,
fragment: Fragment,
): boolean {
return fragment.duration - coverage <= MIN_BUFFERED_PROGRESS;
// .05 BUFFER_APPEND_NO_PROGRESS coverage tolerance accounts for large composition times
return fragment.duration - coverage <= 0.05;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would <= config.maxBufferHole make sense here?

}
8 changes: 4 additions & 4 deletions src/controller/fragment-finders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ export function findNearestWithCC(
export function getNextFrag(
details: LevelDetails,
sn: number,
fragmentHintForParts?: boolean,
): MediaFragment | null {
return fragmentHintForParts && sn === details.endSN && details.fragmentHint
? details.fragmentHint
: details.fragments[1 + sn - details.startSN] || null;
if (sn === details.endSN && details.fragmentHint) {
return details.fragmentHint;
}
return details.fragments[1 + sn - details.startSN] || null;
}
2 changes: 1 addition & 1 deletion src/controller/gap-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ function withInterstitialBoundary(
hls.interstitialsManager
) {
const frag = 'type' in appended ? appended : appended.fragment;
const nextFrag = getNextFrag(levelDetails, frag.sn, true);
const nextFrag = getNextFrag(levelDetails, frag.sn);
if (
nextFrag?.level === frag.level &&
fragOverlapsQueuedInterstitial(
Expand Down
36 changes: 21 additions & 15 deletions src/controller/interstitials-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,17 +870,17 @@ export default class InterstitialsController
return;
}
const dataToAttach =
transferring && attachMediaSourceData ? attachMediaSourceData : { media };
transferring && attachMediaSourceData
? { ...attachMediaSourceData }
: { media };
const schedule = this.schedule;
if (schedule) {
if (schedule && isAssetPlayer) {
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 ||
(player as HlsAssetPlayer).appendInPlace === false,
};
Expand Down Expand Up @@ -1673,22 +1673,21 @@ export default class InterstitialsController
) {
const hls = this.hls;
const { loadingEnabled, bufferingEnabled, startPosition } = hls;

const instructToSeek = !skipSeekToStartPosition && hls.hasEnoughToStart;
const hasEnoughToStart = hls.hasEnoughToStart;

this.log(
`Start loading primary @${bufferPos} bufferedPos: ${this.bufferedPos} instructToSeek: ${instructToSeek} startPosition: ${startPosition} loadingEnabled: ${loadingEnabled} bufferingEnabled: ${bufferingEnabled}`,
`Start loading primary @${bufferPos} bufferedPos: ${this.bufferedPos} startPosition: ${startPosition} skip seek: ${skipSeekToStartPosition} has enough ${hasEnoughToStart} loadingEnabled: ${loadingEnabled} bufferingEnabled: ${bufferingEnabled}`,
);
if (
instructToSeek ||
(!skipSeekToStartPosition && hasEnoughToStart) ||
!loadingEnabled ||
Math.abs(startPosition - bufferPos) > 0.1
) {
const details = this.primaryDetails;
if (details?.live && bufferPos >= details.edge) {
if (details?.live && bufferPos + 0.5 >= details.edge) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the resumption TODO: I tested this branch with just the gate narrowed so primary resumes as soon as the playlist reaches the resume point

-      if (details?.live && bufferPos + 0.5 >= details.edge) {
+      if (
+        details?.live &&
+        bufferPos - details.edge > ALIGNED_END_THRESHOLD_SECONDS
+      ) {

combined with Line 1751 changed back to bufferedPos < details.edge.

Breaks in the #7978 stream are segment-aligned, so it looks like the +0.5 costs a full reload cycle at every cue-in. With the narrow gate the primary append merges across the boundary before the playhead gets there, and the end-of-asset stall/nudge shouldn't happen.

const bufferingItem = this.bufferingItem;
this.log(
`Resume primary loading when live reaches ${bufferPos} buffering item: ${bufferingItem ? segmentToString(bufferingItem) : null}`,
`Resume primary loading when live passes ${bufferPos} buffering item: ${bufferingItem ? segmentToString(bufferingItem) : null}`,
);
hls.pauseBuffering();
this.bufferPastEdge = true;
Expand Down Expand Up @@ -1749,16 +1748,20 @@ export default class InterstitialsController
} else if (this.bufferPastEdge) {
const details = this.primaryDetails;
const bufferedPos = this.bufferedPos;
if (details?.live && bufferedPos < details.edge) {
if (details?.live && bufferedPos + 0.5 < details.edge) {
this.bufferPastEdge = false;
const bufferingItem = this.bufferingItem;
this.log(
`Live edge ${details.edge} reached buffer: ${bufferedPos} buffering item: ${bufferingItem ? segmentToString(bufferingItem) : null}`,
`Live edge ${details.edge} passed buffer: ${bufferedPos} buffering item: ${bufferingItem ? segmentToString(bufferingItem) : null}`,
);
const primaryWaiting = bufferingItem?.end === Infinity;
if (primaryWaiting) {
this.startLoadingPrimaryAt(
const playingItem = this.playingItem;
const skipSeekToStartPosition =
this.isInterstitial(playingItem) && playingItem.event.appendInPlace;
this.hls.startLoad(
Math.max(bufferedPos, bufferingItem.start),
skipSeekToStartPosition,
);
} else {
const bufferingPlayer = this.getBufferingPlayer();
Expand Down Expand Up @@ -2312,8 +2315,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
this.startLoadingPrimaryAt(bufferedPos);
}
} else {
// If not detached seek to resumption point
this.startLoadingPrimaryAt(bufferedPos);
// If not detached check playing item for seek to resumption point
const playingItem = this.playingItem;
const skipSeekToStartPosition =
this.isInterstitial(playingItem) && playingItem.event.appendInPlace;
this.startLoadingPrimaryAt(bufferedPos, skipSeekToStartPosition);
}
}
}
Expand Down
69 changes: 53 additions & 16 deletions src/controller/latency-controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { interstitialsEnabled } from './base-stream-controller';
import { ErrorDetails } from '../errors';
import { Events } from '../events';
import type { HlsConfig } from '../config';
import type Hls from '../hls';
import type { LevelDetails } from '../loader/level-details';
import type { ComponentAPI } from '../types/component-api';
import type {
ErrorData,
InterstitialAssetStartedData,
LevelUpdatedData,
MediaAttachingData,
} from '../types/events';

export default class LatencyController implements ComponentAPI {
private hls: Hls | null;
private readonly config: HlsConfig;
private media: HTMLMediaElement | null = null;
private currentTime: number = 0;
private stallCount: number = 0;
Expand All @@ -21,7 +21,6 @@ export default class LatencyController implements ComponentAPI {

constructor(hls: Hls) {
this.hls = hls;
this.config = hls.config;
this.registerListeners();
}

Expand All @@ -34,7 +33,10 @@ export default class LatencyController implements ComponentAPI {
}

get maxLatency(): number {
const { config } = this;
const config = this.hls?.config;
if (!config) {
return 0;
}
if (config.liveMaxLatencyDuration !== undefined) {
return config.liveMaxLatencyDuration;
}
Expand All @@ -46,12 +48,12 @@ export default class LatencyController implements ComponentAPI {

get targetLatency(): number | null {
const levelDetails = this.levelDetails;
if (levelDetails === null || this.hls === null) {
if (levelDetails === null || !this.hls) {
return null;
}
const config = this.hls.config;
const { holdBack, partHoldBack, targetduration } = levelDetails;
const { liveSyncDuration, liveSyncDurationCount, lowLatencyMode } =
this.config;
const { liveSyncDuration, liveSyncDurationCount, lowLatencyMode } = config;
const userConfig = this.hls.userConfig;
let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack;
if (
Expand All @@ -69,22 +71,25 @@ export default class LatencyController implements ComponentAPI {
return (
targetLatency +
Math.min(
this.stallCount * this.config.liveSyncOnStallIncrease,
this.stallCount * config.liveSyncOnStallIncrease,
maxLiveSyncOnStallIncrease,
)
);
}

set targetLatency(latency: number) {
if (!this.hls) {
return;
}
this.stallCount = 0;
this.config.liveSyncDuration = latency;
this.hls.config.liveSyncDuration = latency;
this._targetLatencyUpdated = true;
}

get liveSyncPosition(): number | null {
const liveEdge = this.estimateLiveEdge();
const targetLatency = this.targetLatency;
if (liveEdge === null || targetLatency === null) {
if (liveEdge === null || targetLatency === null || !this.hls) {
return null;
}
const levelDetails = this.levelDetails;
Expand All @@ -96,7 +101,7 @@ export default class LatencyController implements ComponentAPI {
const min = edge - levelDetails.totalduration;
const max =
edge -
((this.config.lowLatencyMode && levelDetails.partTarget) ||
((this.hls.config.lowLatencyMode && levelDetails.partTarget) ||
levelDetails.targetduration);
return Math.min(Math.max(min, syncPosition), max);
}
Expand All @@ -111,11 +116,11 @@ export default class LatencyController implements ComponentAPI {

get edgeStalled(): number {
const levelDetails = this.levelDetails;
if (levelDetails === null) {
if (levelDetails === null || !this.hls) {
return 0;
}
const maxLevelUpdateAge =
((this.config.lowLatencyMode && levelDetails.partTarget) ||
((this.hls.config.lowLatencyMode && levelDetails.partTarget) ||
levelDetails.targetduration) * 3;
return Math.max(levelDetails.age - maxLevelUpdateAge, 0);
}
Expand All @@ -138,6 +143,8 @@ export default class LatencyController implements ComponentAPI {
this.unregisterListeners();
this.onMediaDetaching();
this.hls = null;
// @ts-ignore
this.config = null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed? it looks like this.config was removed from the constructor

}

private registerListeners() {
Expand All @@ -150,6 +157,7 @@ export default class LatencyController implements ComponentAPI {
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.on(Events.ERROR, this.onError, this);
hls.on(Events.INTERSTITIAL_ASSET_STARTED, this.onAssetStarted, this);
}

private unregisterListeners() {
Expand All @@ -162,6 +170,7 @@ export default class LatencyController implements ComponentAPI {
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.off(Events.ERROR, this.onError, this);
hls.off(Events.INTERSTITIAL_ASSET_STARTED, this.onAssetStarted, this);
}

private onMediaAttached(
Expand Down Expand Up @@ -196,6 +205,28 @@ export default class LatencyController implements ComponentAPI {
}
}

private onAssetStarted(
event: Events.INTERSTITIAL_ASSET_STARTED,
data: InterstitialAssetStartedData,
) {
const hls = this.hls;
const media =
this.media ||
(data.event.appendInPlace &&
hls?.interstitialsManager?.playerQueue.reduce(
(found, player) => found || player.media,
null,
));
if (
hls &&
media &&
media.playbackRate > 1 &&
media.playbackRate <= hls.config.maxLiveSyncPlaybackRate
) {
this.changeMediaPlaybackRate(media, 1);
}
}

private onError(event: Events.ERROR, data: ErrorData) {
if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) {
return;
Expand All @@ -211,19 +242,20 @@ export default class LatencyController implements ComponentAPI {
private onTimeupdate = () => {
const { media } = this;
const levelDetails = this.levelDetails;
if (!media || !levelDetails) {
if (!media || !levelDetails || !this.hls) {
return;
}
this.currentTime = media.currentTime;

const config = this.hls.config;
const latency = this.computeLatency();
if (latency === null) {
return;
}
this._latency = latency;

// Adapt playbackRate to meet target latency in low-latency mode
const { lowLatencyMode, maxLiveSyncPlaybackRate } = this.config;
const { lowLatencyMode, maxLiveSyncPlaybackRate } = config;
if (
!lowLatencyMode ||
maxLiveSyncPlaybackRate === 1 ||
Expand All @@ -245,10 +277,15 @@ export default class LatencyController implements ComponentAPI {
);
const inLiveRange = distanceFromTarget < liveMinLatencyDuration;

const playingInterstitial =
interstitialsEnabled(config) &&
!!this.hls.interstitialsManager?.playingItem?.event;

if (
inLiveRange &&
distanceFromTarget > 0.05 &&
this.forwardBufferLength > 1
this.forwardBufferLength > 1 &&
!playingInterstitial
) {
const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate));
const rate =
Expand Down
15 changes: 15 additions & 0 deletions src/loader/interstitial-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export class InterstitialEvent {
if (this.snapOptions.out) {
const frag = this.dateRange.tagAnchor;
if (frag) {
if (!fragmentRefCoversTime(frag, startTime)) {
// Do not snap to segment boundary when segment does not cover datetime
return startTime;
}
return getSnapToFragmentTime(startTime, frag);
}
}
Expand Down Expand Up @@ -187,6 +191,10 @@ export class InterstitialEvent {
if (this.snapOptions.in) {
const frag = this.resumeAnchor;
if (frag) {
if (!fragmentRefCoversTime(frag, resumeTime)) {
// Do not snap to segment boundary when segment does not cover datetime
return resumeTime;
}
return getSnapToFragmentTime(resumeTime, frag);
}
}
Expand Down Expand Up @@ -305,6 +313,13 @@ export function getSnapToFragmentTime(time: number, frag: MediaFragmentRef) {
: frag.start + frag.duration;
}

function fragmentRefCoversTime(frag: MediaFragmentRef, time: number): boolean {
return (
time > frag.start - ALIGNED_END_THRESHOLD_SECONDS &&
time < frag.start + frag.duration + ALIGNED_END_THRESHOLD_SECONDS
);
}

export function getInterstitialUrl(
uri: string,
sessionId: string,
Expand Down
Loading
Loading