From b3de4afc848e67e15a4e6bf9218e6de837b5b2e6 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:06:11 +0300 Subject: [PATCH] Migrate transcoding page to React --- .../controllers/encodingsettings.html | 407 -------- .../dashboard/controllers/encodingsettings.js | 309 ------ .../features/playback/constants/codecs.ts | 113 +++ src/apps/dashboard/routes/_asyncRoutes.ts | 1 + src/apps/dashboard/routes/_legacyRoutes.ts | 7 - .../dashboard/routes/libraries/display.tsx | 7 +- src/apps/dashboard/routes/libraries/nfo.tsx | 23 +- src/apps/dashboard/routes/playback/resume.tsx | 2 +- .../dashboard/routes/playback/streaming.tsx | 2 +- .../dashboard/routes/playback/transcoding.tsx | 899 ++++++++++++++++++ src/hooks/useNamedConfiguration.ts | 6 +- src/strings/en-us.json | 3 +- 12 files changed, 1031 insertions(+), 748 deletions(-) delete mode 100644 src/apps/dashboard/controllers/encodingsettings.html delete mode 100644 src/apps/dashboard/controllers/encodingsettings.js create mode 100644 src/apps/dashboard/features/playback/constants/codecs.ts create mode 100644 src/apps/dashboard/routes/playback/transcoding.tsx diff --git a/src/apps/dashboard/controllers/encodingsettings.html b/src/apps/dashboard/controllers/encodingsettings.html deleted file mode 100644 index 962c625ed2..0000000000 --- a/src/apps/dashboard/controllers/encodingsettings.html +++ /dev/null @@ -1,407 +0,0 @@ -
-
-
-
-
-
-

${Transcoding}

-
-
- -
- - -
- -
- -
${LabelVaapiDeviceHelp}
-
- -
- -
${LabelQsvDeviceHelp}
-
- -
-
-

${LabelEnableHardwareDecodingFor}

-
- - - - - - - - - -
-
- - -
-
- - -
-
- -
- -
${EnableEnhancedNvdecDecoderHelp}
-
- -
- -
- -
-

${LabelHardwareEncodingOptions}

-
- -
-
- - - -
-
-
- -
-

${LabelEncodingFormatOptions}

-
${EncodingFormatHelp}
-
- -
-
- -
-
- -
-
- -
${AllowVppTonemappingHelp}
-
-
- -
${LabelVppTonemappingBrightnessHelp}
-
-
- -
${LabelVppTonemappingContrastHelp}
-
-
- -
-
- -
${AllowVideoToolboxTonemappingHelp}
-
-
- -
-
- -
${AllowTonemappingHelp}
-
${AllowTonemappingSoftwareHelp}
-
-
- - -
-
- -
${TonemappingModeHelp}
-
-
- -
${TonemappingRangeHelp}
-
-
- -
${LabelTonemappingDesatHelp}
-
-
- -
${LabelTonemappingPeakHelp}
-
-
- -
${LabelTonemappingParamHelp}
-
-
- -
- -
${LabelTranscodingThreadCountHelp}
-
- -
-
-
- -
-
-
-
${LabelffmpegPathHelp}
-
-
-
-
-
- -
- -
-
${LabelTranscodingTempPathHelp}
-
-
-
-
- -
- -
- -
-
- -
${EnableFallbackFontHelp}
-
-
- -
${LabelEnableAudioVbrHelp}
-
-
- -
${LabelDownMixAudioScaleHelp}
-
-
- -
${StereoDownmixAlgorithmHelp}
-
-
- -
${LabelMaxMuxingQueueSizeHelp}
-
- -
- -
${EncoderPresetHelp}
-
-
- -
-
- -
${H264CrfHelp}
-
-
- -
${DeinterlaceMethodHelp}
-
- -
- -
${UseDoubleRateDeinterlacingHelp}
-
- -
- -
${AllowOnTheFlySubtitleExtractionHelp}
-
- -
- -
${AllowFfmpegThrottlingHelp}
-
- -
- -
${AllowSegmentDeletionHelp}
-
- -
- -
${LabelThrottleDelaySecondsHelp}
-
- -
- -
${LabelSegmentKeepSecondsHelp}
-
- -
- -
-
-
-
-
diff --git a/src/apps/dashboard/controllers/encodingsettings.js b/src/apps/dashboard/controllers/encodingsettings.js deleted file mode 100644 index 6ae4fdbb28..0000000000 --- a/src/apps/dashboard/controllers/encodingsettings.js +++ /dev/null @@ -1,309 +0,0 @@ -import 'jquery'; -import loading from 'components/loading/loading'; -import globalize from 'lib/globalize'; -import dom from 'scripts/dom'; -import Dashboard from 'utils/dashboard'; -import alert from 'components/alert'; - -function loadPage(page, config, systemInfo) { - Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) { - c.checked = (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute('data-codec')) !== -1; - }); - page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc; - page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9; - page.querySelector('#chkDecodingColorDepth10HevcRext').checked = config.EnableDecodingColorDepth10HevcRext; - page.querySelector('#chkDecodingColorDepth12HevcRext').checked = config.EnableDecodingColorDepth12HevcRext; - page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder; - page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder; - page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder; - page.querySelector('#chkIntelLpHevcHwEncoder').checked = config.EnableIntelLowPowerHevcHwEncoder; - page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding; - page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding; - page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding; - page.querySelector('#selectVideoDecoder').value = config.HardwareAccelerationType || 'none'; - page.querySelector('#selectThreadCount').value = config.EncodingThreadCount; - page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr; - page.querySelector('#txtDownMixAudioBoost').value = config.DownMixAudioBoost; - page.querySelector('#selectStereoDownmixAlgorithm').value = config.DownMixStereoAlgorithm || 'None'; - page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || ''; - page.querySelector('.txtEncoderPath').value = config.EncoderAppPathDisplay || ''; - page.querySelector('#txtTranscodingTempPath').value = systemInfo.TranscodingTempPath || ''; - page.querySelector('#txtFallbackFontPath').value = config.FallbackFontPath || ''; - page.querySelector('#chkEnableFallbackFont').checked = config.EnableFallbackFont; - page.querySelector('#txtVaapiDevice').value = config.VaapiDevice || ''; - page.querySelector('#txtQsvDevice').value = config.QsvDevice || ''; - page.querySelector('#chkTonemapping').checked = config.EnableTonemapping; - page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping; - page.querySelector('#chkVideoToolboxTonemapping').checked = config.EnableVideoToolboxTonemapping; - page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm || 'none'; - page.querySelector('#selectTonemappingMode').value = config.TonemappingMode || 'auto'; - page.querySelector('#selectTonemappingRange').value = config.TonemappingRange || 'auto'; - page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat; - page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak; - page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || ''; - page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness; - page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast; - page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || 'auto'; - page.querySelector('#txtH264Crf').value = config.H264Crf || ''; - page.querySelector('#txtH265Crf').value = config.H265Crf || ''; - page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || 'yadif'; - page.querySelector('#chkDoubleRateDeinterlacing').checked = config.DeinterlaceDoubleRate; - page.querySelector('#chkEnableSubtitleExtraction').checked = config.EnableSubtitleExtraction || false; - page.querySelector('#chkEnableThrottling').checked = config.EnableThrottling || false; - page.querySelector('#chkEnableSegmentDeletion').checked = config.EnableSegmentDeletion || false; - page.querySelector('#txtThrottleDelaySeconds').value = config.ThrottleDelaySeconds || ''; - page.querySelector('#txtSegmentKeepSeconds').value = config.SegmentKeepSeconds || ''; - page.querySelector('#selectVideoDecoder').dispatchEvent(new CustomEvent('change', { - bubbles: true - })); - loading.hide(); -} - -function onSaveEncodingPathFailure() { - loading.hide(); - alert(globalize.translate('FFmpegSavePathNotFound')); -} - -function updateEncoder(form) { - return ApiClient.getSystemInfo().then(function () { - return ApiClient.ajax({ - url: ApiClient.getUrl('System/MediaEncoder/Path'), - type: 'POST', - data: JSON.stringify({ - Path: form.querySelector('.txtEncoderPath').value, - PathType: 'Custom' - }), - contentType: 'application/json' - }).then(Dashboard.processServerConfigurationUpdateResult, onSaveEncodingPathFailure); - }); -} - -function onSubmit() { - const form = this; - - const onDecoderConfirmed = function () { - loading.show(); - ApiClient.getNamedConfiguration('encoding').then(function (config) { - config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked; - config.DownMixAudioBoost = form.querySelector('#txtDownMixAudioBoost').value; - config.DownMixStereoAlgorithm = form.querySelector('#selectStereoDownmixAlgorithm').value || 'None'; - config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value; - config.TranscodingTempPath = form.querySelector('#txtTranscodingTempPath').value; - config.FallbackFontPath = form.querySelector('#txtFallbackFontPath').value; - config.EnableFallbackFont = form.querySelector('#txtFallbackFontPath').value ? form.querySelector('#chkEnableFallbackFont').checked : false; - config.EncodingThreadCount = form.querySelector('#selectThreadCount').value; - config.HardwareAccelerationType = form.querySelector('#selectVideoDecoder').value; - config.VaapiDevice = form.querySelector('#txtVaapiDevice').value; - config.QsvDevice = form.querySelector('#txtQsvDevice').value; - config.EnableTonemapping = form.querySelector('#chkTonemapping').checked; - config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked; - config.EnableVideoToolboxTonemapping = form.querySelector('#chkVideoToolboxTonemapping').checked; - config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value; - config.TonemappingMode = form.querySelector('#selectTonemappingMode').value; - config.TonemappingRange = form.querySelector('#selectTonemappingRange').value; - config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value; - config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value; - config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0'; - config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value; - config.VppTonemappingContrast = form.querySelector('#txtVppTonemappingContrast').value; - config.EncoderPreset = form.querySelector('#selectEncoderPreset').value; - config.H264Crf = parseInt(form.querySelector('#txtH264Crf').value || '0', 10); - config.H265Crf = parseInt(form.querySelector('#txtH265Crf').value || '0', 10); - config.DeinterlaceMethod = form.querySelector('#selectDeinterlaceMethod').value; - config.DeinterlaceDoubleRate = form.querySelector('#chkDoubleRateDeinterlacing').checked; - config.EnableSubtitleExtraction = form.querySelector('#chkEnableSubtitleExtraction').checked; - config.EnableThrottling = form.querySelector('#chkEnableThrottling').checked; - config.EnableSegmentDeletion = form.querySelector('#chkEnableSegmentDeletion').checked; - config.ThrottleDelaySeconds = parseInt(form.querySelector('#txtThrottleDelaySeconds').value || '0', 10); - config.SegmentKeepSeconds = parseInt(form.querySelector('#txtSegmentKeepSeconds').value || '0', 10); - config.HardwareDecodingCodecs = Array.prototype.map.call(Array.prototype.filter.call(form.querySelectorAll('.chkDecodeCodec'), function (c) { - return c.checked; - }), function (c) { - return c.getAttribute('data-codec'); - }); - config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked; - config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked; - config.EnableDecodingColorDepth10HevcRext = form.querySelector('#chkDecodingColorDepth10HevcRext').checked; - config.EnableDecodingColorDepth12HevcRext = form.querySelector('#chkDecodingColorDepth12HevcRext').checked; - config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked; - config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked; - config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked; - config.EnableIntelLowPowerHevcHwEncoder = form.querySelector('#chkIntelLpHevcHwEncoder').checked; - config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked; - config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked; - config.AllowAv1Encoding = form.querySelector('#chkAllowAv1Encoding').checked; - ApiClient.updateNamedConfiguration('encoding', config).then(function () { - updateEncoder(form); - }, function () { - alert(globalize.translate('ErrorDefault')); - Dashboard.processServerConfigurationUpdateResult(); - }); - }); - }; - - if (form.querySelector('#selectVideoDecoder').value !== 'none') { - alert({ - title: globalize.translate('TitleHardwareAcceleration'), - text: globalize.translate('HardwareAccelerationWarning') - }).then(onDecoderConfirmed); - } else { - onDecoderConfirmed(); - } - - return false; -} - -function setDecodingCodecsVisible(context, value) { - value = value || ''; - let any; - Array.prototype.forEach.call(context.querySelectorAll('.chkDecodeCodec'), function (c) { - if (c.getAttribute('data-types').split(',').indexOf(value) === -1) { - dom.parentWithTag(c, 'LABEL').classList.add('hide'); - } else { - dom.parentWithTag(c, 'LABEL').classList.remove('hide'); - any = true; - } - }); - - if (any) { - context.querySelector('.decodingCodecsList').classList.remove('hide'); - } else { - context.querySelector('.decodingCodecsList').classList.add('hide'); - } -} - -let systemInfo; -function getSystemInfo() { - return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then( - info => { - systemInfo = info; - return info; - } - ); -} - -$(document).on('pageinit', '#encodingSettingsPage', function () { - const page = this; - getSystemInfo(); - page.querySelector('#selectVideoDecoder').addEventListener('change', function () { - if (this.value == 'vaapi') { - page.querySelector('.fldVaapiDevice').classList.remove('hide'); - page.querySelector('#txtVaapiDevice').setAttribute('required', 'required'); - } else { - page.querySelector('.fldVaapiDevice').classList.add('hide'); - page.querySelector('#txtVaapiDevice').removeAttribute('required'); - } - - if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp') { - page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide'); - } else { - page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide'); - } - - if (this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') { - page.querySelector('.fldHevcRextHwDecoding').classList.remove('hide'); - } else { - page.querySelector('.fldHevcRextHwDecoding').classList.add('hide'); - } - - const isHwaSelected = [ 'amf', 'nvenc', 'qsv', 'vaapi', 'rkmpp', 'videotoolbox' ].includes(this.value); - if (this.value === 'none') { - page.querySelector('.tonemappingOptions').classList.remove('hide'); - page.querySelector('.fldTonemapCheckbox').classList.add('hide'); - } else if (isHwaSelected) { - page.querySelector('.tonemappingOptions').classList.remove('hide'); - page.querySelector('.fldTonemapCheckbox').classList.remove('hide'); - } else { - page.querySelector('.tonemappingOptions').classList.add('hide'); - page.querySelector('.fldTonemapCheckbox').classList.add('hide'); - } - - page.querySelector('.tonemappingModeOptions').classList.toggle('hide', !isHwaSelected); - page.querySelector('.allowTonemappingHardwareHelp').classList.toggle('hide', !isHwaSelected); - page.querySelector('.allowTonemappingSoftwareHelp').classList.toggle('hide', isHwaSelected); - - if (this.value == 'qsv' || this.value == 'vaapi') { - page.querySelector('.fldIntelLp').classList.remove('hide'); - } else { - page.querySelector('.fldIntelLp').classList.add('hide'); - } - - if (this.value === 'videotoolbox') { - page.querySelector('.videoToolboxTonemappingOptions').classList.remove('hide'); - } else { - page.querySelector('.videoToolboxTonemappingOptions').classList.add('hide'); - } - - if (this.value == 'qsv' || this.value == 'vaapi') { - page.querySelector('.vppTonemappingOptions').classList.remove('hide'); - } else { - page.querySelector('.vppTonemappingOptions').classList.add('hide'); - } - - if (this.value == 'qsv') { - page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide'); - page.querySelector('.fldQsvDevice').classList.remove('hide'); - } else { - page.querySelector('.fldSysNativeHwDecoder').classList.add('hide'); - page.querySelector('.fldQsvDevice').classList.add('hide'); - } - - if (this.value == 'nvenc') { - page.querySelector('.fldEnhancedNvdec').classList.remove('hide'); - } else { - page.querySelector('.fldEnhancedNvdec').classList.add('hide'); - } - - if (this.value !== 'none') { - page.querySelector('.hardwareAccelerationOptions').classList.remove('hide'); - } else { - page.querySelector('.hardwareAccelerationOptions').classList.add('hide'); - } - - setDecodingCodecsVisible(page, this.value); - }); - $('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - callback: function (path) { - if (path) { - page.querySelector('#txtTranscodingTempPath').value = path; - } - - picker.close(); - }, - validateWriteable: true, - header: globalize.translate('HeaderSelectTranscodingPath'), - instruction: globalize.translate('HeaderSelectTranscodingPathHelp') - }); - }); - }); - $('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - includeDirectories: true, - callback: function (path) { - if (path) { - page.querySelector('#txtFallbackFontPath').value = path; - } - - picker.close(); - }, - header: globalize.translate('HeaderSelectFallbackFontPath'), - instruction: globalize.translate('HeaderSelectFallbackFontPathHelp') - }); - }); - }); - $('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#encodingSettingsPage', function () { - loading.show(); - const page = this; - ApiClient.getNamedConfiguration('encoding').then(function (config) { - ApiClient.getSystemInfo().then(function (fetchedSystemInfo) { - loadPage(page, config, fetchedSystemInfo); - }); - }); -}); - diff --git a/src/apps/dashboard/features/playback/constants/codecs.ts b/src/apps/dashboard/features/playback/constants/codecs.ts new file mode 100644 index 0000000000..98b3e5fad5 --- /dev/null +++ b/src/apps/dashboard/features/playback/constants/codecs.ts @@ -0,0 +1,113 @@ +/** List of codecs and their supported hardware acceleration types */ +export const CODECS = [ + { + name: 'H264', + codec: 'h264', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp', + 'videotoolbox', + 'v4l2m2m' + ] + }, + { + name: 'HEVC', + codec: 'hevc', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp', + 'videotoolbox' + ] + }, + { + name: 'MPEG1', + codec: 'mpeg1video', + types: [ 'rkmpp' ] + }, + { + name: 'MPEG2', + codec: 'mpeg2video', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp' + ] + }, + { + name: 'MPEG4', + codec: 'mpeg4', + types: [ + 'nvenc', + 'rkmpp' + ] + }, + { + name: 'VC1', + codec: 'vc1', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi' + ] + }, + { + name: 'VP8', + codec: 'vp8', + types: [ + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp', + 'videotoolbox' + ] + }, + { + name: 'VP9', + codec: 'vp9', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp', + 'videotoolbox' + ] + }, + { + name: 'AV1', + codec: 'av1', + types: [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp', + 'videotoolbox' + ] + } +]; + +/** Hardware decoders which support 10-bit HEVC & VP9 */ +export const HEVC_VP9_HW_DECODING_TYPES = [ + 'amf', + 'nvenc', + 'qsv', + 'vaapi', + 'rkmpp' +]; + +/** Hardware decoders which support HEVC RExt */ +export const HEVC_REXT_DECODING_TYPES = [ + 'nvenc', + 'qsv', + 'vaapi' +]; diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 2ab21c5d4f..2fd77056f8 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -14,6 +14,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard }, { path: 'playback/resume', type: AppType.Dashboard }, { path: 'playback/streaming', type: AppType.Dashboard }, + { path: 'playback/transcoding', type: AppType.Dashboard }, { path: 'playback/trickplay', type: AppType.Dashboard }, { path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard }, { path: 'tasks', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index ceedb72651..e656d2d7c6 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -23,13 +23,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'library', view: 'library.html' } - }, { - path: 'playback/transcoding', - pageProps: { - appType: AppType.Dashboard, - controller: 'encodingsettings', - view: 'encodingsettings.html' - } }, { path: 'plugins/catalog', pageProps: { diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx index 9cd0852c9b..7e47367acd 100644 --- a/src/apps/dashboard/routes/libraries/display.tsx +++ b/src/apps/dashboard/routes/libraries/display.tsx @@ -14,12 +14,13 @@ import Loading from 'components/loading/LoadingComponent'; import Page from 'components/Page'; import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { QUERY_KEY as CONFIG_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; -import { QUERY_KEY as NAMED_CONFIG_QUERY_KEY, NamedConfiguration, useNamedConfiguration } from 'hooks/useNamedConfiguration'; +import { QUERY_KEY as NAMED_CONFIG_QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration'; import globalize from 'lib/globalize'; import { ServerConnections } from 'lib/jellyfin-apiclient'; import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; import { ActionData } from 'types/actionData'; import { queryClient } from 'utils/query/queryClient'; +import type { MetadataConfiguration } from '@jellyfin/sdk/lib/generated-client/models/metadata-configuration'; const CONFIG_KEY = 'metadata'; @@ -32,7 +33,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const { data: config } = await getConfigurationApi(api).getConfiguration(); - const metadataConfig: NamedConfiguration = { + const metadataConfig: MetadataConfiguration = { UseFileCreationTimeForDateAdded: data.DateAddedBehavior.toString() === '1' }; @@ -70,7 +71,7 @@ export const Component = () => { data: namedConfig, isPending: isNamedConfigPending, isError: isNamedConfigError - } = useNamedConfiguration(CONFIG_KEY); + } = useNamedConfiguration(CONFIG_KEY); const navigation = useNavigation(); const actionData = useActionData() as ActionData | undefined; diff --git a/src/apps/dashboard/routes/libraries/nfo.tsx b/src/apps/dashboard/routes/libraries/nfo.tsx index 326834a831..bbbaf6fdfd 100644 --- a/src/apps/dashboard/routes/libraries/nfo.tsx +++ b/src/apps/dashboard/routes/libraries/nfo.tsx @@ -21,17 +21,10 @@ import React, { useCallback, useState } from 'react'; import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; import { ActionData } from 'types/actionData'; import { queryClient } from 'utils/query/queryClient'; +import type { XbmcMetadataOptions } from '@jellyfin/sdk/lib/generated-client/models/xbmc-metadata-options'; const CONFIG_KEY = 'xbmcmetadata'; -interface NFOSettingsConfig { - UserId?: string; - EnableExtraThumbsDuplication?: boolean; - EnablePathSubstitution?: boolean; - ReleaseDateFormat?: string; - SaveImagePathsInNfo?: boolean; -}; - export const action = async ({ request }: ActionFunctionArgs) => { const api = ServerConnections.getCurrentApi(); if (!api) throw new Error('No Api instance available'); @@ -39,7 +32,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const data = Object.fromEntries(formData); - const newConfig: NFOSettingsConfig = { + const newConfig: XbmcMetadataOptions = { UserId: data.UserId?.toString(), ReleaseDateFormat: 'yyyy-MM-dd', SaveImagePathsInNfo: data.SaveImagePathsInNfo?.toString() === 'on', @@ -64,7 +57,7 @@ export const Component = () => { data: config, isPending: isConfigPending, isError: isConfigError - } = useNamedConfiguration(CONFIG_KEY); + } = useNamedConfiguration(CONFIG_KEY); const { data: users, isPending: isUsersPending, @@ -75,8 +68,6 @@ export const Component = () => { const isSubmitting = navigation.state === 'submitting'; const [isAlertOpen, setIsAlertOpen] = useState(false); - const nfoConfig = config as NFOSettingsConfig; - const onAlertClose = useCallback(() => { setIsAlertOpen(false); }, []); @@ -117,7 +108,7 @@ export const Component = () => { { control={ } label={globalize.translate('LabelKodiMetadataSaveImagePaths')} @@ -154,7 +145,7 @@ export const Component = () => { control={ } label={globalize.translate('LabelKodiMetadataEnablePathSubstitution')} @@ -167,7 +158,7 @@ export const Component = () => { control={ } label={globalize.translate('LabelKodiMetadataEnableExtraThumbs')} diff --git a/src/apps/dashboard/routes/playback/resume.tsx b/src/apps/dashboard/routes/playback/resume.tsx index 6bd23a8ce7..76e5fce9e6 100644 --- a/src/apps/dashboard/routes/playback/resume.tsx +++ b/src/apps/dashboard/routes/playback/resume.tsx @@ -66,7 +66,7 @@ export const Component = () => {
- + {globalize.translate('ButtonResume')} diff --git a/src/apps/dashboard/routes/playback/streaming.tsx b/src/apps/dashboard/routes/playback/streaming.tsx index b3be899d65..51c6962a42 100644 --- a/src/apps/dashboard/routes/playback/streaming.tsx +++ b/src/apps/dashboard/routes/playback/streaming.tsx @@ -57,7 +57,7 @@ export const Component = () => { - + {globalize.translate('TabStreaming')} diff --git a/src/apps/dashboard/routes/playback/transcoding.tsx b/src/apps/dashboard/routes/playback/transcoding.tsx new file mode 100644 index 0000000000..d6782a894c --- /dev/null +++ b/src/apps/dashboard/routes/playback/transcoding.tsx @@ -0,0 +1,899 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +import Page from 'components/Page'; +import globalize from 'lib/globalize'; +import Stack from '@mui/material/Stack'; +import Loading from 'components/loading/LoadingComponent'; +import MenuItem from '@mui/material/MenuItem'; +import FormGroup from '@mui/material/FormGroup'; +import Checkbox from '@mui/material/Checkbox'; +import FormHelperText from '@mui/material/FormHelperText'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import DirectoryBrowser from 'components/directorybrowser/directorybrowser'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; +import SearchIcon from '@mui/icons-material/Search'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation, useSubmit } from 'react-router-dom'; +import { QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration'; +import type { EncodingOptions } from '@jellyfin/sdk/lib/generated-client/models/encoding-options'; +import { HardwareAccelerationType } from '@jellyfin/sdk/lib/generated-client/models/hardware-acceleration-type'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import { queryClient } from 'utils/query/queryClient'; +import { ActionData } from 'types/actionData'; +import { CODECS, HEVC_REXT_DECODING_TYPES, HEVC_VP9_HW_DECODING_TYPES } from 'apps/dashboard/features/playback/constants/codecs'; +import SimpleAlert from 'components/SimpleAlert'; + +const CONFIG_KEY = 'encoding'; + +export const action = async ({ request }: ActionFunctionArgs) => { + const api = ServerConnections.getCurrentApi(); + if (!api) throw new Error('No Api instance available'); + + const data = await request.json() as EncodingOptions; + + await getConfigurationApi(api) + .updateNamedConfiguration({ key: CONFIG_KEY, body: data }); + + void queryClient.invalidateQueries({ + queryKey: [QUERY_KEY, CONFIG_KEY] + }); + + return { + isSaved: true + }; +}; + +export const Component = () => { + const { data: initialConfig, isPending, isError } = useNamedConfiguration(CONFIG_KEY); + const [ config, setConfig ] = useState(null); + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const submit = useSubmit(); + const isSubmitting = navigation.state === 'submitting'; + const [ isAlertOpen, setIsAlertOpen ] = useState(false); + + useEffect(() => { + if (initialConfig && config == null) { + setConfig(initialConfig); + } + }, [ initialConfig, config ]); + + const onConfigChange = useCallback((e: React.ChangeEvent) => { + setConfig({ + ...config, + [e.target.name]: e.target.value + }); + }, [ config ]); + + const onCheckboxChange = useCallback((e: React.ChangeEvent) => { + setConfig({ + ...config, + [e.target.name]: e.target.checked + }); + }, [ config ]); + + const onCodecChange = useCallback((e: React.ChangeEvent) => { + if (config?.HardwareDecodingCodecs) { + if (e.target.checked) { + setConfig({ + ...config, + HardwareDecodingCodecs: [ + ...config.HardwareDecodingCodecs, + e.target.name + ] + }); + } else { + setConfig({ + ...config, + HardwareDecodingCodecs: config.HardwareDecodingCodecs.filter(v => v !== e.target.name) + }); + } + } + }, [ config ]); + + const onAlertClose = useCallback(() => { + setIsAlertOpen(false); + }, []); + + const onSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (config) { + setIsAlertOpen(true); + submit( + { ...config }, + { method: 'post', encType: 'application/json' } + ); + } + }, [ config, submit ]); + + const showTranscodingPathPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + setConfig({ + ...config, + TranscodingTempPath: path + }); + + picker.close(); + }, + validateWriteable: true, + header: globalize.translate('HeaderSelectTranscodingPath'), + instruction: globalize.translate('HeaderSelectTranscodingPathHelp') + }); + }, [ config ]); + + const showFallbackFontPathPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + setConfig({ + ...config, + FallbackFontPath: path + }); + + picker.close(); + }, + header: globalize.translate('HeaderSelectFallbackFontPath'), + instruction: globalize.translate('HeaderSelectFallbackFontPathHelp') + }); + }, [ config ]); + + const hardwareAccelType = config?.HardwareAccelerationType || HardwareAccelerationType.None; + const isHwaSelected = [ 'amf', 'nvenc', 'qsv', 'vaapi', 'rkmpp', 'videotoolbox' ].includes(hardwareAccelType); + + const availableCodecs = useMemo(() => ( + CODECS.filter(codec => codec.types.includes(hardwareAccelType)) + ), [hardwareAccelType]); + + if (isPending || !config) return ; + + return ( + + + + {isError ? ( + {globalize.translate('TranscodingLoadError')} + ) : ( + + + {globalize.translate('Transcoding')} + + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + + + {globalize.translate('LabelHardwareAccelerationTypeHelp')} + + )} + > + {globalize.translate('None')} + AMD AMF + Nvidia NVENC + Intel Quicksync (QSV) + Video Acceleration API (VAAPI) + Rockchip MPP (RKMPP) + Apple VideoToolBox + Video4Linux2 (V4L2) + + + {hardwareAccelType === 'vaapi' && ( + + )} + + {hardwareAccelType === 'qsv' && ( + + )} + + {hardwareAccelType !== 'none' && ( + <> + {globalize.translate('LabelEnableHardwareDecodingFor')} + + {availableCodecs.map(codec => ( + + } + /> + ))} + + {HEVC_VP9_HW_DECODING_TYPES.includes(hardwareAccelType) && ( + + } + /> + )} + + {HEVC_VP9_HW_DECODING_TYPES.includes(hardwareAccelType) && ( + + } + /> + )} + + {HEVC_REXT_DECODING_TYPES.includes(hardwareAccelType) && ( + + } + /> + )} + + {HEVC_REXT_DECODING_TYPES.includes(hardwareAccelType) && ( + + } + /> + )} + + + )} + + {hardwareAccelType === 'nvenc' && ( + + + } + /> + {globalize.translate('EnableEnhancedNvdecDecoderHelp')} + + )} + + {hardwareAccelType === 'qsv' && ( + + + } + /> + + )} + + {hardwareAccelType !== 'none' && ( + + {globalize.translate('LabelHardwareEncodingOptions')} + + + } + /> + {(hardwareAccelType === 'qsv' || hardwareAccelType === 'vaapi') && ( + <> + + } + /> + + } + /> + + + {globalize.translate('IntelLowPowerEncHelp')} + + + + )} + + + )} + + + {globalize.translate('LabelEncodingFormatOptions')} + {globalize.translate('EncodingFormatHelp')} + + + } + /> + + } + /> + + + + {(hardwareAccelType === 'qsv' || hardwareAccelType === 'vaapi') && ( + <> + + + } + /> + {globalize.translate('AllowVppTonemappingHelp')} + + + + + + + )} + + {hardwareAccelType === 'videotoolbox' && ( + + + } + /> + {globalize.translate('AllowVideoToolboxTonemappingHelp')} + + )} + + {(hardwareAccelType === 'none' || isHwaSelected) && ( + <> + + + } + /> + {globalize.translate(isHwaSelected ? 'AllowTonemappingHelp' : 'AllowTonemappingSoftwareHelp')} + + + + {globalize.translate('TonemappingAlgorithmHelp')} + + )} + > + {globalize.translate('None')} + Clip + Linear + Gamma + Reinhard + Hable + Mobius + BT.2390 + + + {isHwaSelected && ( + + {globalize.translate('Auto')} + MAX + RGB + LUM + ITP + + )} + + + {globalize.translate('Auto')} + TV + PC + + + + + + + + + )} + + + {globalize.translate('Auto')} + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + {globalize.translate('OptionMax')} + + + + + + + + + + ) + } + }} + /> + + + {globalize.translate('LabelFallbackFontPathHelp')} + + } + slotProps={{ + input: { + endAdornment: ( + + + + + + ) + } + }} + /> + + + + } + /> + {globalize.translate('EnableFallbackFontHelp')} + + + + + } + /> + {globalize.translate('LabelEnableAudioVbrHelp')} + + + + + + {globalize.translate('None')} + Dave750 + NightmodeDialogue + RFC7845 + AC-4 + + + + + + {globalize.translate('Auto')} + veryslow + slower + slow + medium + fast + faster + veryfast + superfast + ultrafast + + + + + + + + {globalize.translate('Yadif')} + {globalize.translate('Bwdif')} + + + + + } + /> + {globalize.translate('UseDoubleRateDeinterlacingHelp')} + + + + + } + /> + {globalize.translate('AllowOnTheFlySubtitleExtractionHelp')} + + + + + } + /> + {globalize.translate('AllowFfmpegThrottlingHelp')} + + + + + } + /> + {globalize.translate('AllowSegmentDeletionHelp')} + + + + + + + + + + )} + + + ); +}; + +Component.displayName = 'TranscodingPage'; diff --git a/src/hooks/useNamedConfiguration.ts b/src/hooks/useNamedConfiguration.ts index 138355b608..c789a50910 100644 --- a/src/hooks/useNamedConfiguration.ts +++ b/src/hooks/useNamedConfiguration.ts @@ -13,15 +13,15 @@ export interface NamedConfiguration { const fetchNamedConfiguration = async (api: Api, key: string, options?: AxiosRequestConfig) => { const response = await getConfigurationApi(api).getNamedConfiguration({ key }, options); - return response.data as unknown as NamedConfiguration; + return response.data; }; -export const useNamedConfiguration = (key: string) => { +export const useNamedConfiguration = (key: string) => { const { api } = useApi(); return useQuery({ queryKey: [ QUERY_KEY, key ], - queryFn: ({ signal }) => fetchNamedConfiguration(api!, key, { signal }), + queryFn: ({ signal }) => fetchNamedConfiguration(api!, key, { signal }) as ConfigType, enabled: !!api }); }; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 5402157b2e..be432cfc72 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -977,7 +977,7 @@ "LabelTonemappingDesat": "Tone mapping desat", "LabelTonemappingDesatHelp": "Apply desaturation for highlights that exceed this level of brightness. The recommended value is 0 (disable).", "LabelTonemappingParam": "Tone mapping param", - "LabelTonemappingParamHelp": "Tune the tone mapping algorithm. The recommended and default values are NaN. Generally leave it blank.", + "LabelTonemappingParamHelp": "Tune the tone mapping algorithm. Generally leave it blank.", "LabelTonemappingPeak": "Tone mapping peak", "LabelTonemappingPeakHelp": "Override the embedded metadata value for the input signal with this peak value instead. The default value is 100 (1000nit).", "LabelTonemappingRange": "Tone mapping range", @@ -1608,6 +1608,7 @@ "TrackCount": "{0} tracks", "Trailers": "Trailers", "Transcoding": "Transcoding", + "TranscodingLoadError": "Failed to load transcoding settings", "Translator": "Translator", "Tuesday": "Tuesday", "TV": "TV",