mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
main playlist
This commit is contained in:
@@ -20,18 +20,18 @@ export class VideoStreamController {
|
||||
private service: HlsService,
|
||||
) {}
|
||||
|
||||
@Get(':id/video/stream/master.m3u8')
|
||||
@Get(':id/video/stream/main.m3u8')
|
||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||
@Header('Cache-Control', 'no-cache')
|
||||
@Header('Content-Type', PLAYLIST_CONTENT_TYPE)
|
||||
@ApiProduces(PLAYLIST_CONTENT_TYPE)
|
||||
@Endpoint({
|
||||
summary: 'Get HLS master playlist',
|
||||
description: 'Returns an HLS master playlist with all available variants for the asset.',
|
||||
summary: 'Get HLS main playlist',
|
||||
description: 'Returns an HLS main playlist with all available variants for the asset.',
|
||||
history: new HistoryBuilder().added('v3').alpha('v3'),
|
||||
})
|
||||
getMasterPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.getMasterPlaylist(auth, id);
|
||||
getMainPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.getMainPlaylist(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/video/stream/:sessionId/:variantIndex/playlist.m3u8')
|
||||
|
||||
@@ -48,7 +48,7 @@ delete from "video_stream_session"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- VideoStreamRepository.getForMasterPlaylist
|
||||
-- VideoStreamRepository.getForMainPlaylist
|
||||
select
|
||||
(
|
||||
select
|
||||
|
||||
@@ -72,7 +72,7 @@ export class VideoStreamRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getForMasterPlaylist(id: string) {
|
||||
async getForMainPlaylist(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
|
||||
@@ -167,14 +167,14 @@ describe(HlsService.name, () => {
|
||||
({ sut, mocks } = newTestService(HlsService));
|
||||
});
|
||||
|
||||
describe('getMasterPlaylist', () => {
|
||||
describe('getMainPlaylist', () => {
|
||||
const auth = factory.auth();
|
||||
const assetId = 'asset-1';
|
||||
|
||||
const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } });
|
||||
mocks.videoStream.getForMasterPlaylist.mockResolvedValue(asset);
|
||||
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
||||
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
||||
if (event === 'HlsSessionRequest') {
|
||||
@@ -184,31 +184,31 @@ describe(HlsService.name, () => {
|
||||
});
|
||||
};
|
||||
|
||||
it('returns master playlist for eiffel-tower (1080p portrait, no acceleration)', async () => {
|
||||
it('returns main playlist for eiffel-tower (1080p portrait, no acceleration)', async () => {
|
||||
setup(eiffelTower, TranscodeHardwareAcceleration.Disabled);
|
||||
await expect(sut.getMasterPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled);
|
||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled);
|
||||
});
|
||||
|
||||
it('returns master playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => {
|
||||
it('returns main playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => {
|
||||
setup(eiffelTower, TranscodeHardwareAcceleration.Rkmpp);
|
||||
await expect(sut.getMasterPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp);
|
||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp);
|
||||
});
|
||||
|
||||
it('returns master playlist for waterfall (4K landscape) with no acceleration', async () => {
|
||||
it('returns main playlist for waterfall (4K landscape) with no acceleration', async () => {
|
||||
setup(waterfall, TranscodeHardwareAcceleration.Disabled);
|
||||
await expect(sut.getMasterPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled);
|
||||
await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled);
|
||||
});
|
||||
|
||||
it('throws BadRequestException when realtime transcoding is disabled', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: false } } });
|
||||
await expect(sut.getMasterPlaylist(auth, assetId)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('throws NotFoundException when asset is not yet ready for streaming', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } });
|
||||
await expect(sut.getMasterPlaylist(auth, assetId)).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,14 +48,14 @@ export class HlsService extends BaseService {
|
||||
this.pendingSegments.complete(this.getSegmentKey(event), event);
|
||||
}
|
||||
|
||||
async getMasterPlaylist(auth: AuthDto, assetId: string) {
|
||||
async getMainPlaylist(auth: AuthDto, assetId: string) {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });
|
||||
const { ffmpeg } = await this.getConfig({ withCache: true });
|
||||
if (!ffmpeg.realtime.enabled) {
|
||||
throw new BadRequestException('Real-time transcoding is not enabled');
|
||||
}
|
||||
|
||||
const asset = await this.videoStreamRepository.getForMasterPlaylist(assetId);
|
||||
const asset = await this.videoStreamRepository.getForMainPlaylist(assetId);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset is not yet ready for streaming');
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class HlsService extends BaseService {
|
||||
await this.pendingSessions.wait(sessionId);
|
||||
this.sessions.set(sessionId, { lastRequestedSegment: null });
|
||||
|
||||
return this.generateMasterPlaylist(sessionId, ffmpeg, asset);
|
||||
return this.generateMainPlaylist(sessionId, ffmpeg, asset);
|
||||
}
|
||||
|
||||
async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string) {
|
||||
@@ -114,7 +114,7 @@ export class HlsService extends BaseService {
|
||||
this.websocketRepository.serverSend('HlsSessionEnd', { sessionId });
|
||||
}
|
||||
|
||||
private generateMasterPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
||||
private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) {
|
||||
const fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3);
|
||||
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
|
||||
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`];
|
||||
|
||||
Reference in New Issue
Block a user