main playlist

This commit is contained in:
mertalev
2026-05-07 14:57:09 -04:00
parent e058ef868d
commit ba2f1b9842
9 changed files with 37 additions and 37 deletions
+1 -1
View File
@@ -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* | [**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* | [**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* | [**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* | [**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* | [**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 *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video
+8 -8
View File
@@ -878,9 +878,9 @@ class AssetsApi {
return null; 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]. /// Note: This method returns the HTTP [Response].
/// ///
@@ -891,9 +891,9 @@ class AssetsApi {
/// * [String] key: /// * [String] key:
/// ///
/// * [String] slug: /// * [String] slug:
Future<Response> getMasterPlaylistWithHttpInfo(String id, { String? key, String? slug, }) async { Future<Response> getMainPlaylistWithHttpInfo(String id, { String? key, String? slug, }) async {
// ignore: prefer_const_declarations // 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); .replaceAll('{id}', id);
// ignore: prefer_final_locals // 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: /// Parameters:
/// ///
@@ -935,8 +935,8 @@ class AssetsApi {
/// * [String] key: /// * [String] key:
/// ///
/// * [String] slug: /// * [String] slug:
Future<String?> getMasterPlaylist(String id, { String? key, String? slug, }) async { Future<String?> getMainPlaylist(String id, { String? key, String? slug, }) async {
final response = await getMasterPlaylistWithHttpInfo(id, key: key, slug: slug, ); final response = await getMainPlaylistWithHttpInfo(id, key: key, slug: slug, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
+4 -4
View File
@@ -4300,10 +4300,10 @@
"x-immich-state": "Stable" "x-immich-state": "Stable"
} }
}, },
"/assets/{id}/video/stream/master.m3u8": { "/assets/{id}/video/stream/main.m3u8": {
"get": { "get": {
"description": "Returns an HLS master playlist with all available variants for the asset.", "description": "Returns an HLS main playlist with all available variants for the asset.",
"operationId": "getMasterPlaylist", "operationId": "getMainPlaylist",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@@ -4355,7 +4355,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Get HLS master playlist", "summary": "Get HLS main playlist",
"tags": [ "tags": [
"Assets" "Assets"
], ],
+3 -3
View File
@@ -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; id: string;
key?: string; key?: string;
slug?: string; slug?: string;
@@ -4280,7 +4280,7 @@ export function getMasterPlaylist({ id, key, slug }: {
return oazapfts.ok(oazapfts.fetchBlob<{ return oazapfts.ok(oazapfts.fetchBlob<{
status: 200; status: 200;
data: string; 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, key,
slug slug
}))}`, { }))}`, {
@@ -20,18 +20,18 @@ export class VideoStreamController {
private service: HlsService, private service: HlsService,
) {} ) {}
@Get(':id/video/stream/master.m3u8') @Get(':id/video/stream/main.m3u8')
@Authenticated({ permission: Permission.AssetView, sharedLink: true }) @Authenticated({ permission: Permission.AssetView, sharedLink: true })
@Header('Cache-Control', 'no-cache') @Header('Cache-Control', 'no-cache')
@Header('Content-Type', PLAYLIST_CONTENT_TYPE) @Header('Content-Type', PLAYLIST_CONTENT_TYPE)
@ApiProduces(PLAYLIST_CONTENT_TYPE) @ApiProduces(PLAYLIST_CONTENT_TYPE)
@Endpoint({ @Endpoint({
summary: 'Get HLS master playlist', summary: 'Get HLS main playlist',
description: 'Returns an HLS master playlist with all available variants for the asset.', description: 'Returns an HLS main playlist with all available variants for the asset.',
history: new HistoryBuilder().added('v3').alpha('v3'), history: new HistoryBuilder().added('v3').alpha('v3'),
}) })
getMasterPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { getMainPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.getMasterPlaylist(auth, id); return this.service.getMainPlaylist(auth, id);
} }
@Get(':id/video/stream/:sessionId/:variantIndex/playlist.m3u8') @Get(':id/video/stream/:sessionId/:variantIndex/playlist.m3u8')
@@ -48,7 +48,7 @@ delete from "video_stream_session"
where where
"id" = $1 "id" = $1
-- VideoStreamRepository.getForMasterPlaylist -- VideoStreamRepository.getForMainPlaylist
select select
( (
select select
@@ -72,7 +72,7 @@ export class VideoStreamRepository {
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
async getForMasterPlaylist(id: string) { async getForMainPlaylist(id: string) {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
+10 -10
View File
@@ -167,14 +167,14 @@ describe(HlsService.name, () => {
({ sut, mocks } = newTestService(HlsService)); ({ sut, mocks } = newTestService(HlsService));
}); });
describe('getMasterPlaylist', () => { describe('getMainPlaylist', () => {
const auth = factory.auth(); const auth = factory.auth();
const assetId = 'asset-1'; const assetId = 'asset-1';
const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => { const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } }); 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.crypto.randomUUID.mockReturnValue(sessionId);
mocks.websocket.serverSend.mockImplementation((event, ...rest) => { mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
if (event === 'HlsSessionRequest') { 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); 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); 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); 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 () => { it('throws BadRequestException when realtime transcoding is disabled', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: false } } }); 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 () => { it('throws NotFoundException when asset is not yet ready for streaming', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } }); 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);
}); });
}); });
+4 -4
View File
@@ -48,14 +48,14 @@ export class HlsService extends BaseService {
this.pendingSegments.complete(this.getSegmentKey(event), event); 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] }); await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });
const { ffmpeg } = await this.getConfig({ withCache: true }); const { ffmpeg } = await this.getConfig({ withCache: true });
if (!ffmpeg.realtime.enabled) { if (!ffmpeg.realtime.enabled) {
throw new BadRequestException('Real-time transcoding is not 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) { if (!asset) {
throw new NotFoundException('Asset is not yet ready for streaming'); throw new NotFoundException('Asset is not yet ready for streaming');
} }
@@ -65,7 +65,7 @@ export class HlsService extends BaseService {
await this.pendingSessions.wait(sessionId); await this.pendingSessions.wait(sessionId);
this.sessions.set(sessionId, { lastRequestedSegment: null }); 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) { async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string) {
@@ -114,7 +114,7 @@ export class HlsService extends BaseService {
this.websocketRepository.serverSend('HlsSessionEnd', { sessionId }); 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 fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3);
const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width); const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width);
const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`]; const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`];