1
0
mirror of https://github.com/immich-app/immich.git synced 2025-01-12 15:32:36 +02:00

feat(server, web): accepted codecs (#6460)

* chore: rebase

* chore: open api

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Markus 2024-01-26 18:02:56 +01:00 committed by GitHub
parent 96b7885583
commit 8aef92affc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 152 additions and 9 deletions

Binary file not shown.

View File

@ -9386,6 +9386,18 @@
"accel": { "accel": {
"$ref": "#/components/schemas/TranscodeHWAccel" "$ref": "#/components/schemas/TranscodeHWAccel"
}, },
"acceptedAudioCodecs": {
"items": {
"$ref": "#/components/schemas/AudioCodec"
},
"type": "array"
},
"acceptedVideoCodecs": {
"items": {
"$ref": "#/components/schemas/VideoCodec"
},
"type": "array"
},
"bframes": { "bframes": {
"type": "integer" "type": "integer"
}, },
@ -9437,6 +9449,8 @@
}, },
"required": [ "required": [
"accel", "accel",
"acceptedAudioCodecs",
"acceptedVideoCodecs",
"bframes", "bframes",
"cqMode", "cqMode",
"crf", "crf",

View File

@ -3670,6 +3670,18 @@ export interface SystemConfigFFmpegDto {
* @memberof SystemConfigFFmpegDto * @memberof SystemConfigFFmpegDto
*/ */
'accel': TranscodeHWAccel; 'accel': TranscodeHWAccel;
/**
*
* @type {Array<AudioCodec>}
* @memberof SystemConfigFFmpegDto
*/
'acceptedAudioCodecs': Array<AudioCodec>;
/**
*
* @type {Array<VideoCodec>}
* @memberof SystemConfigFFmpegDto
*/
'acceptedVideoCodecs': Array<VideoCodec>;
/** /**
* *
* @type {number} * @type {number}

View File

@ -2,6 +2,7 @@ import {
AssetEntity, AssetEntity,
AssetPathType, AssetPathType,
AssetType, AssetType,
AudioCodec,
Colorspace, Colorspace,
TranscodeHWAccel, TranscodeHWAccel,
TranscodePolicy, TranscodePolicy,
@ -326,9 +327,10 @@ export class MediaService {
containerExtension: string, containerExtension: string,
ffmpegConfig: SystemConfigFFmpegDto, ffmpegConfig: SystemConfigFFmpegDto,
): boolean { ): boolean {
const isTargetVideoCodec = videoStream.codecName === ffmpegConfig.targetVideoCodec; const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(videoStream.codecName as VideoCodec);
const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension); const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension);
const isTargetAudioCodec = audioStream == null || audioStream.codecName === ffmpegConfig.targetAudioCodec; const isTargetAudioCodec =
audioStream == null || ffmpegConfig.acceptedAudioCodecs.includes(audioStream.codecName as AudioCodec);
this.logger.verbose( this.logger.verbose(
`${asset.id}: AudioCodecName ${audioStream?.codecName ?? 'None'}, AudioStreamCodecType ${ `${asset.id}: AudioCodecName ${audioStream?.codecName ?? 'None'}, AudioStreamCodecType ${

View File

@ -24,10 +24,18 @@ export class SystemConfigFFmpegDto {
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
targetVideoCodec!: VideoCodec; targetVideoCodec!: VideoCodec;
@IsEnum(VideoCodec, { each: true })
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
acceptedVideoCodecs!: VideoCodec[];
@IsEnum(AudioCodec) @IsEnum(AudioCodec)
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
targetAudioCodec!: AudioCodec; targetAudioCodec!: AudioCodec;
@IsEnum(AudioCodec, { each: true })
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
acceptedAudioCodecs!: AudioCodec[];
@IsString() @IsString()
targetResolution!: string; targetResolution!: string;

View File

@ -31,7 +31,9 @@ export const defaults = Object.freeze<SystemConfig>({
threads: 0, threads: 0,
preset: 'ultrafast', preset: 'ultrafast',
targetVideoCodec: VideoCodec.H264, targetVideoCodec: VideoCodec.H264,
acceptedVideoCodecs: [VideoCodec.H264],
targetAudioCodec: AudioCodec.AAC, targetAudioCodec: AudioCodec.AAC,
acceptedAudioCodecs: [AudioCodec.AAC],
targetResolution: '720', targetResolution: '720',
maxBitrate: '0', maxBitrate: '0',
bframes: -1, bframes: -1,
@ -248,6 +250,14 @@ export class SystemConfigCore {
} }
} }
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
config.ffmpeg.acceptedVideoCodecs.unshift(config.ffmpeg.targetVideoCodec);
}
if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
config.ffmpeg.acceptedAudioCodecs.unshift(config.ffmpeg.targetAudioCodec);
}
return config; return config;
} }

View File

@ -43,8 +43,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
threads: 0, threads: 0,
preset: 'ultrafast', preset: 'ultrafast',
targetAudioCodec: AudioCodec.AAC, targetAudioCodec: AudioCodec.AAC,
acceptedAudioCodecs: [AudioCodec.AAC],
targetResolution: '720', targetResolution: '720',
targetVideoCodec: VideoCodec.H264, targetVideoCodec: VideoCodec.H264,
acceptedVideoCodecs: [VideoCodec.H264],
maxBitrate: '0', maxBitrate: '0',
bframes: -1, bframes: -1,
refs: 0, refs: 0,

View File

@ -18,7 +18,9 @@ export enum SystemConfigKey {
FFMPEG_THREADS = 'ffmpeg.threads', FFMPEG_THREADS = 'ffmpeg.threads',
FFMPEG_PRESET = 'ffmpeg.preset', FFMPEG_PRESET = 'ffmpeg.preset',
FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec', FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
FFMPEG_ACCEPTED_VIDEO_CODECS = 'ffmpeg.acceptedVideoCodecs',
FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec', FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
FFMPEG_ACCEPTED_AUDIO_CODECS = 'ffmpeg.acceptedAudioCodecs',
FFMPEG_TARGET_RESOLUTION = 'ffmpeg.targetResolution', FFMPEG_TARGET_RESOLUTION = 'ffmpeg.targetResolution',
FFMPEG_MAX_BITRATE = 'ffmpeg.maxBitrate', FFMPEG_MAX_BITRATE = 'ffmpeg.maxBitrate',
FFMPEG_BFRAMES = 'ffmpeg.bframes', FFMPEG_BFRAMES = 'ffmpeg.bframes',
@ -162,7 +164,9 @@ export interface SystemConfig {
threads: number; threads: number;
preset: string; preset: string;
targetVideoCodec: VideoCodec; targetVideoCodec: VideoCodec;
acceptedVideoCodecs: VideoCodec[];
targetAudioCodec: AudioCodec; targetAudioCodec: AudioCodec;
acceptedAudioCodecs: AudioCodec[];
targetResolution: string; targetResolution: string;
maxBitrate: string; maxBitrate: string;
bframes: number; bframes: number;

View File

@ -9,6 +9,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import type { SettingsEventType } from './admin-settings'; import type { SettingsEventType } from './admin-settings';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { cloneDeep } from 'lodash-es';
export let config: SystemConfigDto; export let config: SystemConfigDto;
@ -25,16 +26,17 @@
} }
}; };
const handleSave = async (config: Partial<SystemConfigDto>) => { const handleSave = async (update: Partial<SystemConfigDto>) => {
try { try {
const result = await api.systemConfigApi.updateConfig({ const { data: newConfig } = await api.systemConfigApi.updateConfig({
systemConfigDto: { systemConfigDto: {
...savedConfig, ...savedConfig,
...config, ...update,
}, },
}); });
savedConfig = { ...result.data }; config = cloneDeep(newConfig);
savedConfig = cloneDeep(newConfig);
notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
dispatch('save'); dispatch('save');

View File

@ -12,7 +12,8 @@
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSelect from '../setting-select.svelte'; import SettingSelect from '../setting-select.svelte';
import SettingSwitch from '../setting-switch.svelte'; import SettingSwitch from '../setting-switch.svelte';
import { isEqual } from 'lodash-es'; import SettingCheckboxes from '../setting-checkboxes.svelte';
import { isEqual, sortBy } from 'lodash-es';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import SettingAccordion from '../setting-accordion.svelte'; import SettingAccordion from '../setting-accordion.svelte';
import { mdiHelpCircleOutline } from '@mdi/js'; import { mdiHelpCircleOutline } from '@mdi/js';
@ -89,6 +90,21 @@
]} ]}
name="acodec" name="acodec"
isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec} isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec}
on:select={() => (config.ffmpeg.acceptedAudioCodecs = [config.ffmpeg.targetAudioCodec])}
/>
<SettingCheckboxes
label="ACCEPTED AUDIO CODECS"
{disabled}
desc="Select which audio codecs do not need to be transcoded. Only used for certain transcode policies."
bind:value={config.ffmpeg.acceptedAudioCodecs}
name="audioCodecs"
options={[
{ value: AudioCodec.Aac, text: 'AAC' },
{ value: AudioCodec.Mp3, text: 'MP3' },
{ value: AudioCodec.Libopus, text: 'Opus' },
]}
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedAudioCodecs), sortBy(savedConfig.ffmpeg.acceptedAudioCodecs))}
/> />
<SettingSelect <SettingSelect
@ -103,6 +119,21 @@
]} ]}
name="vcodec" name="vcodec"
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec} isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
/>
<SettingCheckboxes
label="ACCEPTED VIDEO CODECS"
{disabled}
desc="Select which video codecs do not need to be transcoded. Only used for certain transcode policies."
bind:value={config.ffmpeg.acceptedVideoCodecs}
name="videoCodecs"
options={[
{ value: VideoCodec.H264, text: 'H.264' },
{ value: VideoCodec.Hevc, text: 'HEVC' },
{ value: VideoCodec.Vp9, text: 'VP9' },
]}
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedVideoCodecs), sortBy(savedConfig.ffmpeg.acceptedVideoCodecs))}
/> />
<SettingSelect <SettingSelect
@ -150,11 +181,11 @@
{ value: TranscodePolicy.All, text: 'All videos' }, { value: TranscodePolicy.All, text: 'All videos' },
{ {
value: TranscodePolicy.Optimal, value: TranscodePolicy.Optimal,
text: 'Videos higher than target resolution or not in the desired format', text: 'Videos higher than target resolution or not in an accepted format',
}, },
{ {
value: TranscodePolicy.Required, value: TranscodePolicy.Required,
text: 'Only videos not in the desired format', text: 'Only videos not in an accepted format',
}, },
{ {
value: TranscodePolicy.Disabled, value: TranscodePolicy.Disabled,

View File

@ -0,0 +1,54 @@
<script lang="ts">
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let value: string[];
export let options: { value: string; text: string }[];
export let label = '';
export let desc = '';
export let name = '';
export let isEdited = false;
export let disabled = false;
function handleCheckboxChange(option: string) {
if (value.includes(option)) {
value = value.filter((item) => item !== option);
} else {
value = [...value, option];
}
}
</script>
<div class="mb-4 w-full">
<div class={`flex h-[26px] place-items-center gap-1`}>
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
{#if isEdited}
<div
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
>
Unsaved change
</div>
{/if}
</div>
{#if desc}
<p class="immich-form-label pb-2 text-sm" id="{name}-desc">
{desc}
</p>
{/if}
{#each options as option}
<label class="flex items-center mb-2">
<input
type="checkbox"
class="form-checkbox h-5 w-5 color"
{disabled}
checked={value.includes(option.value)}
on:change={() => handleCheckboxChange(option.value)}
/>
<span class="ml-2 text-sm text-gray-500 dark:text-gray-300 pt-1">{option.text}</span>
</label>
{/each}
</div>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
export let value: string | number; export let value: string | number;
export let options: { value: string | number; text: string }[]; export let options: { value: string | number; text: string }[];
@ -11,11 +12,14 @@
export let number = false; export let number = false;
export let disabled = false; export let disabled = false;
const dispatch = createEventDispatcher<{ select: string | number }>();
const handleChange = (e: Event) => { const handleChange = (e: Event) => {
value = (e.target as HTMLInputElement).value; value = (e.target as HTMLInputElement).value;
if (number) { if (number) {
value = parseInt(value); value = parseInt(value);
} }
dispatch('select', value);
}; };
</script> </script>