fix(server): add hint header for segment after init.mp4 (#28867)

* add hint header for segment after init.mp4

* use zod

* actually validate

* update openapi

* linting
This commit is contained in:
Mert
2026-06-10 19:18:36 -04:00
committed by GitHub
parent 74878628c8
commit aa126e377c
9 changed files with 115 additions and 17 deletions
+30 -1
View File
@@ -256,7 +256,7 @@ describe(HlsService.name, () => {
});
});
it('returns lastRequested + 1 for init.mp4 after a segment has been served', async () => {
it('returns lastRequested + 1 for init.mp4 without a target segment', async () => {
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s');
mocks.websocket.serverSend.mockClear();
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4');
@@ -313,6 +313,35 @@ describe(HlsService.name, () => {
NotFoundException,
);
});
it('uses the target segment for init.mp4 when provided', async () => {
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4', 7);
expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', {
sessionId,
variantIndex,
segmentIndex: 7,
});
});
it('prefers the target segment over the lastRequested + 1 fallback', async () => {
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); // fallback would be 6
mocks.websocket.serverSend.mockClear();
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4', 12);
expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', {
sessionId,
variantIndex,
segmentIndex: 12,
});
});
it('ignores the target segment for media segment requests (the filename wins)', async () => {
await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s', 99);
expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', {
sessionId,
variantIndex,
segmentIndex: 5,
});
});
});
describe('endSession', () => {
+15 -4
View File
@@ -82,7 +82,14 @@ export class HlsService extends BaseService {
return this.generateMediaPlaylist(asset);
}
async getSegment(auth: AuthDto, assetId: string, sessionId: string, variantIndex: number, filename: string) {
async getSegment(
auth: AuthDto,
assetId: string,
sessionId: string,
variantIndex: number,
filename: string,
initSegment?: number,
) {
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });
const session = await this.videoStreamRepository.getSession(sessionId);
@@ -99,7 +106,7 @@ export class HlsService extends BaseService {
});
const apiSession = this.trackSession(sessionId, variantIndex);
const segmentIndex = this.getSegmentIndex(apiSession, filename);
const segmentIndex = this.getSegmentIndex(apiSession, filename, initSegment);
this.websocketRepository.serverSend('HlsHeartbeat', { sessionId, variantIndex, segmentIndex });
if (await this.storageRepository.checkFileExists(path, constants.R_OK)) {
@@ -172,9 +179,13 @@ export class HlsService extends BaseService {
return `${sessionId}:${variantIndex}:${segmentIndex}`;
}
private getSegmentIndex(session: ApiSession, filename: string) {
private getSegmentIndex(session: ApiSession, filename: string, initSegment?: number) {
if (filename.endsWith('.mp4')) {
return (session.lastRequestedSegment ?? -1) + 1;
// We need to know where to start transcoding, but the init.mp4 has no segment number in its name.
// We can infer this from the last requested segment, but this can be inaccurate given the client
// can load cached segments without reaching out to the server. `initSegment` acts as a hint to
// remove ambiguity when possible.
return initSegment ?? (session.lastRequestedSegment ?? -1) + 1;
}
const segmentIndex = Number.parseInt(HLS_SEGMENT_FILENAME_REGEX.exec(filename)![1]);
session.lastRequestedSegment = segmentIndex;
+13 -3
View File
@@ -30,6 +30,7 @@ type Session = {
ownerId: string;
paused: boolean;
process: ChildProcess | null;
starting: boolean;
startSegment: number | null;
variantIndex: number | null;
};
@@ -75,6 +76,7 @@ export class TranscodingService extends BaseService {
ownerId,
paused: false,
process: null,
starting: false,
startSegment: null,
variantIndex: null,
});
@@ -145,11 +147,19 @@ export class TranscodingService extends BaseService {
} else if (session.process) {
this.resumeTranscode(session);
return;
} else if (session.starting) {
this.logger.debug(`Session ${sessionId} is already starting a transcode, skipping duplicate start request`);
return;
}
const process = await this.startTranscode(session, variantIndex, segmentIndex);
if (process) {
session.process = process;
session.starting = true;
try {
const process = await this.startTranscode(session, variantIndex, segmentIndex);
if (process) {
session.process = process;
}
} finally {
session.starting = false;
}
}