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:
parent
96b7885583
commit
8aef92affc
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
BIN
mobile/openapi/doc/SystemConfigFFmpegDto.md
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/test/system_config_f_fmpeg_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_f_fmpeg_dto_test.dart
generated
Binary file not shown.
@ -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",
|
||||||
|
12
open-api/typescript-sdk/client/api.ts
generated
12
open-api/typescript-sdk/client/api.ts
generated
@ -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}
|
||||||
|
@ -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 ${
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user