mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
reject on variant switch
This commit is contained in:
@@ -303,6 +303,16 @@ describe(HlsService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects pending waiters for the previous variant on variant change', async () => {
|
||||||
|
mocks.storage.checkFileExists.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
const pending = sut.getSegment(auth, assetId, sessionId, 0, 'seg_1.m4s');
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
await sut.getSegment(auth, assetId, sessionId, 1, 'seg_1.m4s');
|
||||||
|
|
||||||
|
await expect(pending).rejects.toThrow('Variant changed');
|
||||||
|
});
|
||||||
|
|
||||||
it('throws NotFoundException when the session does not exist', async () => {
|
it('throws NotFoundException when the session does not exist', async () => {
|
||||||
mocks.videoStream.getSession.mockReset();
|
mocks.videoStream.getSession.mockReset();
|
||||||
await expect(sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4')).rejects.toBeInstanceOf(
|
await expect(sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4')).rejects.toBeInstanceOf(
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import { ImmichFileResponse } from 'src/utils/file';
|
|||||||
import { getOutputSize } from 'src/utils/media';
|
import { getOutputSize } from 'src/utils/media';
|
||||||
|
|
||||||
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo };
|
||||||
|
type ApiSession = { lastRequestedSegment: number | null; lastVariantIndex: number | null };
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HlsService extends BaseService {
|
export class HlsService extends BaseService {
|
||||||
private pendingSegments = new PendingEvents<'HlsSegmentResult'>({ timeoutMs: 15_000 });
|
private pendingSegments = new PendingEvents<'HlsSegmentResult'>({ timeoutMs: 15_000 });
|
||||||
private pendingSessions = new PendingEvents<'HlsSessionResult'>({ timeoutMs: 5000 });
|
private pendingSessions = new PendingEvents<'HlsSessionResult'>({ timeoutMs: 5000 });
|
||||||
private sessions = new Map<string, { lastRequestedSegment: number | null }>();
|
private sessions = new Map<string, ApiSession>();
|
||||||
|
|
||||||
@OnEvent({ name: 'HlsSessionResult', server: true, workers: [ImmichWorker.Api] })
|
@OnEvent({ name: 'HlsSessionResult', server: true, workers: [ImmichWorker.Api] })
|
||||||
onSessionResult(event: ArgOf<'HlsSessionResult'>) {
|
onSessionResult(event: ArgOf<'HlsSessionResult'>) {
|
||||||
@@ -65,7 +66,7 @@ export class HlsService extends BaseService {
|
|||||||
const sessionId = this.cryptoRepository.randomUUID();
|
const sessionId = this.cryptoRepository.randomUUID();
|
||||||
this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id });
|
this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id });
|
||||||
await this.pendingSessions.wait(sessionId);
|
await this.pendingSessions.wait(sessionId);
|
||||||
this.sessions.set(sessionId, { lastRequestedSegment: null });
|
this.trackSession(sessionId);
|
||||||
|
|
||||||
return this.generateMainPlaylist(sessionId, ffmpeg, asset);
|
return this.generateMainPlaylist(sessionId, ffmpeg, asset);
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,8 @@ export class HlsService extends BaseService {
|
|||||||
cacheControl: CacheControl.PrivateWithCache,
|
cacheControl: CacheControl.PrivateWithCache,
|
||||||
});
|
});
|
||||||
|
|
||||||
const segmentIndex = this.getSegmentIndex(sessionId, filename);
|
const apiSession = this.trackSession(sessionId, variantIndex);
|
||||||
|
const segmentIndex = this.getSegmentIndex(apiSession, filename);
|
||||||
this.websocketRepository.serverSend('HlsHeartbeat', { sessionId, variantIndex, segmentIndex });
|
this.websocketRepository.serverSend('HlsHeartbeat', { sessionId, variantIndex, segmentIndex });
|
||||||
|
|
||||||
if (await this.storageRepository.checkFileExists(path, constants.R_OK)) {
|
if (await this.storageRepository.checkFileExists(path, constants.R_OK)) {
|
||||||
@@ -170,17 +172,27 @@ export class HlsService extends BaseService {
|
|||||||
return `${sessionId}:${variantIndex}:${segmentIndex}`;
|
return `${sessionId}:${variantIndex}:${segmentIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSegmentIndex(sessionId: string, filename: string) {
|
private getSegmentIndex(session: ApiSession, filename: string) {
|
||||||
const existing = this.sessions.get(sessionId);
|
|
||||||
if (filename.endsWith('.mp4')) {
|
if (filename.endsWith('.mp4')) {
|
||||||
return (existing?.lastRequestedSegment ?? -1) + 1;
|
return (session.lastRequestedSegment ?? -1) + 1;
|
||||||
}
|
}
|
||||||
const segmentIndex = Number.parseInt(HLS_SEGMENT_FILENAME_REGEX.exec(filename)![1]);
|
const segmentIndex = Number.parseInt(HLS_SEGMENT_FILENAME_REGEX.exec(filename)![1]);
|
||||||
if (existing) {
|
session.lastRequestedSegment = segmentIndex;
|
||||||
existing.lastRequestedSegment = segmentIndex;
|
|
||||||
} else {
|
|
||||||
this.sessions.set(sessionId, { lastRequestedSegment: segmentIndex });
|
|
||||||
}
|
|
||||||
return segmentIndex;
|
return segmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trackSession(id: string, variantIndex: number | null = null) {
|
||||||
|
const session = this.sessions.get(id);
|
||||||
|
if (!session) {
|
||||||
|
const newSession = { lastRequestedSegment: null, lastVariantIndex: variantIndex };
|
||||||
|
this.sessions.set(id, newSession);
|
||||||
|
return newSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.lastVariantIndex !== null && session.lastVariantIndex !== variantIndex) {
|
||||||
|
this.pendingSegments.rejectByPrefix(`${id}:${session.lastVariantIndex}:`, 'Variant changed');
|
||||||
|
}
|
||||||
|
session.lastVariantIndex = variantIndex;
|
||||||
|
return session;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user