From ba2f1b984288808ccc2f9924821d46f642c17cb6 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Thu, 7 May 2026 14:57:09 -0400 Subject: [PATCH] main playlist --- mobile/openapi/README.md | 2 +- mobile/openapi/lib/api/assets_api.dart | 16 +++++++-------- open-api/immich-openapi-specs.json | 8 ++++---- open-api/typescript-sdk/src/fetch-client.ts | 6 +++--- .../controllers/video-stream.controller.ts | 10 +++++----- .../src/queries/video.stream.repository.sql | 2 +- .../repositories/video-stream.repository.ts | 2 +- server/src/services/hls.service.spec.ts | 20 +++++++++---------- server/src/services/hls.service.ts | 8 ++++---- 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 956edc6673..bdf054e0ee 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -110,7 +110,7 @@ Class | Method | HTTP request | Description *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics -*AssetsApi* | [**getMasterPlaylist**](doc//AssetsApi.md#getmasterplaylist) | **GET** /assets/{id}/video/stream/master.m3u8 | Get HLS master playlist +*AssetsApi* | [**getMainPlaylist**](doc//AssetsApi.md#getmainplaylist) | **GET** /assets/{id}/video/stream/main.m3u8 | Get HLS main playlist *AssetsApi* | [**getMediaPlaylist**](doc//AssetsApi.md#getmediaplaylist) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8 | Get HLS media playlist *AssetsApi* | [**getSegment**](doc//AssetsApi.md#getsegment) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename} | Get HLS segment or init file *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 126035be56..66e24e969d 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -878,9 +878,9 @@ class AssetsApi { return null; } - /// Get HLS master playlist + /// Get HLS main playlist /// - /// Returns an HLS master playlist with all available variants for the asset. + /// Returns an HLS main playlist with all available variants for the asset. /// /// Note: This method returns the HTTP [Response]. /// @@ -891,9 +891,9 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getMasterPlaylistWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getMainPlaylistWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/video/stream/master.m3u8' + final apiPath = r'/assets/{id}/video/stream/main.m3u8' .replaceAll('{id}', id); // ignore: prefer_final_locals @@ -924,9 +924,9 @@ class AssetsApi { ); } - /// Get HLS master playlist + /// Get HLS main playlist /// - /// Returns an HLS master playlist with all available variants for the asset. + /// Returns an HLS main playlist with all available variants for the asset. /// /// Parameters: /// @@ -935,8 +935,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getMasterPlaylist(String id, { String? key, String? slug, }) async { - final response = await getMasterPlaylistWithHttpInfo(id, key: key, slug: slug, ); + Future getMainPlaylist(String id, { String? key, String? slug, }) async { + final response = await getMainPlaylistWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5e95bfd98b..23148fc29f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4300,10 +4300,10 @@ "x-immich-state": "Stable" } }, - "/assets/{id}/video/stream/master.m3u8": { + "/assets/{id}/video/stream/main.m3u8": { "get": { - "description": "Returns an HLS master playlist with all available variants for the asset.", - "operationId": "getMasterPlaylist", + "description": "Returns an HLS main playlist with all available variants for the asset.", + "operationId": "getMainPlaylist", "parameters": [ { "name": "id", @@ -4355,7 +4355,7 @@ "api_key": [] } ], - "summary": "Get HLS master playlist", + "summary": "Get HLS main playlist", "tags": [ "Assets" ], diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index a434e1bf26..cd225beeee 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -4270,9 +4270,9 @@ export function playAssetVideo({ id, key, slug }: { })); } /** - * Get HLS master playlist + * Get HLS main playlist */ -export function getMasterPlaylist({ id, key, slug }: { +export function getMainPlaylist({ id, key, slug }: { id: string; key?: string; slug?: string; @@ -4280,7 +4280,7 @@ export function getMasterPlaylist({ id, key, slug }: { return oazapfts.ok(oazapfts.fetchBlob<{ status: 200; data: string; - }>(`/assets/${encodeURIComponent(id)}/video/stream/master.m3u8${QS.query(QS.explode({ + }>(`/assets/${encodeURIComponent(id)}/video/stream/main.m3u8${QS.query(QS.explode({ key, slug }))}`, { diff --git a/server/src/controllers/video-stream.controller.ts b/server/src/controllers/video-stream.controller.ts index 9eeb605932..423b2f3a09 100644 --- a/server/src/controllers/video-stream.controller.ts +++ b/server/src/controllers/video-stream.controller.ts @@ -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') diff --git a/server/src/queries/video.stream.repository.sql b/server/src/queries/video.stream.repository.sql index 5658db4724..714e138ce8 100644 --- a/server/src/queries/video.stream.repository.sql +++ b/server/src/queries/video.stream.repository.sql @@ -48,7 +48,7 @@ delete from "video_stream_session" where "id" = $1 --- VideoStreamRepository.getForMasterPlaylist +-- VideoStreamRepository.getForMainPlaylist select ( select diff --git a/server/src/repositories/video-stream.repository.ts b/server/src/repositories/video-stream.repository.ts index c14179a358..43c5ef80f0 100644 --- a/server/src/repositories/video-stream.repository.ts +++ b/server/src/repositories/video-stream.repository.ts @@ -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') diff --git a/server/src/services/hls.service.spec.ts b/server/src/services/hls.service.spec.ts index 042251e24a..7a39c2905e 100644 --- a/server/src/services/hls.service.spec.ts +++ b/server/src/services/hls.service.spec.ts @@ -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); }); }); diff --git a/server/src/services/hls.service.ts b/server/src/services/hls.service.ts index ac2ff98537..949aab12fb 100644 --- a/server/src/services/hls.service.ts +++ b/server/src/services/hls.service.ts @@ -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}`];