mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(server): accepted video containers (#11274)
* add accepted container config * update api * mp4 option makes no sense * add to transcoding settings * wording * updated spec config * formatting
This commit is contained in:
parent
7ecdcb3bc0
commit
9d2d556200
16 changed files with 127 additions and 29 deletions
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/video_container.dart
generated
Normal file
BIN
mobile/openapi/lib/model/video_container.dart
generated
Normal file
Binary file not shown.
|
@ -10691,6 +10691,12 @@
|
|||
},
|
||||
"type": "array"
|
||||
},
|
||||
"acceptedContainers": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/VideoContainer"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"acceptedVideoCodecs": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/VideoCodec"
|
||||
|
@ -10762,6 +10768,7 @@
|
|||
"accel",
|
||||
"accelDecode",
|
||||
"acceptedAudioCodecs",
|
||||
"acceptedContainers",
|
||||
"acceptedVideoCodecs",
|
||||
"bframes",
|
||||
"cqMode",
|
||||
|
@ -11847,6 +11854,15 @@
|
|||
"av1"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"VideoContainer": {
|
||||
"enum": [
|
||||
"mov",
|
||||
"mp4",
|
||||
"ogg",
|
||||
"webm"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -960,6 +960,7 @@ export type SystemConfigFFmpegDto = {
|
|||
accel: TranscodeHWAccel;
|
||||
accelDecode: boolean;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
acceptedContainers: VideoContainer[];
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
bframes: number;
|
||||
cqMode: CQMode;
|
||||
|
@ -3178,6 +3179,12 @@ export enum AudioCodec {
|
|||
Aac = "aac",
|
||||
Libopus = "libopus"
|
||||
}
|
||||
export enum VideoContainer {
|
||||
Mov = "mov",
|
||||
Mp4 = "mp4",
|
||||
Ogg = "ogg",
|
||||
Webm = "webm"
|
||||
}
|
||||
export enum VideoCodec {
|
||||
H264 = "h264",
|
||||
Hevc = "hevc",
|
||||
|
|
|
@ -37,6 +37,13 @@ export enum AudioCodec {
|
|||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum VideoContainer {
|
||||
MOV = 'mov',
|
||||
MP4 = 'mp4',
|
||||
OGG = 'ogg',
|
||||
WEBM = 'webm',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
|
@ -86,6 +93,7 @@ export interface SystemConfig {
|
|||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
acceptedContainers: VideoContainer[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
|
@ -218,6 +226,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.AAC,
|
||||
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
|
||||
acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/config';
|
||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
|
@ -79,6 +80,10 @@ export class SystemConfigFFmpegDto {
|
|||
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
|
||||
acceptedAudioCodecs!: AudioCodec[];
|
||||
|
||||
@IsEnum(VideoContainer, { each: true })
|
||||
@ApiProperty({ enumName: 'VideoContainer', enum: VideoContainer, isArray: true })
|
||||
acceptedContainers!: VideoContainer[];
|
||||
|
||||
@IsString()
|
||||
targetResolution!: string;
|
||||
|
||||
|
|
|
@ -957,6 +957,21 @@ describe(MediaService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should remux when input is not an accepted container', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamAvi);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.any(Array),
|
||||
outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy']),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception if transcode value is invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } });
|
||||
|
@ -973,6 +988,14 @@ describe(MediaService.name, () => {
|
|||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not remux when input is not an accepted container and transcoding is disabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not transcode if target codec is invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } });
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
TranscodePolicy,
|
||||
TranscodeTarget,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/config';
|
||||
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
|
@ -27,7 +28,7 @@ import {
|
|||
QueueName,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from 'src/interfaces/media.interface';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
|
@ -314,8 +315,7 @@ export class MediaService {
|
|||
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
||||
const mainVideoStream = this.getMainStream(videoStreams);
|
||||
const mainAudioStream = this.getMainStream(audioStreams);
|
||||
const containerExtension = format.formatName;
|
||||
if (!mainVideoStream || !containerExtension) {
|
||||
if (!mainVideoStream || !format.formatName) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
|
@ -326,7 +326,7 @@ export class MediaService {
|
|||
|
||||
const { ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
||||
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
|
||||
if (target === TranscodeTarget.NONE) {
|
||||
if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) {
|
||||
if (asset.encodedVideoPath) {
|
||||
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
|
||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } });
|
||||
|
@ -456,6 +456,15 @@ export class MediaService {
|
|||
}
|
||||
}
|
||||
|
||||
private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean {
|
||||
if (ffmpegConfig.transcode === TranscodePolicy.DISABLED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = formatLongName === 'QuickTime / MOV' ? VideoContainer.MOV : (formatName as VideoContainer);
|
||||
return name !== VideoContainer.MP4 && !ffmpegConfig.acceptedContainers.includes(name);
|
||||
}
|
||||
|
||||
isSRGB(asset: AssetEntity): boolean {
|
||||
const { colorspace, profileDescription, bitsPerSample } = asset.exifInfo ?? {};
|
||||
if (colorspace || profileDescription) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
defaults,
|
||||
} from 'src/config';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
|
@ -54,6 +55,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
targetResolution: '720',
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM],
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
|
|
10
server/test/fixtures/media.stub.ts
vendored
10
server/test/fixtures/media.stub.ts
vendored
|
@ -177,4 +177,14 @@ export const probeStub = {
|
|||
...probeStubDefault,
|
||||
videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }],
|
||||
}),
|
||||
videoStreamAvi: Object.freeze<VideoInfo>({
|
||||
...probeStubDefault,
|
||||
videoStreams: [{ ...probeStubDefaultVideoStream[0], codecName: 'h264' }],
|
||||
format: {
|
||||
formatName: 'avi',
|
||||
formatLongName: 'AVI (Audio Video Interleaved)',
|
||||
duration: 0,
|
||||
bitrate: 0,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
type SystemConfigDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiHelpCircleOutline } from '@mdi/js';
|
||||
|
@ -85,6 +86,22 @@
|
|||
isEdited={config.ffmpeg.preset !== savedConfig.ffmpeg.preset}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_video_codec')}
|
||||
{disabled}
|
||||
desc={$t('admin.transcoding_video_codec_description')}
|
||||
bind:value={config.ffmpeg.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
{ value: VideoCodec.Av1, text: 'av1' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
|
||||
on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_audio_codec')}
|
||||
{disabled}
|
||||
|
@ -103,6 +120,21 @@
|
|||
: config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)}
|
||||
/>
|
||||
|
||||
<SettingCheckboxes
|
||||
label={$t('admin.transcoding_accepted_video_codecs')}
|
||||
{disabled}
|
||||
desc={$t('admin.transcoding_accepted_video_codecs_description')}
|
||||
bind:value={config.ffmpeg.acceptedVideoCodecs}
|
||||
name="videoCodecs"
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'H.264' },
|
||||
{ value: VideoCodec.Hevc, text: 'HEVC' },
|
||||
{ value: VideoCodec.Vp9, text: 'VP9' },
|
||||
{ value: VideoCodec.Av1, text: 'AV1' },
|
||||
]}
|
||||
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedVideoCodecs), sortBy(savedConfig.ffmpeg.acceptedVideoCodecs))}
|
||||
/>
|
||||
|
||||
<SettingCheckboxes
|
||||
label={$t('admin.transcoding_accepted_audio_codecs')}
|
||||
{disabled}
|
||||
|
@ -117,35 +149,18 @@
|
|||
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedAudioCodecs), sortBy(savedConfig.ffmpeg.acceptedAudioCodecs))}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.transcoding_video_codec')}
|
||||
{disabled}
|
||||
desc={$t('admin.transcoding_video_codec_description')}
|
||||
bind:value={config.ffmpeg.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
{ value: VideoCodec.Av1, text: 'av1' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
|
||||
on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
|
||||
/>
|
||||
|
||||
<SettingCheckboxes
|
||||
label={$t('admin.transcoding_accepted_video_codecs')}
|
||||
label={$t('admin.transcoding_accepted_containers')}
|
||||
{disabled}
|
||||
desc={$t('admin.transcoding_accepted_video_codecs_description')}
|
||||
bind:value={config.ffmpeg.acceptedVideoCodecs}
|
||||
name="videoCodecs"
|
||||
desc={$t('admin.transcoding_accepted_containers_description')}
|
||||
bind:value={config.ffmpeg.acceptedContainers}
|
||||
name="videoContainers"
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'H.264' },
|
||||
{ value: VideoCodec.Hevc, text: 'HEVC' },
|
||||
{ value: VideoCodec.Vp9, text: 'VP9' },
|
||||
{ value: VideoCodec.Av1, text: 'AV1' },
|
||||
{ value: VideoContainer.Mov, text: 'MOV' },
|
||||
{ value: VideoContainer.Ogg, text: 'Ogg' },
|
||||
{ value: VideoContainer.Webm, text: 'WebM' },
|
||||
]}
|
||||
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedVideoCodecs), sortBy(savedConfig.ffmpeg.acceptedVideoCodecs))}
|
||||
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedContainers), sortBy(savedConfig.ffmpeg.acceptedContainers))}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
|
|
|
@ -246,6 +246,8 @@
|
|||
"transcoding_acceleration_vaapi": "VAAPI",
|
||||
"transcoding_accepted_audio_codecs": "Accepted audio codecs",
|
||||
"transcoding_accepted_audio_codecs_description": "Select which audio codecs do not need to be transcoded. Only used for certain transcode policies.",
|
||||
"transcoding_accepted_containers": "Accepted containers",
|
||||
"transcoding_accepted_containers_description": "Select which container formats do not need to be remuxed to MP4. Only used for certain transcode policies.",
|
||||
"transcoding_accepted_video_codecs": "Accepted video codecs",
|
||||
"transcoding_accepted_video_codecs_description": "Select which video codecs do not need to be transcoded. Only used for certain transcode policies.",
|
||||
"transcoding_advanced_options_description": "Options most users should not need to change",
|
||||
|
|
Loading…
Reference in a new issue