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:
Generated
+1
-1
@@ -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
|
||||||
|
|||||||
Generated
+8
-8
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}`];
|
||||||
|
|||||||
Reference in New Issue
Block a user