mirror of
https://github.com/immich-app/immich.git
synced 2026-08-01 16:38:02 -07:00
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:
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user