Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fix Low-Latency HLS VTT subtitle part loading#7626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
robwalch wants to merge3 commits intomaster
base:master
Choose a base branch
Loading
frombugfix/low-latency-webvtt-part-loading
Open
Show file tree
Hide file tree
Changes from1 commit
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: 2 additions & 0 deletionsapi-extractor/report/hls.js.api.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4620,6 +4620,8 @@ export interface SubtitleFragProcessedData {
// (undocumented)
frag: Fragment;
// (undocumented)
part: Part | null;
// (undocumented)
success: boolean;
}

Expand Down
4 changes: 0 additions & 4 deletionssrc/controller/base-stream-controller.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1127,10 +1127,6 @@ export default class BaseStreamController
// Buffer must be ahead of first part + duration of parts after last segment
// and playback must be at or past segment adjacent to part list
const firstPart = details.partList[0];
// Loading of VTT subtitle parts is not implemented in subtitle-stream-controller (#7460)
if (firstPart.fragment.type === PlaylistLevelType.SUBTITLE) {
return false;
}
const safePartStart =
firstPart.end + (details.fragmentHint?.duration || 0);
if (bufferEnd >= safePartStart) {
Expand Down
158 changes: 56 additions & 102 deletionssrc/controller/subtitle-stream-controller.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
import BaseStreamController, { State } from './base-stream-controller';
import { findFragmentByPTS } from './fragment-finders';
import { FragmentState } from './fragment-tracker';
import { ErrorDetails, ErrorTypes } from '../errors';
import { Events } from '../events';
Expand DownExpand Up@@ -48,7 +47,7 @@ export class SubtitleStreamController
implements NetworkComponentAPI
{
private currentTrackId: number = -1;
private tracksBuffered: Array<TimeRange[]> = [];
private tracksBuffered: Array<TimeRange[] | undefined> = [];
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see anywhere where we are appending an undefined object to tracksBuffered? Do we suspect we need this?

Copy link
CollaboratorAuthor

@robwalchrobwalchNov 6, 2025
edited
Loading

Choose a reason for hiding this comment

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

We don't appendundefined values, but we do not usenoUncheckedIndexedAccess either. When using (@typescript-eslint/)no-unnecessary-condition (I use this offline, but not in the project proper), typing this way helps by signaling that the result totracksBuffered[<missing-key-or-index>] may be undefined. Yes, it is bad typing, but simpler safety. I'm open to suggestions - short and long term.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Got it, I think this is fine for now. And agree maybe something to look into in the future.

private mainDetails: LevelDetails | null = null;

constructor(
Expand DownExpand Up@@ -128,13 +127,7 @@ export class SubtitleStreamController
event: Events.SUBTITLE_FRAG_PROCESSED,
data: SubtitleFragProcessed,
) {
const { frag, success } = data;
if (!this.fragContextChanged(frag)) {
if (isMediaFragment(frag)) {
this.fragPrevious = frag;
}
this.state = State.IDLE;
}
const { frag, part, success } = data;
if (!success) {
return;
}
Expand All@@ -147,28 +140,32 @@ export class SubtitleStreamController
// Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo
// so we can re-use the logic used to detect how much has been buffered
let timeRange: TimeRange | undefined;
constfragStart = frag.start;
conststart =(part ||frag).start;
for (let i = 0; i < buffered.length; i++) {
if (fragStart >= buffered[i].start &&fragStart <= buffered[i].end) {
if (start >= buffered[i].start &&start <= buffered[i].end) {
timeRange = buffered[i];
break;
}
}

constfragEnd =frag.start + frag.duration;
constend = start +(part ||frag).duration;
if (timeRange) {
timeRange.end =fragEnd;
timeRange.end =end;
} else {
timeRange = {
start: fragStart,
end: fragEnd,
};
timeRange = { start, end };
buffered.push(timeRange);
}
this.fragmentTracker.fragBuffered(frag as MediaFragment);
this.fragBufferedComplete(frag, null);
if (this.media) {
this.tick();
if (!part || end >= frag.end) {
this.fragmentTracker.fragBuffered(frag as MediaFragment);
if (!this.fragContextChanged(frag)) {
if (isMediaFragment(frag)) {
this.fragPrevious = frag;
}
}
this.fragBufferedComplete(frag, part);
if (this.media) {
this.tickImmediate();
}
}
}

Expand All@@ -184,6 +181,7 @@ export class SubtitleStreamController
}
data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles);
this.tracksBuffered.forEach((buffered) => {
if (!buffered) return;
for (let i = 0; i < buffered.length; ) {
if (buffered[i].end <= endOffsetSubtitles) {
buffered.shift();
Expand DownExpand Up@@ -259,13 +257,13 @@ export class SubtitleStreamController
}

// Check if track has the necessary details to load fragments
const currentTrack = this.levels[this.currentTrackId];
if (currentTrack?.details) {
this.mediaBuffer = this.mediaBufferTimeRanges;
} else {
const currentTrack = this.levels[this.currentTrackId] as Level | undefined;
if (!currentTrack?.details) {
this.mediaBuffer = null;
return;
}
if (currentTrack && this.state !== State.STOPPED) {
this.mediaBuffer = this.mediaBufferTimeRanges;
if (this.state !== State.STOPPED) {
this.setInterval(TICK_INTERVAL);
}
}
Expand All@@ -281,7 +279,7 @@ export class SubtitleStreamController
this.warn(`Subtitle tracks were reset while loading level ${trackId}`);
return;
}
const track: Level= levels[trackId];
const track= levels[trackId] as Level | undefined;
if (trackId >= levels.length || !track) {
return;
}
Expand DownExpand Up@@ -346,26 +344,7 @@ export class SubtitleStreamController
});

// trigger handler right now
this.tick();

// If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload
if (
newDetails.live &&
!this.fragCurrent &&
this.media &&
this.state === State.IDLE
) {
const foundFrag = findFragmentByPTS(
null,
newDetails.fragments,
this.media.currentTime,
0,
);
if (!foundFrag) {
this.warn('Subtitle playlist not aligned with playback');
track.details = undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

The logic for streaming subtitles in LL-HLS has become much more lenient 👍

Copy link
CollaboratorAuthor

@robwalchrobwalchNov 6, 2025
edited
Loading

Choose a reason for hiding this comment

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

Additional changes in#7482 will help to ensure proper playlist alignment. When combined with these changes, audio and subtitle segment loading will follow more of the same logic.

hongjun-bae reacted with eyes emoji
}
}
this.tickImmediate();
}

_handleFragmentLoadComplete(fragLoadedData: FragLoadedData) {
Expand DownExpand Up@@ -423,18 +402,23 @@ export class SubtitleStreamController
}

doTick() {
if (!this.media) {
this.state = State.IDLE;
return;
}

if (this.state === State.IDLE) {
const { currentTrackId, levels } = this;
const track = levels?.[currentTrackId];
if (!track || !levels.length || !track.details) {
if (
!this.media &&
!this.primaryPrefetch &&
(this.startFragRequested || !this.config.startFragPrefetch)
) {
return;
}
if (this.waitForLive(track)) {
const { currentTrackId, levels } = this;
const track = levels?.[currentTrackId];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Before there was a check to see if track is undefined, now it's removed here. I believe this may cause issues. We may need to keep what was here previously:
if (!track || !levels.length || !track.details) { return; }

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

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

We're covered bytrack?.details and!trackDetails. No details, no track.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Got it. that's true that the original code had that:!track.details

const trackDetails = track?.details;
if (
!trackDetails ||
this.waitForLive(track) ||
this.waitForCdnTuneIn(trackDetails)
) {
this.startFragRequested = false;
return;
}
const { config } = this;
Expand All@@ -445,62 +429,32 @@ export class SubtitleStreamController
config.maxBufferHole,
);
const { end: targetBufferTime, len: bufferLen } = bufferedInfo;
const trackDetails = track.details as LevelDetails;
const maxBufLen =
this.hls.maxBufferLength + trackDetails.levelTargetDuration;

if (bufferLen > maxBufLen) {
if (bufferLen > maxBufLen || (bufferLen && !this.buffering)) {
return;
}
const fragments = trackDetails.fragments;
const fragLen = fragments.length;
const end = trackDetails.edge;

let foundFrag: MediaFragment | null = null;
const fragPrevious = this.fragPrevious;
if (targetBufferTime < end) {
const tolerance = config.maxFragLookUpTolerance;
const lookupTolerance =
targetBufferTime > end - tolerance ? 0 : tolerance;
foundFrag = findFragmentByPTS(
fragPrevious,
fragments,
Math.max(fragments[0].start, targetBufferTime),
lookupTolerance,
);
if (
!foundFrag &&
fragPrevious &&
fragPrevious.start < fragments[0].start
) {
foundFrag = fragments[0];
}
} else {
foundFrag = fragments[fragLen - 1];
}
foundFrag = this.filterReplacedPrimary(foundFrag, track.details);
if (!foundFrag) {
let frag = this.getNextFragment(currentTime, trackDetails);
if (!frag) {
return;
}
// Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment
const curSNIdx = foundFrag.sn - trackDetails.startSN;
const prevFrag = fragments[curSNIdx - 1];
if (
prevFrag &&
prevFrag.cc === foundFrag.cc &&
this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED
) {
foundFrag = prevFrag;
}
if (
this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED
) {
// only load if fragment is not loaded
const fragToLoad = this.mapToInitFragWhenRequired(foundFrag);
if (fragToLoad) {
this.loadFragment(fragToLoad, track, targetBufferTime);
if (isMediaFragment(frag)) {
const curSNIdx = frag.sn - trackDetails.startSN;
const prevFrag = trackDetails.fragments[curSNIdx - 1] as
| MediaFragment
| undefined;
if (
prevFrag &&
prevFrag.cc === frag.cc &&
!trackDetails.partList?.length &&
this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED
) {
frag = prevFrag;
}
}
this.loadFragment(frag, track, targetBufferTime);
}
}

Expand Down
1 change: 0 additions & 1 deletionsrc/controller/subtitle-track-controller.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -526,7 +526,6 @@ class SubtitleTrackController extends BasePlaylistController {
// and we'll set subtitleTrack when onMediaAttached is triggered
if (!this.media) {
this.queuedDefaultTrack = newId;
return;
}

// exit if track id as already set or invalid
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp