aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml8
-rw-r--r--.github/workflows/ci-compat.yml4
-rw-r--r--.github/workflows/ci-openapi.yml8
-rw-r--r--.github/workflows/ci-tests.yml4
-rw-r--r--.github/workflows/commands.yml6
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--.github/workflows/release-bump-version.yaml4
-rw-r--r--CONTRIBUTORS.md4
-rw-r--r--Directory.Packages.props6
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs4
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs990
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs6
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs38
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs35
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json50
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json30
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json8
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs29
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs118
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs168
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs10
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs79
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs11
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs52
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs75
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs7
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs1
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs2
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs46
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs15
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs33
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs5
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs5
-rw-r--r--Jellyfin.Data/Dtos/DeviceOptionsDto.cs33
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs89
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs111
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs712
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs36
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs7
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs185
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs1
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs245
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs10
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs85
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs104
-rw-r--r--Jellyfin.Server/StartupOptions.cs11
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationResult.cs33
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs150
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs26
-rw-r--r--MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs56
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs5
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs3
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs18
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs1181
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs9
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs17
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs36
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs5
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs7
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs12
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs120
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs36
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs88
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs13
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs7
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs38
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs28
-rw-r--r--MediaBrowser.Model/Configuration/MediaPathInfo.cs2
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPluginType.cs3
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.Model/Devices/DeviceInfo.cs119
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs136
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs107
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs109
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs79
-rw-r--r--MediaBrowser.Model/Dlna/MediaOptions.cs5
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs69
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs303
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs1680
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs84
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs196
-rw-r--r--MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs (renamed from Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs)20
-rw-r--r--MediaBrowser.Model/Dto/DeviceInfoDto.cs83
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs7
-rw-r--r--MediaBrowser.Model/Dto/PlaylistDto.cs26
-rw-r--r--MediaBrowser.Model/Dto/SessionInfoDto.cs186
-rw-r--r--MediaBrowser.Model/Entities/DeinterlaceMethod.cs19
-rw-r--r--MediaBrowser.Model/Entities/EncoderPreset.cs64
-rw-r--r--MediaBrowser.Model/Entities/HardwareAccelerationType.cs49
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs33
-rw-r--r--MediaBrowser.Model/Entities/TonemappingAlgorithm.cs49
-rw-r--r--MediaBrowser.Model/Entities/TonemappingMode.cs34
-rw-r--r--MediaBrowser.Model/Entities/TonemappingRange.cs24
-rw-r--r--MediaBrowser.Model/Extensions/ContainerHelper.cs145
-rw-r--r--MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs32
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs7
-rw-r--r--MediaBrowser.Model/LiveTv/TunerHostInfo.cs9
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs3
-rw-r--r--MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs14
-rw-r--r--MediaBrowser.Model/Session/HardwareEncodingType.cs43
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs1
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs78
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs9
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs10
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs15
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs74
-rw-r--r--MediaBrowser.Providers/TV/EpisodeMetadataService.cs18
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs13
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs15
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs3
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs119
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayProvider.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs2
-rw-r--r--src/Jellyfin.Extensions/FormattingStreamWriter.cs38
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs65
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs19
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs4
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs6
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs4
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs9
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs29
-rw-r--r--tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs23
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs16
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs83
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs19
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs244
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json389
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json445
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json322
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json355
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json95
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json97
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json71
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json82
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json94
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json97
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json100
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json86
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs47
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs1
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs27
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs2
185 files changed, 9340 insertions, 3265 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index cfb5a6ec2..b52241208 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.10+
+ - 10.9.11+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 513139ea5..f730408f2 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- name: Setup .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/autobuild@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
+ uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index c6e655d08..85f5d00ca 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -11,7 +11,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -35,7 +35,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index d01c506db..82804852a 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -16,7 +16,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -41,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
+ uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
+ uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index af8106c0a..eaa054ca9 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
with:
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@e3af7259842d9c814021ea121f85526e0872b25f # v5.3.9
+ uses: danielpalme/ReportGenerator-GitHub-Action@b7115d212c0f7814a0cb17fb43ec36983c707ccb # v5.3.10
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 4b469f0d7..d79d437d9 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index d89076595..ea4c31f61 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index 575f2d756..ac8bb2516 100644
--- a/.github/workflows/release-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index cdf8df17f..a9deb1c4a 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -65,6 +65,7 @@
- [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan)
- [justinfenn](https://github.com/justinfenn)
+ - [JPVenson](https://github.com/JPVenson)
- [KerryRJ](https://github.com/KerryRJ)
- [Larvitar](https://github.com/Larvitar)
- [LeoVerto](https://github.com/LeoVerto)
@@ -188,6 +189,9 @@
- [TheMelmacian](https://github.com/TheMelmacian)
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
- [pret0rian8](https://github.com/pret0rian)
+ - [jaina heartles](https://github.com/heartles)
+ - [oxixes](https://github.com/oxixes)
+ - [elfalem](https://github.com/elfalem)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 02937b193..1bc7c12d0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -55,7 +55,7 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
- <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
+ <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
@@ -80,12 +80,12 @@
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.3.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.5.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.9.0" />
+ <PackageVersion Include="xunit" Version="2.9.2" />
</ItemGroup>
</Project> \ No newline at end of file
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 51f29cf08..12bc22a6a 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -141,7 +141,9 @@ namespace Emby.Naming.Video
{
if (group.Key)
{
- videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+ videos.InsertRange(0, group
+ .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
+ .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
}
else
{
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index e86010513..91791a1c8 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -20,7 +20,9 @@ namespace Emby.Server.Implementations
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
- { FfmpegSkipValidationKey, bool.FalseString }
+ { FfmpegSkipValidationKey, bool.FalseString },
+ { FfmpegImgExtractPerfTradeoffKey, bool.FalseString },
+ { DetectNetworkChangeKey, bool.TrueString }
};
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index c7a8421c6..a2aeaf0fc 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -212,6 +210,949 @@ namespace Emby.Server.Implementations.Data
/// <inheritdoc />
protected override TempStoreMode TempStore => TempStoreMode.Memory;
+ /// <summary>
+ /// Opens the connection to the database.
+ /// </summary>
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ const string CreateMediaStreamsTableCommand
+ = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
+
+ const string CreateMediaAttachmentsTableCommand
+ = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
+
+ string[] queries =
+ {
+ "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
+
+ "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
+ "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
+ "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
+
+ "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
+
+ "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
+
+ "drop index if exists idxPeopleItemId",
+ "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
+ "create index if not exists idxPeopleName on People(Name)",
+
+ "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
+
+ CreateMediaStreamsTableCommand,
+ CreateMediaAttachmentsTableCommand,
+
+ "pragma shrink_memory"
+ };
+
+ string[] postQueries =
+ {
+ "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
+ "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
+
+ "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
+ "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
+ "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
+
+ // covering index
+ "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
+
+ // series
+ "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
+
+ // series counts
+ // seriesdateplayed sort order
+ "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
+
+ // live tv programs
+ "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
+
+ // covering index for getitemvalues
+ "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
+
+ // used by movie suggestions
+ "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
+ "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
+
+ // latest items
+ "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
+ "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
+
+ // resume
+ "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
+
+ // items by name
+ "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
+ "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
+
+ // Used to update inherited tags
+ "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
+
+ "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)",
+ "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)"
+ };
+
+ using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
+ {
+ connection.Execute(string.Join(';', queries));
+
+ var existingColumnNames = GetColumnNames(connection, "AncestorIds");
+ AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
+
+ AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "ItemValues");
+ AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, ChaptersTableName);
+ AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "MediaStreams");
+ AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames);
+
+ connection.Execute(string.Join(';', postQueries));
+
+ transaction.Commit();
+ }
+ }
+
+ /// <inheritdoc />
+ public void SaveImages(BaseItem item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ CheckDisposed();
+
+ var images = SerializeImages(item.ImageInfos);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
+ saveImagesStatement.TryBind("@Id", item.Id);
+ saveImagesStatement.TryBind("@Images", images);
+
+ saveImagesStatement.ExecuteNonQuery();
+ transaction.Commit();
+ }
+
+ /// <summary>
+ /// Saves the items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <exception cref="ArgumentNullException">
+ /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
+ /// </exception>
+ public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(items);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ CheckDisposed();
+
+ var itemsLen = items.Count;
+ var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen];
+ for (int i = 0; i < itemsLen; i++)
+ {
+ var item = items[i];
+ var ancestorIds = item.SupportsAncestors ?
+ item.GetAncestorIds().Distinct().ToList() :
+ null;
+
+ var topParent = item.GetTopParent();
+
+ var userdataKey = item.GetUserDataKeys().FirstOrDefault();
+ var inheritedTags = item.GetInheritedTags();
+
+ tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
+ }
+
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ SaveItemsInTransaction(connection, tuples);
+ transaction.Commit();
+ }
+
+ private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
+ {
+ using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
+ using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
+ {
+ var requiresReset = false;
+ foreach (var tuple in tuples)
+ {
+ if (requiresReset)
+ {
+ saveItemStatement.Parameters.Clear();
+ deleteAncestorsStatement.Parameters.Clear();
+ }
+
+ var item = tuple.Item;
+ var topParent = tuple.TopParent;
+ var userDataKey = tuple.UserDataKey;
+
+ SaveItem(item, topParent, userDataKey, saveItemStatement);
+
+ var inheritedTags = tuple.InheritedTags;
+
+ if (item.SupportsAncestors)
+ {
+ UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement);
+ }
+
+ UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db);
+
+ requiresReset = true;
+ }
+ }
+ }
+
+ private string GetPathToSave(string path)
+ {
+ if (path is null)
+ {
+ return null;
+ }
+
+ return _appHost.ReverseVirtualPath(path);
+ }
+
+ private string RestorePath(string path)
+ {
+ return _appHost.ExpandVirtualPath(path);
+ }
+
+ private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
+ {
+ Type type = item.GetType();
+
+ saveItemStatement.TryBind("@guid", item.Id);
+ saveItemStatement.TryBind("@type", type.FullName);
+
+ if (TypeRequiresDeserialization(type))
+ {
+ saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@data");
+ }
+
+ saveItemStatement.TryBind("@Path", GetPathToSave(item.Path));
+
+ if (item is IHasStartDate hasStartDate)
+ {
+ saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@StartDate");
+ }
+
+ if (item.EndDate.HasValue)
+ {
+ saveItemStatement.TryBind("@EndDate", item.EndDate.Value);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@EndDate");
+ }
+
+ saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
+
+ if (item is IHasProgramAttributes hasProgramAttributes)
+ {
+ saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie);
+ saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries);
+ saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle);
+ saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@IsMovie");
+ saveItemStatement.TryBindNull("@IsSeries");
+ saveItemStatement.TryBindNull("@EpisodeTitle");
+ saveItemStatement.TryBindNull("@IsRepeat");
+ }
+
+ saveItemStatement.TryBind("@CommunityRating", item.CommunityRating);
+ saveItemStatement.TryBind("@CustomRating", item.CustomRating);
+ saveItemStatement.TryBind("@IndexNumber", item.IndexNumber);
+ saveItemStatement.TryBind("@IsLocked", item.IsLocked);
+ saveItemStatement.TryBind("@Name", item.Name);
+ saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
+ saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
+ saveItemStatement.TryBind("@Overview", item.Overview);
+ saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
+ saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
+ saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
+
+ var parentId = item.ParentId;
+ if (parentId.IsEmpty())
+ {
+ saveItemStatement.TryBindNull("@ParentId");
+ }
+ else
+ {
+ saveItemStatement.TryBind("@ParentId", parentId);
+ }
+
+ if (item.Genres.Length > 0)
+ {
+ saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Genres");
+ }
+
+ saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
+
+ saveItemStatement.TryBind("@SortName", item.SortName);
+
+ saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName);
+
+ saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks);
+ saveItemStatement.TryBind("@Size", item.Size);
+
+ saveItemStatement.TryBind("@DateCreated", item.DateCreated);
+ saveItemStatement.TryBind("@DateModified", item.DateModified);
+
+ saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage);
+ saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode);
+
+ if (item.Width > 0)
+ {
+ saveItemStatement.TryBind("@Width", item.Width);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Width");
+ }
+
+ if (item.Height > 0)
+ {
+ saveItemStatement.TryBind("@Height", item.Height);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Height");
+ }
+
+ if (item.DateLastRefreshed != default(DateTime))
+ {
+ saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@DateLastRefreshed");
+ }
+
+ if (item.DateLastSaved != default(DateTime))
+ {
+ saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@DateLastSaved");
+ }
+
+ saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder);
+
+ if (item.LockedFields.Length > 0)
+ {
+ saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@LockedFields");
+ }
+
+ if (item.Studios.Length > 0)
+ {
+ saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Studios");
+ }
+
+ if (item.Audio.HasValue)
+ {
+ saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString());
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Audio");
+ }
+
+ if (item is LiveTvChannel liveTvChannel)
+ {
+ saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@ExternalServiceId");
+ }
+
+ if (item.Tags.Length > 0)
+ {
+ saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@Tags");
+ }
+
+ saveItemStatement.TryBind("@IsFolder", item.IsFolder);
+
+ saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString());
+
+ if (topParent is null)
+ {
+ saveItemStatement.TryBindNull("@TopParentId");
+ }
+ else
+ {
+ saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture));
+ }
+
+ if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
+ {
+ saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@TrailerTypes");
+ }
+
+ saveItemStatement.TryBind("@CriticRating", item.CriticRating);
+
+ if (string.IsNullOrWhiteSpace(item.Name))
+ {
+ saveItemStatement.TryBindNull("@CleanName");
+ }
+ else
+ {
+ saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name));
+ }
+
+ saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey);
+ saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle);
+
+ if (item is Video video)
+ {
+ saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@PrimaryVersionId");
+ }
+
+ if (item is Folder folder && folder.DateLastMediaAdded.HasValue)
+ {
+ saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@DateLastMediaAdded");
+ }
+
+ saveItemStatement.TryBind("@Album", item.Album);
+ saveItemStatement.TryBind("@LUFS", item.LUFS);
+ saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
+ saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
+
+ if (item is IHasSeries hasSeriesName)
+ {
+ saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@SeriesName");
+ }
+
+ if (string.IsNullOrWhiteSpace(userDataKey))
+ {
+ saveItemStatement.TryBindNull("@UserDataKey");
+ }
+ else
+ {
+ saveItemStatement.TryBind("@UserDataKey", userDataKey);
+ }
+
+ if (item is Episode episode)
+ {
+ saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
+
+ var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
+
+ saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@SeasonName");
+ saveItemStatement.TryBindNull("@SeasonId");
+ }
+
+ if (item is IHasSeries hasSeries)
+ {
+ var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
+
+ saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
+ saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@SeriesId");
+ saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey");
+ }
+
+ saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
+ saveItemStatement.TryBind("@Tagline", item.Tagline);
+
+ saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
+ saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
+
+ if (item.ProductionLocations.Length > 0)
+ {
+ saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@ProductionLocations");
+ }
+
+ if (item.ExtraIds.Length > 0)
+ {
+ saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@ExtraIds");
+ }
+
+ saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate);
+ if (item.ExtraType.HasValue)
+ {
+ saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString());
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@ExtraType");
+ }
+
+ string artists = null;
+ if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
+ {
+ artists = string.Join('|', hasArtists.Artists);
+ }
+
+ saveItemStatement.TryBind("@Artists", artists);
+
+ string albumArtists = null;
+ if (item is IHasAlbumArtist hasAlbumArtists
+ && hasAlbumArtists.AlbumArtists.Count > 0)
+ {
+ albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
+ }
+
+ saveItemStatement.TryBind("@AlbumArtists", albumArtists);
+ saveItemStatement.TryBind("@ExternalId", item.ExternalId);
+
+ if (item is LiveTvProgram program)
+ {
+ saveItemStatement.TryBind("@ShowId", program.ShowId);
+ }
+ else
+ {
+ saveItemStatement.TryBindNull("@ShowId");
+ }
+
+ Guid ownerId = item.OwnerId;
+ if (ownerId.IsEmpty())
+ {
+ saveItemStatement.TryBindNull("@OwnerId");
+ }
+ else
+ {
+ saveItemStatement.TryBind("@OwnerId", ownerId);
+ }
+
+ saveItemStatement.ExecuteNonQuery();
+ }
+
+ internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
+ {
+ StringBuilder str = new StringBuilder();
+ foreach (var i in providerIds)
+ {
+ // Ideally we shouldn't need this IsNullOrWhiteSpace check,
+ // but we're seeing some cases of bad data slip through
+ if (string.IsNullOrWhiteSpace(i.Value))
+ {
+ continue;
+ }
+
+ str.Append(i.Key)
+ .Append('=')
+ .Append(i.Value)
+ .Append('|');
+ }
+
+ if (str.Length == 0)
+ {
+ return null;
+ }
+
+ str.Length -= 1; // Remove last |
+ return str.ToString();
+ }
+
+ internal static void DeserializeProviderIds(string value, IHasProviderIds item)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return;
+ }
+
+ foreach (var part in value.SpanSplit('|'))
+ {
+ var providerDelimiterIndex = part.IndexOf('=');
+ // Don't let empty values through
+ if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1)
+ {
+ item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString());
+ }
+ }
+ }
+
+ internal string SerializeImages(ItemImageInfo[] images)
+ {
+ if (images.Length == 0)
+ {
+ return null;
+ }
+
+ StringBuilder str = new StringBuilder();
+ foreach (var i in images)
+ {
+ if (string.IsNullOrWhiteSpace(i.Path))
+ {
+ continue;
+ }
+
+ AppendItemImageInfo(str, i);
+ str.Append('|');
+ }
+
+ str.Length -= 1; // Remove last |
+ return str.ToString();
+ }
+
+ internal ItemImageInfo[] DeserializeImages(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.Count('|') + 1;
+
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
+ {
+ var image = ItemImageInfoFromValueString(part);
+
+ if (image is not null)
+ {
+ result[position++] = image;
+ }
+ }
+
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
+ }
+
+ private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
+ {
+ const char Delimiter = '*';
+
+ var path = image.Path ?? string.Empty;
+
+ bldr.Append(GetPathToSave(path))
+ .Append(Delimiter)
+ .Append(image.DateModified.Ticks)
+ .Append(Delimiter)
+ .Append(image.Type)
+ .Append(Delimiter)
+ .Append(image.Width)
+ .Append(Delimiter)
+ .Append(image.Height);
+
+ var hash = image.BlurHash;
+ if (!string.IsNullOrEmpty(hash))
+ {
+ bldr.Append(Delimiter)
+ // Replace delimiters with other characters.
+ // This can be removed when we migrate to a proper DB.
+ .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
+ }
+ }
+
+ internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
+ {
+ const char Delimiter = '*';
+
+ var nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan<char> path = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan<char> dateModified = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan<char> imageType = value[..nextSegment];
+
+ var image = new ItemImageInfo
+ {
+ Path = RestorePath(path.ToString())
+ };
+
+ if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
+ && ticks >= DateTime.MinValue.Ticks
+ && ticks <= DateTime.MaxValue.Ticks)
+ {
+ image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
+ }
+ else
+ {
+ return null;
+ }
+
+ if (Enum.TryParse(imageType, true, out ImageType type))
+ {
+ image.Type = type;
+ }
+ else
+ {
+ return null;
+ }
+
+ // Optional parameters: width*height*blurhash
+ if (nextSegment + 1 < value.Length - 1)
+ {
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1 || nextSegment == value.Length)
+ {
+ return image;
+ }
+
+ ReadOnlySpan<char> widthSpan = value[..nextSegment];
+
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan<char> heightSpan = value[..nextSegment];
+
+ if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
+ && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
+ {
+ image.Width = width;
+ image.Height = height;
+ }
+
+ if (nextSegment < value.Length - 1)
+ {
+ value = value[(nextSegment + 1)..];
+ var length = value.Length;
+
+ Span<char> blurHashSpan = stackalloc char[length];
+ for (int i = 0; i < length; i++)
+ {
+ var c = value[i];
+ blurHashSpan[i] = c switch
+ {
+ '/' => Delimiter,
+ '\\' => '|',
+ _ => c
+ };
+ }
+
+ image.BlurHash = new string(blurHashSpan);
+ }
+ }
+
+ return image;
+ }
+
+ /// <summary>
+ /// Internal retrieve from items or users table.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
+ public BaseItem RetrieveItem(Guid id)
+ {
+ if (id.IsEmpty())
+ {
+ throw new ArgumentException("Guid can't be empty", nameof(id));
+ }
+
+ CheckDisposed();
+
+ using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
+ {
+ statement.TryBind("@guid", id);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ return GetItem(row, new InternalItemsQuery());
+ }
+ }
+
+ return null;
+ }
+
private bool TypeRequiresDeserialization(Type type)
{
if (_config.Configuration.SkipDeserializationForBasicTypes)
@@ -694,9 +1635,6 @@ namespace Emby.Server.Implementations.Data
if (query.SearchTerm.Length > 1)
{
builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
- builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)");
- builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)");
- builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)");
}
builder.Append(") as SearchScore");
@@ -727,11 +1665,6 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
}
-
- if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermEquals", searchTerm);
- }
}
private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
@@ -797,6 +1730,7 @@ namespace Emby.Server.Implementations.Data
return string.Empty;
}
+ /// <inheritdoc />
public int GetCount(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -844,6 +1778,7 @@ namespace Emby.Server.Implementations.Data
}
}
+ /// <inheritdoc />
public List<BaseItem> GetItemList(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -997,6 +1932,7 @@ namespace Emby.Server.Implementations.Data
items.Add(newItem);
}
+ /// <inheritdoc />
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -1241,6 +2177,7 @@ namespace Emby.Server.Implementations.Data
};
}
+ /// <inheritdoc />
public List<Guid> GetItemIdsList(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -2557,6 +3494,15 @@ namespace Emby.Server.Implementations.Data
OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
""");
}
+
+ // A playlist should be accessible to its owner regardless of allowed tags.
+ else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
+ {
+ whereClauses.Add($"""
+ ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
+ OR data like @PlaylistOwnerUserId)
+ """);
+ }
else
{
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
@@ -2568,6 +3514,11 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
}
+
+ if (query.User is not null)
+ {
+ statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%""");
+ }
}
}
@@ -2785,6 +3736,7 @@ namespace Emby.Server.Implementations.Data
|| query.IncludeItemTypes.Contains(BaseItemKind.Season);
}
+ /// <inheritdoc />
public void UpdateInheritedValues()
{
const string Statements = """
@@ -2801,6 +3753,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit();
}
+ /// <inheritdoc />
public void DeleteItem(Guid id)
{
if (id.IsEmpty())
@@ -2843,6 +3796,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
+ /// <inheritdoc />
public List<string> GetPeopleNames(InternalPeopleQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -2881,6 +3835,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return list;
}
+ /// <inheritdoc />
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -3040,46 +3995,55 @@ AND Type = @InternalPersonType)");
}
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
}
+ /// <inheritdoc />
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
}
+ /// <inheritdoc />
public List<string> GetStudioNames()
{
return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
}
+ /// <inheritdoc />
public List<string> GetAllArtistNames()
{
return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
}
+ /// <inheritdoc />
public List<string> GetMusicGenreNames()
{
return GetItemValueNames(
@@ -3094,6 +4058,7 @@ AND Type = @InternalPersonType)");
Array.Empty<string>());
}
+ /// <inheritdoc />
public List<string> GetGenreNames()
{
return GetItemValueNames(
@@ -3571,6 +4536,7 @@ AND Type = @InternalPersonType)");
}
}
+ /// <inheritdoc />
public void UpdatePeople(Guid itemId, List<PersonInfo> people)
{
if (itemId.IsEmpty())
@@ -3672,6 +4638,7 @@ AND Type = @InternalPersonType)");
return item;
}
+ /// <inheritdoc />
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
CheckDisposed();
@@ -3720,6 +4687,7 @@ AND Type = @InternalPersonType)");
}
}
+ /// <inheritdoc />
public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -4074,6 +5042,7 @@ AND Type = @InternalPersonType)");
return item;
}
+ /// <inheritdoc />
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
CheckDisposed();
@@ -4109,6 +5078,7 @@ AND Type = @InternalPersonType)");
return list;
}
+ /// <inheritdoc />
public void SaveMediaAttachments(
Guid id,
IReadOnlyList<MediaAttachment> attachments,
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 31617d1a5..6af2a553d 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -314,6 +314,12 @@ namespace Emby.Server.Implementations.IO
var ex = e.GetException();
var dw = (FileSystemWatcher)sender;
+ if (ex is UnauthorizedAccessException unauthorizedAccessException)
+ {
+ _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path);
+ return;
+ }
+
_logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true);
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 28bb29df8..4b68f21d5 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO
}
}
+ /// <inheritdoc />
+ public void MoveDirectory(string source, string destination)
+ {
+ try
+ {
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
+ }
+
+ Directory.Delete(source, true);
+ }
+ }
+
/// <summary>
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
/// </summary>
@@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO
}
}
- /// <summary>
- /// Gets the creation time UTC.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>DateTime.</returns>
+ /// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(string path)
{
return GetCreationTimeUtc(GetFileSystemInfo(path));
@@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO
}
}
- /// <summary>
- /// Gets the last write time UTC.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>DateTime.</returns>
+ /// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(string path)
{
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
@@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO
File.SetAttributes(path, attributes);
}
- /// <summary>
- /// Swaps the files.
- /// </summary>
- /// <param name="file1">The file1.</param>
- /// <param name="file2">The file2.</param>
+ /// <inheritdoc />
public virtual void SwapFiles(string file1, string file2)
{
ArgumentException.ThrowIfNullOrEmpty(file1);
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index a2301c8ae..bb45dd87e 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library
"**/lost+found/**",
"**/lost+found",
+ // Trickplay files
+ "**/*.trickplay",
+ "**/*.trickplay/**",
+
// WMC temp recording directories that will constantly be written to
"**/TempRec/**",
"**/TempRec",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 48d24385e..28f7ed659 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2725,33 +2725,9 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
{
- string? newPath;
- if (ownerItem is not null)
- {
- var libraryOptions = GetLibraryOptions(ownerItem);
- if (libraryOptions is not null)
- {
- foreach (var pathInfo in libraryOptions.PathInfos)
- {
- if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
- {
- return newPath;
- }
- }
- }
- }
-
- var metadataPath = _configurationManager.Configuration.MetadataPath;
- var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
-
- if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
- {
- return newPath;
- }
-
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
- if (path.TryReplaceSubPath(map.From, map.To, out newPath))
+ if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
{
return newPath;
}
@@ -3070,15 +3046,6 @@ namespace Emby.Server.Implementations.Library
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
- foreach (var originalPathInfo in libraryOptions.PathInfos)
- {
- if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
- {
- originalPathInfo.NetworkPath = mediaPath.NetworkPath;
- break;
- }
- }
-
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 9172af516..97aa0ca58 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -129,5 +129,11 @@
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
- "TaskAudioNormalization": "Нармалізацыя гуку"
+ "TaskAudioNormalization": "Нармалізацыя гуку",
+ "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.",
+ "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
+ "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
+ "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
+ "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 2998489b5..6b3b78fa1 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
"TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Normalització d'Àudio",
- "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio."
+ "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.",
+ "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons",
+ "TaskDownloadMissingLyrics": "Baixar lletres que falten"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index ad9e555a3..ba2e2700d 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Normalizace zvuku",
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
- "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni"
+ "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni",
+ "TaskExtractMediaSegments": "Skenování segmentů médií",
+ "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
+ "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index bbb162c77..51c9e87d5 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Audio Normalisierung",
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
- "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen"
+ "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen",
+ "TaskExtractMediaSegments": "Scanne Mediensegmente",
+ "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
+ "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
+ "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 65df1e45b..ca52ffb14 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Audio Normalisation",
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
"TaskDownloadMissingLyrics": "Download missing lyrics",
- "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs"
+ "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs",
+ "TaskExtractMediaSegments": "Media Segment Scan",
+ "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
+ "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
+ "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index d1410ef5e..9702ab712 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -131,5 +131,9 @@
"TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
+ "TaskExtractMediaSegments": "Media Segment Scan",
+ "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
+ "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
+ "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index b926d9d30..f2f657b04 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -132,5 +132,9 @@
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
- "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
+ "TaskExtractMediaSegments": "Escanear Segmentos de Media",
+ "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medio de plugins habilitados para MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Mueve archivos existentes de trickplay de acuerdo a la configuración de la biblioteca.",
+ "TaskMoveTrickplayImages": "Migrar Ubicación de Imagen de Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index 075bcc9a4..3b2bb70a9 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -102,7 +102,7 @@
"Forced": "Sunnitud",
"Folders": "Kaustad",
"Favorites": "Lemmikud",
- "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
+ "FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}",
"DeviceOnlineWithName": "{0} on ühendatud",
"DeviceOfflineWithName": "{0} katkestas ühenduse",
"Default": "Vaikimisi",
@@ -129,5 +129,11 @@
"TaskAudioNormalization": "Heli Normaliseerimine",
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.",
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
- "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid"
+ "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
+ "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika",
+ "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika",
+ "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
+ "TaskExtractMediaSegments": "Meediasegmentide skaneerimine",
+ "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
+ "TaskMoveTrickplayImages": "Migreeri trickplay piltide asukoht"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index b0ddec104..ff14c1367 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -132,5 +132,9 @@
"TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.",
"TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود",
"TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها",
- "TaskAudioNormalization": "نرمال کردن صدا"
+ "TaskAudioNormalization": "نرمال کردن صدا",
+ "TaskExtractMediaSegments": "بررسی بخش محتوا",
+ "TaskExtractMediaSegmentsDescription": "بخش‌های محتوا را از افزونه‌های مربوط استخراح می‌کند.",
+ "TaskMoveTrickplayImages": "جابه‌جایی عکس‌های Trickplay",
+ "TaskMoveTrickplayImagesDescription": "داده‌های Trickplay را با توجه به تنظیمات کتاب‌خانه جابه‌جا می‌کند."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index dced61c5e..8a88cf28e 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -129,5 +129,6 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
- "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
+ "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
+ "TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 1dba78add..3caf8b547 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
- "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes"
+ "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
+ "TaskExtractMediaSegments": "Analyse des segments de média",
+ "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
+ "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque."
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 76a98aa54..3ba3e6679 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -1,7 +1,7 @@
{
"Albums": "Álbumes",
- "Collections": "Colecións",
- "ChapterNameValue": "Capítulos {0}",
+ "Collections": "Coleccións",
+ "ChapterNameValue": "Capítulo {0}",
"Channels": "Canles",
"CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
"Books": "Libros",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index c8e036424..af57b1693 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -60,7 +60,7 @@
"NotificationOptionUserLockedOut": "משתמש ננעל",
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
- "Photos": "תמונות",
+ "Photos": "צילומים",
"Playlists": "רשימות נגינה",
"Plugin": "תוסף",
"PluginInstalledWithName": "{0} הותקן",
@@ -130,5 +130,11 @@
"TaskAudioNormalization": "נרמול שמע",
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
- "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
+ "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
+ "TaskDownloadMissingLyrics": "הורדת מילים חסרות",
+ "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
+ "TaskMoveTrickplayImages": "מעביר את מיקום תמונות Trickplay",
+ "TaskExtractMediaSegments": "סריקת מדיה",
+ "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
+ "TaskMoveTrickplayImagesDescription": "מזיז קבצי trickplay קיימים בהתאם להגדרות הספרייה."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 31d6aaedb..f205e8b64 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -1,13 +1,13 @@
{
"Albums": "Albumok",
- "AppDeviceValues": "Program: {0}, Eszköz: {1}",
+ "AppDeviceValues": "Program: {0}, eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek",
"CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}",
"Channels": "Csatornák",
- "ChapterNameValue": "Jelenet {0}",
+ "ChapterNameValue": "{0}. jelenet",
"Collections": "Gyűjtemények",
"DeviceOfflineWithName": "{0} kijelentkezett",
"DeviceOnlineWithName": "{0} belépett",
@@ -15,31 +15,31 @@
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
- "HeaderAlbumArtists": "Album előadók",
+ "HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása",
- "HeaderFavoriteAlbums": "Kedvenc Albumok",
- "HeaderFavoriteArtists": "Kedvenc Előadók",
- "HeaderFavoriteEpisodes": "Kedvenc Epizódok",
- "HeaderFavoriteShows": "Kedvenc Sorozatok",
- "HeaderFavoriteSongs": "Kedvenc Dalok",
+ "HeaderFavoriteAlbums": "Kedvenc albumok",
+ "HeaderFavoriteArtists": "Kedvenc előadók",
+ "HeaderFavoriteEpisodes": "Kedvenc epizódok",
+ "HeaderFavoriteShows": "Kedvenc sorozatok",
+ "HeaderFavoriteSongs": "Kedvenc számok",
"HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik",
- "HeaderRecordingGroups": "Felvevő Csoportok",
- "HomeVideos": "Otthoni Videók",
- "Inherit": "Örökölt",
- "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
- "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
+ "HeaderRecordingGroups": "Felvételi csoportok",
+ "HomeVideos": "Otthoni videók",
+ "Inherit": "Öröklés",
+ "ItemAddedWithName": "{0} hozzáadva a médiatárhoz",
+ "ItemRemovedWithName": "{0} eltávolítva a médiatárból",
"LabelIpAddressValue": "IP-cím: {0}",
"LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb",
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett",
"MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}",
- "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett",
+ "MessageServerConfigurationUpdated": "A kiszolgálókonfiguráció frissítve lett",
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zenék",
- "MusicVideos": "Zenei videóklippek",
+ "MusicVideos": "Zenei videóklipek",
"NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad",
@@ -56,7 +56,7 @@
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
"NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
- "NotificationOptionTaskFailed": "Ütemezett feladat hiba",
+ "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban",
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
"NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
@@ -107,7 +107,7 @@
"TaskCleanCache": "Gyorsítótár könyvtárának ürítése",
"TasksChannelsCategory": "Internetes csatornák",
"TasksApplicationCategory": "Alkalmazás",
- "TasksLibraryCategory": "Könyvtár",
+ "TasksLibraryCategory": "Médiatár",
"TasksMaintenanceCategory": "Karbantartás",
"TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
@@ -119,16 +119,22 @@
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
"Default": "Alapértelmezett",
- "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
+ "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a médiatár beolvasása, vagy egyéb adatbázis-módosítást igénylő változtatás végrehajtása után, javíthatja a teljesítményt.",
"TaskOptimizeDatabase": "Adatbázis optimalizálása",
"TaskKeyframeExtractor": "Kulcsképkockák kibontása",
"TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
"External": "Külső",
"HearingImpaired": "Hallássérült",
- "TaskRefreshTrickplayImages": "Trickplay képek generálása",
+ "TaskRefreshTrickplayImages": "Trickplay képek előállítása",
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
- "TaskAudioNormalization": "Hangerő Normalizáció",
+ "TaskAudioNormalization": "Hangerő-normalizálás",
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
- "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.",
- "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása"
+ "TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
+ "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása",
+ "TaskExtractMediaSegments": "Médiaszegmens felismerése",
+ "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
+ "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
+ "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
+ "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
+ "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből."
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 961d1a0df..6b0cfb359 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -132,5 +132,7 @@
"TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
- "TaskDownloadMissingLyrics": "Scarica testi mancanti"
+ "TaskDownloadMissingLyrics": "Scarica testi mancanti",
+ "TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index c8ed7d0fb..10f4aee25 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -129,5 +129,10 @@
"TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
"TaskAudioNormalization": "音声の正規化",
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
- "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。"
+ "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。",
+ "TaskDownloadMissingLyricsDescription": "歌詞をダウンロード",
+ "TaskExtractMediaSegments": "メディアセグメントを読み取る",
+ "TaskMoveTrickplayImages": "Trickplayの画像を移動",
+ "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
+ "TaskDownloadMissingLyrics": "記録されていない歌詞をダウンロード"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
index ffb4345c8..336d286fc 100644
--- a/Emby.Server.Implementations/Localization/Core/kw.json
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -131,5 +131,9 @@
"TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
"TaskKeyframeExtractor": "Estennell Framalhwedh",
"TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
- "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir."
+ "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir.",
+ "TaskExtractMediaSegments": "Arhwilas Rann Media",
+ "TaskExtractMediaSegmentsDescription": "Kavos rannow media a-dhyworth ystynansow gallosegys MediaSegment.",
+ "TaskMoveTrickplayImages": "Divroa Tyller Imach TrickPlay",
+ "TaskMoveTrickplayImagesDescription": "Y hwra movya restrennow a-lemmyn trickplay herwydh settyansow lyverva."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index 7ef907918..e149f8adf 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -55,7 +55,7 @@
"Genres": "Жанрови",
"Folders": "Папки",
"Favorites": "Омилени",
- "FailedLoginAttemptWithUserName": "Неуспешно поврзување од {0}",
+ "FailedLoginAttemptWithUserName": "Неуспешен обид за најавување од {0}",
"DeviceOnlineWithName": "{0} е приклучен",
"DeviceOfflineWithName": "{0} се исклучи",
"Collections": "Колекции",
@@ -123,5 +123,13 @@
"TaskCleanActivityLogDescription": "Избришува логови на активности постари од определеното време.",
"TaskCleanActivityLog": "Избриши Лог на Активности",
"External": "Надворешен",
- "HearingImpaired": "Оштетен слух"
+ "HearingImpaired": "Оштетен слух",
+ "TaskCleanCollectionsAndPlaylists": "Исчисти ги колекциите и плејлистите",
+ "TaskAudioNormalizationDescription": "Скенирање датотеки за податоци за нормализација на звукот.",
+ "TaskDownloadMissingLyrics": "Преземи стихови кои недостасуваат",
+ "TaskDownloadMissingLyricsDescription": "Преземи стихови/текстови за песни",
+ "TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)",
+ "TaskAudioNormalization": "Нормализација на звукот",
+ "TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 747652538..b1b6e96ea 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -128,9 +128,13 @@
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
"TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.",
"TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
- "TaskAudioNormalization": "Lyd Normalisering",
- "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes",
+ "TaskAudioNormalization": "Lydnormalisering",
+ "TaskAudioNormalizationDescription": "Skan filer for lydnormaliserende data.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes.",
"TaskDownloadMissingLyrics": "Last ned manglende tekster",
- "TaskDownloadMissingLyricsDescription": "Last ned sangtekster"
+ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
+ "TaskExtractMediaSegments": "Skann mediasegment",
+ "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.",
+ "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 39e7cd546..7d101195b 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -16,13 +16,13 @@
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
- "HeaderContinueWatching": "Kijken hervatten",
+ "HeaderContinueWatching": "Verderkijken",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
- "HeaderFavoriteShows": "Favoriete shows",
+ "HeaderFavoriteShows": "Favoriete series",
"HeaderFavoriteSongs": "Favoriete nummers",
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Live-tv",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
"HomeVideos": "Homevideo's",
@@ -34,8 +34,8 @@
"Latest": "Nieuwste",
"MessageApplicationUpdated": "Jellyfin Server is bijgewerkt",
"MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt",
- "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de serverconfiguratie is bijgewerkt",
+ "MessageServerConfigurationUpdated": "Serverconfiguratie is bijgewerkt",
"MixedContent": "Gemengde inhoud",
"Movies": "Films",
"Music": "Muziek",
@@ -50,12 +50,12 @@
"NotificationOptionAudioPlaybackStopped": "Muziek gestopt",
"NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload",
"NotificationOptionInstallationFailed": "Installatie mislukt",
- "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd",
- "NotificationOptionPluginError": "Plug-in fout",
+ "NotificationOptionNewLibraryContent": "Nieuwe inhoud toegevoegd",
+ "NotificationOptionPluginError": "Plug-in-fout",
"NotificationOptionPluginInstalled": "Plug-in geïnstalleerd",
"NotificationOptionPluginUninstalled": "Plug-in verwijderd",
"NotificationOptionPluginUpdateInstalled": "Plug-in-update geïnstalleerd",
- "NotificationOptionServerRestartRequired": "Server herstart nodig",
+ "NotificationOptionServerRestartRequired": "Herstarten server vereist",
"NotificationOptionTaskFailed": "Geplande taak mislukt",
"NotificationOptionUserLockedOut": "Gebruiker is vergrendeld",
"NotificationOptionVideoPlayback": "Afspelen van video gestart",
@@ -72,16 +72,16 @@
"ServerNameNeedsToBeRestarted": "{0} moet herstart worden",
"Shows": "Series",
"Songs": "Nummers",
- "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.",
+ "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.",
"SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt",
- "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}",
+ "SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}",
"Sync": "Synchronisatie",
"System": "Systeem",
"TvShows": "TV-series",
"User": "Gebruiker",
"UserCreatedWithName": "Gebruiker {0} is aangemaakt",
"UserDeletedWithName": "Gebruiker {0} is verwijderd",
- "UserDownloadingItemWithValues": "{0} download {1}",
+ "UserDownloadingItemWithValues": "{0} downloadt {1}",
"UserLockedOutWithName": "Gebruikersaccount {0} is vergrendeld",
"UserOfflineFromDevice": "Verbinding van {0} met {1} is verbroken",
"UserOnlineFromDevice": "{0} heeft verbinding met {1}",
@@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}",
"UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}",
"ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
- "ValueSpecialEpisodeName": "Speciaal - {0}",
+ "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Versie {0}",
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Geluidsnormalisatie",
"TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.",
"TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden",
- "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten"
+ "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten",
+ "TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.",
+ "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
+ "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
+ "TaskExtractMediaSegments": "Scannen op mediasegmenten"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index a24a837ab..33b0bb7e1 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Normalizacja dźwięku",
"TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.",
"TaskDownloadMissingLyrics": "Pobierz brakujące słowa",
- "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek"
+ "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek",
+ "TaskExtractMediaSegments": "Skanowanie segmentów mediów",
+ "TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay",
+ "TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index d9867f5e0..9f4f58cb6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Uma nova imagem da câmera foi enviada de {0}",
"Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
- "Collections": "Coletâneas",
+ "Collections": "Coleções",
"DeviceOfflineWithName": "{0} se desconectou",
"DeviceOnlineWithName": "{0} se conectou",
"FailedLoginAttemptWithUserName": "Falha na tentativa de login de {0}",
@@ -130,5 +130,11 @@
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
"TaskAudioNormalization": "Normalização de áudio",
- "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
+ "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.",
+ "TaskDownloadMissingLyricsDescription": "Baixar letras para músicas",
+ "TaskDownloadMissingLyrics": "Baixar letra faltante",
+ "TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.",
+ "TaskExtractMediaSegments": "Varredura do segmento de mídia",
+ "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.",
+ "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index ff9a0d4f4..7e9be76e5 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -129,5 +129,11 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
- "TaskAudioNormalization": "Normalização de áudio"
+ "TaskAudioNormalization": "Normalização de áudio",
+ "TaskDownloadMissingLyrics": "Baixar letras faltantes",
+ "TaskDownloadMissingLyricsDescription": "Baixa letras para músicas",
+ "TaskMoveTrickplayImagesDescription": "Transfere ficheiros de miniatura de vídeo, conforme as definições da biblioteca.",
+ "TaskExtractMediaSegments": "Varrimento de segmentos da média",
+ "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de média de extensões com suporte a MediaSegment.",
+ "TaskMoveTrickplayImages": "Migração de miniaturas de vídeo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 2f52aafa3..bf59e1583 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -129,5 +129,11 @@
"TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.",
"TaskAudioNormalization": "Normalizare sunet",
"TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare.",
+ "TaskExtractMediaSegments": "Scanează segmentele media",
+ "TaskMoveTrickplayImagesDescription": "Mută fișierele trickplay existente conform setărilor librăriei.",
+ "TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
+ "TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay",
+ "TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
+ "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index a9b6fbeef..66d8bf899 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -130,5 +130,11 @@
"TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
"TaskAudioNormalization": "Normalizácia zvuku",
- "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku."
+ "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku.",
+ "TaskExtractMediaSegments": "Skenovanie segmentov médií",
+ "TaskExtractMediaSegmentsDescription": "Extrahuje alebo získava segmenty médií zo zásuvných modulov s povolenou funkciou MediaSegment.",
+ "TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.",
+ "TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní",
+ "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 110af11b7..19be1a23e 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Aplikacija",
"Artists": "Izvajalci",
- "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
+ "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil/a",
"Books": "Knjige",
"CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
"Channels": "Kanali",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index a4e2302d1..5cf54522b 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,7 +9,7 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har avbrutit uppkopplingen",
+ "DeviceOfflineWithName": "{0} har kopplat ned",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
@@ -121,7 +121,7 @@
"Default": "Standard",
"TaskOptimizeDatabase": "Optimera databasen",
"TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna aktivitet efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.",
- "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.",
+ "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna körning kan ta lång tid.",
"TaskKeyframeExtractor": "Extraktor för nyckelbildrutor",
"External": "Extern",
"HearingImpaired": "Hörselskadad",
@@ -132,5 +132,9 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.",
"TaskDownloadMissingLyrics": "Ladda ner saknad låttext",
- "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter"
+ "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter",
+ "TaskExtractMediaSegments": "Skanning av mediesegment",
+ "TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.",
+ "TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder",
+ "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 1dceadc61..a3cf78fcb 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -130,5 +130,11 @@
"TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
"TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin",
"TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.",
- "TaskAudioNormalization": "Ses Normalleştirme"
+ "TaskAudioNormalization": "Ses Normalleştirme",
+ "TaskExtractMediaSegments": "Medya Segmenti Tarama",
+ "TaskMoveTrickplayImages": "Trickplay Görsel Konumunu Taşıma",
+ "TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.",
+ "TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir",
+ "TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır."
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 97bad4532..3fddc2e78 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -131,5 +131,9 @@
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо",
"TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
- "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень"
+ "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень",
+ "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
+ "TaskExtractMediaSegments": "Сканування медіа-сегментів",
+ "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
+ "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 32e2f4bab..f890ea74d 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -131,5 +131,9 @@
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.",
"TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát",
- "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu"
+ "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu",
+ "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.",
+ "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.",
+ "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 4bec590fb..9a0e2115e 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -93,7 +93,7 @@
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}",
"TaskUpdatePluginsDescription": "为已设置为自动更新的插件下载和安装更新。",
- "TaskRefreshPeople": "刷新人员",
+ "TaskRefreshPeople": "刷新演职人员",
"TasksChannelsCategory": "互联网频道",
"TasksLibraryCategory": "媒体库",
"TaskDownloadMissingSubtitlesDescription": "根据元数据设置在互联网上搜索缺少的字幕。",
@@ -122,15 +122,19 @@
"TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
"TaskOptimizeDatabase": "优化数据库",
"TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。",
- "TaskKeyframeExtractor": "关键帧提取器",
+ "TaskKeyframeExtractor": "关键帧提取",
"External": "外部",
"HearingImpaired": "听力障碍",
- "TaskRefreshTrickplayImages": "生成时间轴缩略图",
- "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
+ "TaskRefreshTrickplayImages": "生成进度条预览图",
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成进度条预览图。",
"TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
"TaskAudioNormalization": "音频标准化",
"TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。",
"TaskDownloadMissingLyrics": "下载缺失的歌词",
- "TaskDownloadMissingLyricsDescription": "下载歌曲歌词"
+ "TaskDownloadMissingLyricsDescription": "下载歌曲歌词",
+ "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置",
+ "TaskExtractMediaSegments": "媒体片段扫描",
+ "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。",
+ "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index f06bbc591..81d5b83d6 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -129,5 +129,11 @@
"TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
"TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。",
"TaskAudioNormalization": "音量標準化",
- "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。"
+ "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。",
+ "TaskDownloadMissingLyrics": "下載缺少的歌詞",
+ "TaskDownloadMissingLyricsDescription": "卡在歌曲歌詞",
+ "TaskExtractMediaSegments": "掃描媒體片段",
+ "TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。",
+ "TaskMoveTrickplayImages": "遷移快轉縮圖位置",
+ "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。"
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 896f47923..eb55e32c5 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -91,8 +91,29 @@ namespace Emby.Server.Implementations.MediaEncoder
return video.DefaultVideoStreamIndex.HasValue;
}
+ private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
+ {
+ if (chapters.Count < 2)
+ {
+ return 0;
+ }
+
+ long sum = 0;
+ for (int i = 1; i < chapters.Count; i++)
+ {
+ sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
+ }
+
+ return sum / chapters.Count;
+ }
+
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
{
+ if (chapters.Count == 0)
+ {
+ return true;
+ }
+
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
@@ -100,6 +121,14 @@ namespace Emby.Server.Implementations.MediaEncoder
extractImages = false;
}
+ var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
+ var threshold = TimeSpan.FromSeconds(1).Ticks;
+ if (averageChapterDuration < threshold)
+ {
+ _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
+ extractImages = false;
+ }
+
var success = true;
var changesMade = false;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
new file mode 100644
index 000000000..d6fad7526
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Task to obtain media segments.
+/// </summary>
+public class MediaSegmentExtractionTask : IScheduledTask
+{
+ /// <summary>
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IMediaSegmentManager _mediaSegmentManager;
+ private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Audio, BaseItemKind.AudioBook];
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaSegmentExtractionTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="mediaSegmentManager">The segment manager.</param>
+ public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager)
+ {
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _mediaSegmentManager = mediaSegmentManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments");
+
+ /// <inheritdoc/>
+ public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription");
+
+ /// <inheritdoc/>
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc/>
+ public string Key => "TaskExtractMediaSegments";
+
+ /// <inheritdoc/>
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(0);
+
+ var pagesize = 100;
+
+ var query = new InternalItemsQuery
+ {
+ MediaTypes = new[] { MediaType.Video, MediaType.Audio },
+ IsVirtualItem = false,
+ IncludeItemTypes = _itemTypes,
+ DtoOptions = new DtoOptions(true),
+ SourceTypes = new[] { SourceType.Library },
+ Recursive = true,
+ Limit = pagesize
+ };
+
+ var numberOfVideos = _libraryManager.GetCount(query);
+
+ var startIndex = 0;
+ var numComplete = 0;
+
+ while (startIndex < numberOfVideos)
+ {
+ query.StartIndex = startIndex;
+
+ var baseItems = _libraryManager.GetItemList(query);
+ var currentPageCount = baseItems.Count;
+ // TODO parallelize with Parallel.ForEach?
+ for (var i = 0; i < currentPageCount; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var item = baseItems[i];
+ // Only local files supported
+ if (item.IsFileProtocol && File.Exists(item.Path))
+ {
+ await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false);
+ }
+
+ // Update progress
+ numComplete++;
+ double percent = (double)numComplete / numberOfVideos;
+ progress.Report(100 * percent);
+ }
+
+ startIndex += pagesize;
+ }
+
+ progress.Report(100);
+ }
+
+ /// <inheritdoc/>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ yield return new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerInterval,
+ IntervalTicks = TimeSpan.FromHours(12).Ticks
+ };
+ }
+}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 72e164b52..6a8ad2bdc 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session
private Timer _inactiveTimer;
private DtoOptions _itemInfoDtoOptions;
- private bool _disposed = false;
+ private bool _disposed;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionManager"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param>
+ /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+ /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+ /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param>
public SessionManager(
ILogger<SessionManager> logger,
IEventManager eventManager,
IUserDataManager userDataManager,
- IServerConfigurationManager config,
+ IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
@@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_eventManager = eventManager;
_userDataManager = userDataManager;
- _config = config;
+ _config = serverConfigurationManager;
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
@@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
- var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
+ var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
+ {
+ DeviceId = deviceId
+ };
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session
return session;
}
+ private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
+ {
+ return new SessionInfoDto
+ {
+ PlayState = sessionInfo.PlayState,
+ AdditionalUsers = sessionInfo.AdditionalUsers,
+ Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
+ RemoteEndPoint = sessionInfo.RemoteEndPoint,
+ PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
+ Id = sessionInfo.Id,
+ UserId = sessionInfo.UserId,
+ UserName = sessionInfo.UserName,
+ Client = sessionInfo.Client,
+ LastActivityDate = sessionInfo.LastActivityDate,
+ LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
+ LastPausedDate = sessionInfo.LastPausedDate,
+ DeviceName = sessionInfo.DeviceName,
+ DeviceType = sessionInfo.DeviceType,
+ NowPlayingItem = sessionInfo.NowPlayingItem,
+ NowViewingItem = sessionInfo.NowViewingItem,
+ DeviceId = sessionInfo.DeviceId,
+ ApplicationVersion = sessionInfo.ApplicationVersion,
+ TranscodingInfo = sessionInfo.TranscodingInfo,
+ IsActive = sessionInfo.IsActive,
+ SupportsMediaControl = sessionInfo.SupportsMediaControl,
+ SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
+ NowPlayingQueue = sessionInfo.NowPlayingQueue,
+ NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
+ HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
+ PlaylistItemId = sessionInfo.PlaylistItemId,
+ ServerId = sessionInfo.ServerId,
+ UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
+ SupportedCommands = sessionInfo.SupportedCommands
+ };
+ }
+
/// <inheritdoc />
public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
{
@@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session
UserName = user.Username
};
- session.AdditionalUsers = [..session.AdditionalUsers, newUser];
+ session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
}
}
@@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session
var returnResult = new AuthenticationResult
{
User = _userManager.GetUserDto(user, request.RemoteEndPoint),
- SessionInfo = session,
+ SessionInfo = ToSessionInfoDto(session),
AccessToken = token,
ServerId = _appHost.SystemId
};
@@ -1800,6 +1853,105 @@ namespace Emby.Server.Implementations.Session
return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
}
+ /// <inheritdoc/>
+ public IReadOnlyList<SessionInfoDto> GetSessions(
+ Guid userId,
+ string deviceId,
+ int? activeWithinSeconds,
+ Guid? controllableUserToCheck,
+ bool isApiKey)
+ {
+ var result = Sessions;
+ if (!string.IsNullOrEmpty(deviceId))
+ {
+ result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ var userCanControlOthers = false;
+ var userIsAdmin = false;
+ User user = null;
+
+ if (isApiKey)
+ {
+ userCanControlOthers = true;
+ userIsAdmin = true;
+ }
+ else if (!userId.IsEmpty())
+ {
+ user = _userManager.GetUserById(userId);
+ if (user is not null)
+ {
+ userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
+ userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
+ }
+ else
+ {
+ return [];
+ }
+ }
+
+ if (!controllableUserToCheck.IsNullOrEmpty())
+ {
+ result = result.Where(i => i.SupportsRemoteControl);
+
+ var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
+ if (controlledUser is null)
+ {
+ return [];
+ }
+
+ if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
+ {
+ // Controlled user has device sharing disabled
+ result = result.Where(i => !i.UserId.IsEmpty());
+ }
+
+ if (!userCanControlOthers)
+ {
+ // User cannot control other user's sessions, validate user id.
+ result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
+ }
+
+ result = result.Where(i =>
+ {
+ if (isApiKey)
+ {
+ return true;
+ }
+
+ if (user is null)
+ {
+ return false;
+ }
+
+ return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
+ });
+ }
+ else if (!userIsAdmin)
+ {
+ // Request isn't from administrator, limit to "own" sessions.
+ result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
+ }
+
+ if (!userIsAdmin)
+ {
+ // Don't report acceleration type for non-admin users.
+ result = result.Select(r =>
+ {
+ r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
+ return r;
+ });
+ }
+
+ if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+ {
+ var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+ result = result.Where(i => i.LastActivityDate >= minActiveDate);
+ }
+
+ return result.Select(ToSessionInfoDto).ToList();
+ }
+
/// <inheritdoc />
public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 2a2ab4ad1..50050262f 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,15 +1,13 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos;
-using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
+ public ActionResult<QueryResult<DeviceInfoDto>> GetDevices([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
return _deviceManager.GetDevicesForUser(userId);
@@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
+ public ActionResult<DeviceInfoDto> GetDeviceInfo([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo is null)
@@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
+ public ActionResult<DeviceOptionsDto> GetDeviceOptions([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo is null)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 662e2acbc..54e0527c9 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -40,8 +40,8 @@ namespace Jellyfin.Api.Controllers;
[Authorize]
public class DynamicHlsController : BaseJellyfinApiController
{
- private const string DefaultVodEncoderPreset = "veryfast";
- private const string DefaultEventEncoderPreset = "superfast";
+ private const EncoderPreset DefaultVodEncoderPreset = EncoderPreset.veryfast;
+ private const EncoderPreset DefaultEventEncoderPreset = EncoderPreset.superfast;
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
@@ -158,6 +158,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="maxHeight">Optional. The max height.</param>
/// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Hls live stream retrieved.</response>
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
[HttpGet("Videos/{itemId}/live.m3u8")]
@@ -216,7 +217,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? enableSubtitlesInManifest,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
VideoRequestDto streamingRequest = new VideoRequestDto
{
@@ -251,7 +253,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -271,7 +273,8 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
MaxWidth = maxWidth,
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
// CTS lifecycle is managed internally.
@@ -398,6 +401,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
/// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
/// <param name="enableAudioVbrEncoding">Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -457,7 +461,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true,
[FromQuery] bool enableTrickplay = true,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -493,7 +498,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -512,7 +517,8 @@ public class DynamicHlsController : BaseJellyfinApiController
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
EnableTrickplay = enableTrickplay,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -663,7 +669,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -681,7 +687,8 @@ public class DynamicHlsController : BaseJellyfinApiController
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -741,6 +748,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/main.m3u8")]
@@ -797,7 +805,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
@@ -834,7 +843,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -851,7 +860,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1001,7 +1011,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1018,7 +1028,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1084,6 +1095,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1146,7 +1158,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
var streamingRequest = new VideoRequestDto
{
@@ -1185,7 +1198,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1202,7 +1215,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1365,7 +1379,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1382,7 +1396,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1797,10 +1812,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var args = "-codec:v:0 " + codec;
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ var isActualOutputVideoCodecAv1 = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+ var isActualOutputVideoCodecHevc = string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
+
+ if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
{
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
@@ -1814,10 +1830,17 @@ public class DynamicHlsController : BaseJellyfinApiController
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
{
- // Prefer dvh1 to dvhe
- args += " -tag:v:0 dvh1 -strict -2";
+ if (isActualOutputVideoCodecHevc)
+ {
+ // Prefer dvh1 to dvhe
+ args += " -tag:v:0 dvh1 -strict -2";
+ }
+ else if (isActualOutputVideoCodecAv1)
+ {
+ args += " -tag:v:0 dav1 -strict -2";
+ }
}
- else
+ else if (isActualOutputVideoCodecHevc)
{
// Prefer hvc1 to hev1
args += " -tag:v:0 hvc1";
@@ -1885,7 +1908,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!string.IsNullOrEmpty(state.OutputVideoSync))
{
- args += " -vsync " + state.OutputVideoSync;
+ args += EncodingHelper.GetVideoSyncOption(state.OutputVideoSync, _mediaEncoder.EncoderVersion);
}
args += _encodingHelper.GetOutputFFlags(state);
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index bc52be184..f22ac0b73 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -209,6 +209,7 @@ public class MediaInfoController : BaseJellyfinApiController
enableTranscoding.Value,
allowVideoStreamCopy.Value,
allowAudioStreamCopy.Value,
+ playbackInfoDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false,
Request.HttpContext.GetNormalizedRemoteIP());
}
@@ -236,7 +237,8 @@ public class MediaInfoController : BaseJellyfinApiController
StartTimeTicks = startTimeTicks,
SubtitleStreamIndex = subtitleStreamIndex,
UserId = userId ?? Guid.Empty,
- OpenToken = mediaSource.OpenToken
+ OpenToken = mediaSource.OpenToken,
+ AlwaysBurnInSubtitleWhenTranscoding = playbackInfoDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false
}).ConfigureAwait(false);
info.MediaSources = new[] { openStreamResult.MediaSource };
@@ -261,6 +263,7 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="openLiveStreamDto">The open live stream dto.</param>
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
/// <response code="200">Media source opened.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
[HttpPost("LiveStreams/Open")]
@@ -277,7 +280,8 @@ public class MediaInfoController : BaseJellyfinApiController
[FromQuery] Guid? itemId,
[FromBody] OpenLiveStreamDto? openLiveStreamDto,
[FromQuery] bool? enableDirectPlay,
- [FromQuery] bool? enableDirectStream)
+ [FromQuery] bool? enableDirectStream,
+ [FromQuery] bool? alwaysBurnInSubtitleWhenTranscoding)
{
userId ??= openLiveStreamDto?.UserId;
userId = RequestHelpers.GetUserId(User, userId);
@@ -295,7 +299,8 @@ public class MediaInfoController : BaseJellyfinApiController
DeviceProfile = openLiveStreamDto?.DeviceProfile,
EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
- DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
+ DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http },
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding ?? openLiveStreamDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false
};
return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index e97704d48..3dc5167a2 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -45,7 +45,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
[HttpGet("{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetSegmentsAsync(
+ public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetItemSegments(
[FromRoute, Required] Guid itemId,
[FromQuery] IEnumerable<MediaSegmentType>? includeSegmentTypes = null)
{
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 63d6e1cc3..e6f23b136 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -150,6 +150,37 @@ public class PlaylistsController : BaseJellyfinApiController
}
/// <summary>
+ /// Get a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">The playlist.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Playlist"/> objects.
+ /// </returns>
+ [HttpGet("{playlistId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<PlaylistDto> GetPlaylist(
+ [FromRoute, Required] Guid playlistId)
+ {
+ var userId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ return new PlaylistDto()
+ {
+ Shares = playlist.Shares,
+ OpenAccess = playlist.OpenAccess,
+ ItemIds = playlist.GetManageableItems().Select(t => t.Item2.Id).ToList()
+ };
+ }
+
+ /// <summary>
/// Get a playlist's users.
/// </summary>
/// <param name="playlistId">The playlist id.</param>
@@ -467,32 +498,23 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- userId = RequestHelpers.GetUserId(User, userId);
- var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
+ var callingUserId = userId ?? User.GetUserId();
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
if (playlist is null)
{
return NotFound("Playlist not found");
}
var isPermitted = playlist.OpenAccess
- || playlist.OwnerUserId.Equals(userId.Value)
- || playlist.Shares.Any(s => s.UserId.Equals(userId.Value));
+ || playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.UserId.Equals(callingUserId));
if (!isPermitted)
{
return Forbid();
}
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById<Playlist>(playlistId, user);
- if (item is null)
- {
- return NotFound();
- }
-
- var items = item.GetManageableItems().ToArray();
+ var items = playlist.GetManageableItems().ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@@ -507,7 +529,7 @@ public class PlaylistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
+ var user = _userManager.GetUserById(callingUserId);
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 60de66ab0..2f9e9f091 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -1,21 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -31,22 +27,18 @@ public class SessionController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
/// <summary>
/// Initializes a new instance of the <see cref="SessionController"/> class.
/// </summary>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
/// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
public SessionController(
ISessionManager sessionManager,
- IUserManager userManager,
- IDeviceManager deviceManager)
+ IUserManager userManager)
{
_sessionManager = sessionManager;
_userManager = userManager;
- _deviceManager = deviceManager;
}
/// <summary>
@@ -56,67 +48,22 @@ public class SessionController : BaseJellyfinApiController
/// <param name="deviceId">Filter by device Id.</param>
/// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
/// <response code="200">List of sessions returned.</response>
- /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
+ /// <returns>An <see cref="IReadOnlyList{SessionInfoDto}"/> with the available sessions.</returns>
[HttpGet("Sessions")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+ public ActionResult<IReadOnlyList<SessionInfoDto>> GetSessions(
[FromQuery] Guid? controllableByUserId,
[FromQuery] string? deviceId,
[FromQuery] int? activeWithinSeconds)
{
- var result = _sessionManager.Sessions;
-
- if (!string.IsNullOrEmpty(deviceId))
- {
- result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
- }
-
- if (!controllableByUserId.IsNullOrEmpty())
- {
- result = result.Where(i => i.SupportsRemoteControl);
-
- var user = _userManager.GetUserById(controllableByUserId.Value);
- if (user is null)
- {
- return NotFound();
- }
-
- if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
- {
- // User cannot control other user's sessions, validate user id.
- result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId)));
- }
-
- if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
- {
- result = result.Where(i => !i.UserId.IsEmpty());
- }
-
- result = result.Where(i =>
- {
- if (!string.IsNullOrWhiteSpace(i.DeviceId))
- {
- if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
- {
- return false;
- }
- }
-
- return true;
- });
- }
- else if (!User.IsInRole(UserRoles.Administrator))
- {
- // Request isn't from administrator, limit to "own" sessions.
- result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId()));
- }
-
- if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
+ Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId);
+ var result = _sessionManager.GetSessions(
+ User.GetUserId(),
+ deviceId,
+ activeWithinSeconds,
+ controllableUserToCheck,
+ User.GetIsApiKey());
return Ok(result);
}
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 60d49af9e..2cf66144c 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public ActionResult GetTrickplayTileImage(
+ public async Task<ActionResult> GetTrickplayTileImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
@@ -92,8 +92,9 @@ public class TrickplayController : BaseJellyfinApiController
return NotFound();
}
- var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
- if (System.IO.File.Exists(path))
+ var saveWithMedia = _libraryManager.GetLibraryOptions(item).SaveTrickplayWithMedia;
+ var path = await _trickplayManager.GetTrickplayTilePathAsync(item, width, index, saveWithMedia).ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
Response.Headers.ContentDisposition = "attachment";
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index fe7353496..41c4886d4 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -160,6 +160,7 @@ public class UniversalAudioController : BaseJellyfinApiController
true,
true,
true,
+ false,
Request.HttpContext.GetNormalizedRemoteIP());
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index effe7b021..8348fd937 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -482,7 +482,7 @@ public class VideosController : BaseJellyfinApiController
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, "superfast");
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, EncoderPreset.superfast);
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index ba92d811c..0e620e72a 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -304,6 +304,8 @@ public class DynamicHlsHelper
AppendPlaylistCodecsField(playlistBuilder, state);
+ AppendPlaylistSupplementalCodecsField(playlistBuilder, state);
+
AppendPlaylistResolutionField(playlistBuilder, state);
AppendPlaylistFramerateField(playlistBuilder, state);
@@ -407,6 +409,48 @@ public class DynamicHlsHelper
}
/// <summary>
+ /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
+ /// the active streams output Dolby Vision Videos.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
+ {
+ // Dolby Vision currently cannot exist when transcoding
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ return;
+ }
+
+ var dvProfile = state.VideoStream.DvProfile;
+ var dvLevel = state.VideoStream.DvLevel;
+ var dvRangeString = state.VideoStream.VideoRangeType switch
+ {
+ VideoRangeType.DOVIWithHDR10 => "db1p",
+ VideoRangeType.DOVIWithHLG => "db4h",
+ _ => string.Empty
+ };
+
+ if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
+ {
+ return;
+ }
+
+ var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
+ builder.Append(",SUPPLEMENTAL-CODECS=\"")
+ .Append(dvFourCc)
+ .Append('.')
+ .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
+ .Append('.')
+ .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
+ .Append('/')
+ .Append(dvRangeString)
+ .Append('"');
+ }
+
+ /// <summary>
/// Appends a RESOLUTION field containing the resolution of the output stream.
/// </summary>
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
@@ -738,7 +782,7 @@ public class DynamicHlsHelper
{
var width = state.VideoStream.Width ?? 0;
var height = state.VideoStream.Height ?? 0;
- var framerate = state.VideoStream.AverageFrameRate ?? 30;
+ var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
var bitDepth = state.VideoStream.BitDepth ?? 8;
return HlsCodecStringHelpers.GetVp9String(
width,
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 9bda27031..4adda0b69 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -156,6 +156,7 @@ public class MediaInfoHelper
/// <param name="enableTranscoding">Enable transcoding.</param>
/// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
/// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
/// <param name="ipAddress">Requesting IP address.</param>
public void SetDeviceSpecificData(
BaseItem item,
@@ -175,6 +176,7 @@ public class MediaInfoHelper
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy,
+ bool alwaysBurnInSubtitleWhenTranscoding,
IPAddress ipAddress)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
@@ -188,7 +190,8 @@ public class MediaInfoHelper
Profile = profile,
MaxAudioChannels = maxAudioChannels,
AllowAudioStreamCopy = allowAudioStreamCopy,
- AllowVideoStreamCopy = allowVideoStreamCopy
+ AllowVideoStreamCopy = allowVideoStreamCopy,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
@@ -290,6 +293,10 @@ public class MediaInfoHelper
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+ if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
+ {
+ mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
+ }
}
else
{
@@ -307,6 +314,11 @@ public class MediaInfoHelper
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
+
+ if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
+ {
+ mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
+ }
}
}
@@ -420,6 +432,7 @@ public class MediaInfoHelper
true,
true,
true,
+ request.AlwaysBurnInSubtitleWhenTranscoding,
httpContext.GetNormalizedRemoteIP());
}
else
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 535ef27c3..3a5db2f3f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -142,28 +142,15 @@ public static class StreamingHelpers
}
else
{
- // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
- // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
- // which will cause the client to request extremely high bitrate that may fail the player/encoder
- streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
-
- if (streamingRequest.SegmentContainer is not null)
- {
- // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
- // Notably: Some channels won't play on FireFox and LG webOS
- // Some channels from HDHomerun will experience A/V sync issues
- streamingRequest.SegmentContainer = "ts";
- streamingRequest.VideoCodec = "h264";
- streamingRequest.AudioCodec = "aac";
- state.SupportedVideoCodecs = ["h264"];
- state.Request.VideoCodec = "h264";
- state.SupportedAudioCodecs = ["aac"];
- state.Request.AudioCodec = "aac";
- }
-
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2;
+
+ // Cap the max bitrate when it is too high. This is usually due to ffmpeg is unable to probe the source liveTV streams' bitrate.
+ if (mediaSource.FallbackMaxStreamingBitrate is not null && streamingRequest.VideoBitRate is not null)
+ {
+ streamingRequest.VideoBitRate = Math.Min(streamingRequest.VideoBitRate.Value, mediaSource.FallbackMaxStreamingBitrate.Value);
+ }
}
var encodingOptions = serverConfigurationManager.GetEncodingOptions();
@@ -232,11 +219,17 @@ public static class StreamingHelpers
}
else
{
+ var h264EquivalentBitrate = EncodingHelper.ScaleBitrate(
+ state.OutputVideoBitrate.Value,
+ state.ActualOutputVideoCodec,
+ "h264");
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream?.BitRate,
state.OutputVideoBitrate.Value,
+ h264EquivalentBitrate,
state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
+ state.VideoRequest.MaxHeight,
+ state.TargetFramerate);
state.VideoRequest.MaxWidth = resolution.MaxWidth;
state.VideoRequest.MaxHeight = resolution.MaxHeight;
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index 53104988f..978e99b35 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -66,6 +66,11 @@ public class OpenLiveStreamDto
public bool? EnableDirectStream { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ /// </summary>
+ public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; }
+
+ /// <summary>
/// Gets or sets the device profile.
/// </summary>
public DeviceProfile? DeviceProfile { get; set; }
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
index 9e12ddde6..82f603ca1 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
@@ -82,4 +82,9 @@ public class PlaybackInfoDto
/// Gets or sets a value indicating whether to auto open the live stream.
/// </summary>
public bool? AutoOpenLiveStream { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ /// </summary>
+ public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; }
}
diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
index 392ef5ff4..aad578709 100644
--- a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
+++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Data.Dtos
+namespace Jellyfin.Data.Dtos;
+
+/// <summary>
+/// A dto representing custom options for a device.
+/// </summary>
+public class DeviceOptionsDto
{
/// <summary>
- /// A dto representing custom options for a device.
+ /// Gets or sets the id.
/// </summary>
- public class DeviceOptionsDto
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- public int Id { get; set; }
+ public int Id { get; set; }
- /// <summary>
- /// Gets or sets the device id.
- /// </summary>
- public string? DeviceId { get; set; }
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
- /// <summary>
- /// Gets or sets the custom name.
- /// </summary>
- public string? CustomName { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ public string? CustomName { get; set; }
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index d7a46e2d5..d3bff2936 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Enums;
@@ -13,6 +14,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using Microsoft.EntityFrameworkCore;
@@ -68,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public async Task UpdateDeviceOptions(string deviceId, string deviceName)
+ public async Task UpdateDeviceOptions(string deviceId, string? deviceName)
{
DeviceOptions? deviceOptions;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -105,29 +107,37 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public DeviceOptions GetDeviceOptions(string deviceId)
+ public DeviceOptionsDto? GetDeviceOptions(string deviceId)
{
- _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
+ if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions))
+ {
+ return ToDeviceOptionsDto(deviceOptions);
+ }
- return deviceOptions ?? new DeviceOptions(deviceId);
+ return null;
}
/// <inheritdoc />
- public ClientCapabilities GetCapabilities(string deviceId)
+ public ClientCapabilities GetCapabilities(string? deviceId)
{
+ if (deviceId is null)
+ {
+ return new();
+ }
+
return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result)
? result
- : new ClientCapabilities();
+ : new();
}
/// <inheritdoc />
- public DeviceInfo? GetDevice(string id)
+ public DeviceInfoDto? GetDevice(string id)
{
var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
_deviceOptions.TryGetValue(id, out var deviceOption);
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
- return deviceInfo;
+ return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo);
}
/// <inheritdoc />
@@ -135,8 +145,8 @@ namespace Jellyfin.Server.Implementations.Devices
{
IEnumerable<Device> devices = _devices.Values
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
- .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
- .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
+ .Where(device => query.DeviceId is null || device.DeviceId == query.DeviceId)
+ .Where(device => query.AccessToken is null || device.AccessToken == query.AccessToken)
.OrderBy(d => d.Id)
.ToList();
var count = devices.Count();
@@ -166,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
- public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
+ public QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId)
{
IEnumerable<Device> devices = _devices.Values
.OrderByDescending(d => d.DateLastActivity)
@@ -187,9 +197,11 @@ namespace Jellyfin.Server.Implementations.Devices
{
_deviceOptions.TryGetValue(device.DeviceId, out var option);
return ToDeviceInfo(device, option);
- }).ToArray();
+ })
+ .Select(ToDeviceInfoDto)
+ .ToArray();
- return new QueryResult<DeviceInfo>(array);
+ return new QueryResult<DeviceInfoDto>(array);
}
/// <inheritdoc />
@@ -235,13 +247,9 @@ namespace Jellyfin.Server.Implementations.Devices
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{
var caps = GetCapabilities(authInfo.DeviceId);
- var user = _userManager.GetUserById(authInfo.UserId);
- if (user is null)
- {
- throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
- }
+ var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
- return new DeviceInfo
+ return new()
{
AppName = authInfo.AppName,
AppVersion = authInfo.AppVersion,
@@ -254,5 +262,48 @@ namespace Jellyfin.Server.Implementations.Devices
CustomName = options?.CustomName,
};
}
+
+ private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options)
+ {
+ return new()
+ {
+ Id = options.Id,
+ DeviceId = options.DeviceId,
+ CustomName = options.CustomName,
+ };
+ }
+
+ private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info)
+ {
+ return new()
+ {
+ Name = info.Name,
+ CustomName = info.CustomName,
+ AccessToken = info.AccessToken,
+ Id = info.Id,
+ LastUserName = info.LastUserName,
+ AppName = info.AppName,
+ AppVersion = info.AppVersion,
+ LastUserId = info.LastUserId,
+ DateLastActivity = info.DateLastActivity,
+ Capabilities = ToClientCapabilitiesDto(info.Capabilities),
+ IconUrl = info.IconUrl
+ };
+ }
+
+ /// <inheritdoc />
+ public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities)
+ {
+ return new()
+ {
+ PlayableMediaTypes = capabilities.PlayableMediaTypes,
+ SupportedCommands = capabilities.SupportedCommands,
+ SupportsMediaControl = capabilities.SupportsMediaControl,
+ SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier,
+ DeviceProfile = capabilities.DeviceProfile,
+ AppStoreUrl = capabilities.AppStoreUrl,
+ IconUrl = capabilities.IconUrl
+ };
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index 7916d15c9..d641f521b 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -1,14 +1,23 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Globalization;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model;
using MediaBrowser.Model.MediaSegments;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.MediaSegments;
@@ -17,15 +26,98 @@ namespace Jellyfin.Server.Implementations.MediaSegments;
/// </summary>
public class MediaSegmentManager : IMediaSegmentManager
{
+ private readonly ILogger<MediaSegmentManager> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IMediaSegmentProvider[] _segmentProviders;
+ private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaSegmentManager"/> class.
/// </summary>
+ /// <param name="logger">Logger.</param>
/// <param name="dbProvider">EFCore Database factory.</param>
- public MediaSegmentManager(IDbContextFactory<JellyfinDbContext> dbProvider)
+ /// <param name="segmentProviders">List of all media segment providers.</param>
+ /// <param name="libraryManager">Library manager.</param>
+ public MediaSegmentManager(
+ ILogger<MediaSegmentManager> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IEnumerable<IMediaSegmentProvider> segmentProviders,
+ ILibraryManager libraryManager)
{
+ _logger = logger;
_dbProvider = dbProvider;
+
+ _segmentProviders = segmentProviders
+ .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
+ .ToArray();
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public async Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(baseItem);
+ var providers = _segmentProviders
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .OrderBy(i =>
+ {
+ var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name);
+ return index == -1 ? int.MaxValue : index;
+ })
+ .ToList();
+
+ if (providers.Count == 0)
+ {
+ _logger.LogDebug("Skipping media segment extraction as no providers are enabled for {MediaPath}", baseItem.Path);
+ return;
+ }
+
+ using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+
+ if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false)))
+ {
+ _logger.LogDebug("Skip {MediaPath} as it already contains media segments", baseItem.Path);
+ return;
+ }
+
+ _logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
+
+ await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+
+ // no need to recreate the request object every time.
+ var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id };
+
+ foreach (var provider in providers)
+ {
+ if (!await provider.Supports(baseItem).ConfigureAwait(false))
+ {
+ _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
+ continue;
+ }
+
+ try
+ {
+ var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
+ .ConfigureAwait(false);
+ if (segments.Count == 0)
+ {
+ _logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
+ continue;
+ }
+
+ _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
+ var providerId = GetProviderId(provider.Name);
+ foreach (var segment in segments)
+ {
+ segment.ItemId = baseItem.Id;
+ await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
+ }
+ }
}
/// <inheritdoc />
@@ -103,4 +195,21 @@ public class MediaSegmentManager : IMediaSegmentManager
{
return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio;
}
+
+ /// <inheritdoc/>
+ public IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item)
+ {
+ if (item is not (Video or Audio))
+ {
+ return [];
+ }
+
+ return _segmentProviders
+ .Select(p => (p.Name, GetProviderId(p.Name)));
+ }
+
+ private string GetProviderId(string name)
+ => name.ToLowerInvariant()
+ .GetMD5()
+ .ToString("N", CultureInfo.InvariantCulture);
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs
new file mode 100644
index 000000000..8dba31a67
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs
@@ -0,0 +1,712 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20240928082930_MarkSegmentProviderIdNonNullable")]
+ partial class MarkSegmentProviderIdNonNullable
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs
new file mode 100644
index 000000000..55b90a54d
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class MarkSegmentProviderIdNonNullable : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "SegmentProviderId",
+ table: "MediaSegments",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "SegmentProviderId",
+ table: "MediaSegments",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 399e2a08a..f6191dd2c 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -282,15 +282,16 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
b.Property<long>("StartTicks")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
- b.Property<string>("SegmentProviderId")
- .HasColumnType("TEXT");
-
b.HasKey("Id");
b.ToTable("MediaSegments");
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index bb32b7c20..f6c48498c 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
- public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
+ {
+ var options = _config.Configuration.TrickplayOptions;
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var existingTrickplayResolutions = await GetTrickplayResolutions(video.Id).ConfigureAwait(false);
+ foreach (var resolution in existingTrickplayResolutions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var existingResolution = resolution.Key;
+ var tileWidth = resolution.Value.TileWidth;
+ var tileHeight = resolution.Value.TileHeight;
+ var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
+ var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false);
+ var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true);
+ if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir))
+ {
+ var localDirFiles = Directory.GetFiles(localOutputDir);
+ var mediaDirExists = Directory.Exists(mediaOutputDir);
+ if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists))
+ {
+ // Move images from local dir to media dir
+ MoveContent(localOutputDir, mediaOutputDir);
+ _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir);
+ }
+ }
+ else if (!shouldBeSavedWithMedia && Directory.Exists(mediaOutputDir))
+ {
+ var mediaDirFiles = Directory.GetFiles(mediaOutputDir);
+ var localDirExists = Directory.Exists(localOutputDir);
+ if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists))
+ {
+ // Move images from media dir to local dir
+ MoveContent(mediaOutputDir, localOutputDir);
+ _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir);
+ }
+ }
+ }
+ }
+
+ private void MoveContent(string sourceFolder, string destinationFolder)
+ {
+ _fileSystem.MoveDirectory(sourceFolder, destinationFolder);
+ var parent = Directory.GetParent(sourceFolder);
+ if (parent is not null)
+ {
+ var parentContent = Directory.GetDirectories(parent.FullName);
+ if (parentContent.Length == 0)
+ {
+ Directory.Delete(parent.FullName);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
@@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager
replace,
width,
options,
+ libraryOptions,
cancellationToken).ConfigureAwait(false);
}
}
@@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager
bool replace,
int width,
TrickplayOptions options,
+ LibraryOptions? libraryOptions,
CancellationToken cancellationToken)
{
if (!CanGenerateTrickplay(video, options.Interval))
@@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
}
- var outputDir = GetTrickplayDirectory(video, actualWidth);
+ var tileWidth = options.TileWidth;
+ var tileHeight = options.TileHeight;
+ var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
+ var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia);
- if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
+ // Import existing trickplay tiles
+ if (!replace && Directory.Exists(outputDir))
{
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
- return;
+ var existingFiles = Directory.GetFiles(outputDir);
+ if (existingFiles.Length > 0)
+ {
+ var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false);
+ if (hasTrickplayResolution)
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}.", video.Id);
+ return;
+ }
+
+ // Import tiles
+ var localTrickplayInfo = new TrickplayInfo
+ {
+ ItemId = video.Id,
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = existingFiles.Length,
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ foreach (var tile in existingFiles)
+ {
+ var image = _imageEncoder.GetImageSize(tile);
+ localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
+ localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
+ }
+
+ await SaveTrickplayInfo(localTrickplayInfo).ConfigureAwait(false);
+
+ _logger.LogDebug("Imported existing trickplay files for {ItemId}.", video.Id);
+ return;
+ }
}
+ // Generate trickplay tiles
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
@@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
- public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
+ public TrickplayInfo CreateTiles(IReadOnlyList<string> images, int width, TrickplayOptions options, string outputDir)
{
if (images.Count == 0)
{
@@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
- imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
+ imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
@@ -289,7 +388,7 @@ public class TrickplayManager : ITrickplayManager
Directory.Delete(outputDir, true);
}
- MoveDirectory(workDir, outputDir);
+ _fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
@@ -356,6 +455,26 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
+ public async Task<IReadOnlyList<TrickplayInfo>> GetTrickplayItemsAsync(int limit, int offset)
+ {
+ IReadOnlyList<TrickplayInfo> trickplayItems;
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ trickplayItems = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .OrderBy(i => i.ItemId)
+ .Skip(offset)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+ }
+
+ return trickplayItems;
+ }
+
+ /// <inheritdoc />
public async Task SaveTrickplayInfo(TrickplayInfo info)
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -392,9 +511,15 @@ public class TrickplayManager : ITrickplayManager
}
/// <inheritdoc />
- public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ public async Task<string> GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia)
{
- return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ var trickplayResolutions = await GetTrickplayResolutions(item.Id).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ return Path.Combine(GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, width, saveWithMedia), index + ".jpg");
+ }
+
+ return string.Empty;
}
/// <inheritdoc />
@@ -470,29 +595,33 @@ public class TrickplayManager : ITrickplayManager
return null;
}
- private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ /// <inheritdoc />
+ public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
- var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
-
- return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ var path = saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ var subdirectory = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} - {1}x{2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ tileWidth.ToString(CultureInfo.InvariantCulture),
+ tileHeight.ToString(CultureInfo.InvariantCulture));
+
+ return Path.Combine(path, subdirectory);
}
- private void MoveDirectory(string source, string destination)
+ private async Task<bool> HasTrickplayResolutionAsync(Guid itemId, int width)
{
- try
- {
- Directory.Move(source, destination);
- }
- catch (IOException)
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- // Cross device move requires a copy
- Directory.CreateDirectory(destination);
- foreach (string file in Directory.GetFiles(source))
- {
- File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
- }
-
- Directory.Delete(source, true);
+ return await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .AnyAsync(i => i.Width == width)
+ .ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 81fecc9a1..9d4441ac3 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -23,7 +23,8 @@ namespace Jellyfin.Server.Migrations
{
typeof(PreStartupRoutines.CreateNetworkConfiguration),
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
- typeof(PreStartupRoutines.MigrateNetworkConfiguration)
+ typeof(PreStartupRoutines.MigrateNetworkConfiguration),
+ typeof(PreStartupRoutines.MigrateEncodingOptions)
};
/// <summary>
@@ -46,6 +47,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
+ typeof(Routines.MoveTrickplayFiles)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
index 139a6ec64..8462d0a8c 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
@@ -132,5 +132,4 @@ public class CreateNetworkConfiguration : IMigrationRoutine
public string[] KnownProxies { get; set; } = Array.Empty<string>();
}
-#pragma warning restore
}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
new file mode 100644
index 000000000..61f5620dc
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
@@ -0,0 +1,245 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using Emby.Server.Implementations;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class MigrateEncodingOptions : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger<MigrateEncodingOptions> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateEncodingOptions"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public MigrateEncodingOptions(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<MigrateEncodingOptions>();
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB");
+
+ /// <inheritdoc />
+ public string Name => nameof(MigrateEncodingOptions);
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "encoding.xml");
+ var oldSerializer = new XmlSerializer(typeof(OldEncodingOptions), new XmlRootAttribute("EncodingOptions"));
+ OldEncodingOptions? oldConfig = null;
+
+ try
+ {
+ using var xmlReader = XmlReader.Create(path);
+ oldConfig = (OldEncodingOptions?)oldSerializer.Deserialize(xmlReader);
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogError(ex, "Migrate EncodingOptions deserialize Invalid Operation error");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Migrate EncodingOptions deserialize error");
+ }
+
+ if (oldConfig is null)
+ {
+ return;
+ }
+
+ var hardwareAccelerationType = HardwareAccelerationType.none;
+ if (Enum.TryParse<HardwareAccelerationType>(oldConfig.HardwareAccelerationType, true, out var parsedHardwareAccelerationType))
+ {
+ hardwareAccelerationType = parsedHardwareAccelerationType;
+ }
+
+ var tonemappingAlgorithm = TonemappingAlgorithm.none;
+ if (Enum.TryParse<TonemappingAlgorithm>(oldConfig.TonemappingAlgorithm, true, out var parsedTonemappingAlgorithm))
+ {
+ tonemappingAlgorithm = parsedTonemappingAlgorithm;
+ }
+
+ var tonemappingMode = TonemappingMode.auto;
+ if (Enum.TryParse<TonemappingMode>(oldConfig.TonemappingMode, true, out var parsedTonemappingMode))
+ {
+ tonemappingMode = parsedTonemappingMode;
+ }
+
+ var tonemappingRange = TonemappingRange.auto;
+ if (Enum.TryParse<TonemappingRange>(oldConfig.TonemappingRange, true, out var parsedTonemappingRange))
+ {
+ tonemappingRange = parsedTonemappingRange;
+ }
+
+ var encoderPreset = EncoderPreset.superfast;
+ if (Enum.TryParse<EncoderPreset>(oldConfig.TonemappingRange, true, out var parsedEncoderPreset))
+ {
+ encoderPreset = parsedEncoderPreset;
+ }
+
+ var deinterlaceMethod = DeinterlaceMethod.yadif;
+ if (Enum.TryParse<DeinterlaceMethod>(oldConfig.TonemappingRange, true, out var parsedDeinterlaceMethod))
+ {
+ deinterlaceMethod = parsedDeinterlaceMethod;
+ }
+
+ var encodingOptions = new EncodingOptions()
+ {
+ EncodingThreadCount = oldConfig.EncodingThreadCount,
+ TranscodingTempPath = oldConfig.TranscodingTempPath,
+ FallbackFontPath = oldConfig.FallbackFontPath,
+ EnableFallbackFont = oldConfig.EnableFallbackFont,
+ EnableAudioVbr = oldConfig.EnableAudioVbr,
+ DownMixAudioBoost = oldConfig.DownMixAudioBoost,
+ DownMixStereoAlgorithm = oldConfig.DownMixStereoAlgorithm,
+ MaxMuxingQueueSize = oldConfig.MaxMuxingQueueSize,
+ EnableThrottling = oldConfig.EnableThrottling,
+ ThrottleDelaySeconds = oldConfig.ThrottleDelaySeconds,
+ EnableSegmentDeletion = oldConfig.EnableSegmentDeletion,
+ SegmentKeepSeconds = oldConfig.SegmentKeepSeconds,
+ HardwareAccelerationType = hardwareAccelerationType,
+ EncoderAppPath = oldConfig.EncoderAppPath,
+ EncoderAppPathDisplay = oldConfig.EncoderAppPathDisplay,
+ VaapiDevice = oldConfig.VaapiDevice,
+ EnableTonemapping = oldConfig.EnableTonemapping,
+ EnableVppTonemapping = oldConfig.EnableVppTonemapping,
+ EnableVideoToolboxTonemapping = oldConfig.EnableVideoToolboxTonemapping,
+ TonemappingAlgorithm = tonemappingAlgorithm,
+ TonemappingMode = tonemappingMode,
+ TonemappingRange = tonemappingRange,
+ TonemappingDesat = oldConfig.TonemappingDesat,
+ TonemappingPeak = oldConfig.TonemappingPeak,
+ TonemappingParam = oldConfig.TonemappingParam,
+ VppTonemappingBrightness = oldConfig.VppTonemappingBrightness,
+ VppTonemappingContrast = oldConfig.VppTonemappingContrast,
+ H264Crf = oldConfig.H264Crf,
+ H265Crf = oldConfig.H265Crf,
+ EncoderPreset = encoderPreset,
+ DeinterlaceDoubleRate = oldConfig.DeinterlaceDoubleRate,
+ DeinterlaceMethod = deinterlaceMethod,
+ EnableDecodingColorDepth10Hevc = oldConfig.EnableDecodingColorDepth10Hevc,
+ EnableDecodingColorDepth10Vp9 = oldConfig.EnableDecodingColorDepth10Vp9,
+ EnableEnhancedNvdecDecoder = oldConfig.EnableEnhancedNvdecDecoder,
+ PreferSystemNativeHwDecoder = oldConfig.PreferSystemNativeHwDecoder,
+ EnableIntelLowPowerH264HwEncoder = oldConfig.EnableIntelLowPowerH264HwEncoder,
+ EnableIntelLowPowerHevcHwEncoder = oldConfig.EnableIntelLowPowerHevcHwEncoder,
+ EnableHardwareEncoding = oldConfig.EnableHardwareEncoding,
+ AllowHevcEncoding = oldConfig.AllowHevcEncoding,
+ AllowAv1Encoding = oldConfig.AllowAv1Encoding,
+ EnableSubtitleExtraction = oldConfig.EnableSubtitleExtraction,
+ HardwareDecodingCodecs = oldConfig.HardwareDecodingCodecs,
+ AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = oldConfig.AllowOnDemandMetadataBasedKeyframeExtractionForExtensions
+ };
+
+ var newSerializer = new XmlSerializer(typeof(EncodingOptions));
+ var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ newSerializer.Serialize(xmlWriter, encodingOptions);
+ }
+
+#pragma warning disable
+ public sealed class OldEncodingOptions
+ {
+ public int EncodingThreadCount { get; set; }
+
+ public string TranscodingTempPath { get; set; }
+
+ public string FallbackFontPath { get; set; }
+
+ public bool EnableFallbackFont { get; set; }
+
+ public bool EnableAudioVbr { get; set; }
+
+ public double DownMixAudioBoost { get; set; }
+
+ public DownMixStereoAlgorithms DownMixStereoAlgorithm { get; set; }
+
+ public int MaxMuxingQueueSize { get; set; }
+
+ public bool EnableThrottling { get; set; }
+
+ public int ThrottleDelaySeconds { get; set; }
+
+ public bool EnableSegmentDeletion { get; set; }
+
+ public int SegmentKeepSeconds { get; set; }
+
+ public string HardwareAccelerationType { get; set; }
+
+ public string EncoderAppPath { get; set; }
+
+ public string EncoderAppPathDisplay { get; set; }
+
+ public string VaapiDevice { get; set; }
+
+ public bool EnableTonemapping { get; set; }
+
+ public bool EnableVppTonemapping { get; set; }
+
+ public bool EnableVideoToolboxTonemapping { get; set; }
+
+ public string TonemappingAlgorithm { get; set; }
+
+ public string TonemappingMode { get; set; }
+
+ public string TonemappingRange { get; set; }
+
+ public double TonemappingDesat { get; set; }
+
+ public double TonemappingPeak { get; set; }
+
+ public double TonemappingParam { get; set; }
+
+ public double VppTonemappingBrightness { get; set; }
+
+ public double VppTonemappingContrast { get; set; }
+
+ public int H264Crf { get; set; }
+
+ public int H265Crf { get; set; }
+
+ public string EncoderPreset { get; set; }
+
+ public bool DeinterlaceDoubleRate { get; set; }
+
+ public string DeinterlaceMethod { get; set; }
+
+ public bool EnableDecodingColorDepth10Hevc { get; set; }
+
+ public bool EnableDecodingColorDepth10Vp9 { get; set; }
+
+ public bool EnableEnhancedNvdecDecoder { get; set; }
+
+ public bool PreferSystemNativeHwDecoder { get; set; }
+
+ public bool EnableIntelLowPowerH264HwEncoder { get; set; }
+
+ public bool EnableIntelLowPowerHevcHwEncoder { get; set; }
+
+ public bool EnableHardwareEncoding { get; set; }
+
+ public bool AllowHevcEncoding { get; set; }
+
+ public bool AllowAv1Encoding { get; set; }
+
+ public bool EnableSubtitleExtraction { get; set; }
+
+ public string[] HardwareDecodingCodecs { get; set; }
+
+ public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
index 0544fe561..580282a5f 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -48,9 +48,11 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
if (oldPluginConfiguration is not null)
{
- var newPluginConfiguration = new PluginConfiguration();
- newPluginConfiguration.Server = oldPluginConfiguration.Server;
- newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName;
+ var newPluginConfiguration = new PluginConfiguration
+ {
+ Server = oldPluginConfiguration.Server,
+ ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName
+ };
var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0;
newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit;
WriteNew(path, newPluginConfiguration);
@@ -93,6 +95,4 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
public bool ReplaceArtistName { get; set; }
}
-#pragma warning restore
-
}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
index d92c00991..49960f430 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
@@ -55,49 +55,53 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
_logger.LogError(ex, "Migrate NetworkConfiguration deserialize error");
}
- if (oldNetworkConfiguration is not null)
+ if (oldNetworkConfiguration is null)
{
- // Migrate network config values to new config schema
- var networkConfiguration = new NetworkConfiguration();
- networkConfiguration.AutoDiscovery = oldNetworkConfiguration.AutoDiscovery;
- networkConfiguration.BaseUrl = oldNetworkConfiguration.BaseUrl;
- networkConfiguration.CertificatePassword = oldNetworkConfiguration.CertificatePassword;
- networkConfiguration.CertificatePath = oldNetworkConfiguration.CertificatePath;
- networkConfiguration.EnableHttps = oldNetworkConfiguration.EnableHttps;
- networkConfiguration.EnableIPv4 = oldNetworkConfiguration.EnableIPV4;
- networkConfiguration.EnableIPv6 = oldNetworkConfiguration.EnableIPV6;
- networkConfiguration.EnablePublishedServerUriByRequest = oldNetworkConfiguration.EnablePublishedServerUriByRequest;
- networkConfiguration.EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess;
- networkConfiguration.EnableUPnP = oldNetworkConfiguration.EnableUPnP;
- networkConfiguration.IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces;
- networkConfiguration.InternalHttpPort = oldNetworkConfiguration.HttpServerPortNumber;
- networkConfiguration.InternalHttpsPort = oldNetworkConfiguration.HttpsPortNumber;
- networkConfiguration.IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist;
- networkConfiguration.KnownProxies = oldNetworkConfiguration.KnownProxies;
- networkConfiguration.LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses;
- networkConfiguration.LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets;
- networkConfiguration.PublicHttpPort = oldNetworkConfiguration.PublicPort;
- networkConfiguration.PublicHttpsPort = oldNetworkConfiguration.PublicHttpsPort;
- networkConfiguration.PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet;
- networkConfiguration.RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter;
- networkConfiguration.RequireHttps = oldNetworkConfiguration.RequireHttps;
-
- // Migrate old virtual interface name schema
- var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames;
- if (oldVirtualInterfaceNames.Equals("vEthernet*", StringComparison.OrdinalIgnoreCase))
- {
- networkConfiguration.VirtualInterfaceNames = new string[] { "veth" };
- }
- else
- {
- networkConfiguration.VirtualInterfaceNames = oldVirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).Split(',');
- }
+ return;
+ }
- var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
- var xmlWriterSettings = new XmlWriterSettings { Indent = true };
- using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
- networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
+ // Migrate network config values to new config schema
+ var networkConfiguration = new NetworkConfiguration
+ {
+ AutoDiscovery = oldNetworkConfiguration.AutoDiscovery,
+ BaseUrl = oldNetworkConfiguration.BaseUrl,
+ CertificatePassword = oldNetworkConfiguration.CertificatePassword,
+ CertificatePath = oldNetworkConfiguration.CertificatePath,
+ EnableHttps = oldNetworkConfiguration.EnableHttps,
+ EnableIPv4 = oldNetworkConfiguration.EnableIPV4,
+ EnableIPv6 = oldNetworkConfiguration.EnableIPV6,
+ EnablePublishedServerUriByRequest = oldNetworkConfiguration.EnablePublishedServerUriByRequest,
+ EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess,
+ EnableUPnP = oldNetworkConfiguration.EnableUPnP,
+ IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces,
+ InternalHttpPort = oldNetworkConfiguration.HttpServerPortNumber,
+ InternalHttpsPort = oldNetworkConfiguration.HttpsPortNumber,
+ IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist,
+ KnownProxies = oldNetworkConfiguration.KnownProxies,
+ LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses,
+ LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets,
+ PublicHttpPort = oldNetworkConfiguration.PublicPort,
+ PublicHttpsPort = oldNetworkConfiguration.PublicHttpsPort,
+ PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet,
+ RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter,
+ RequireHttps = oldNetworkConfiguration.RequireHttps
+ };
+
+ // Migrate old virtual interface name schema
+ var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames;
+ if (oldVirtualInterfaceNames.Equals("vEthernet*", StringComparison.OrdinalIgnoreCase))
+ {
+ networkConfiguration.VirtualInterfaceNames = new string[] { "veth" };
}
+ else
+ {
+ networkConfiguration.VirtualInterfaceNames = oldVirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).Split(',');
+ }
+
+ var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
+ var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
}
#pragma warning disable
@@ -204,5 +208,4 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
public bool EnablePublishedServerUriByRequest { get; set; } = false;
}
-#pragma warning restore
}
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
new file mode 100644
index 000000000..c1a9e8894
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to move trickplay files to the new directory.
+/// </summary>
+public class MoveTrickplayFiles : IMigrationRoutine
+{
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MoveTrickplayFiles> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MoveTrickplayFiles"/> class.
+ /// </summary>
+ /// <param name="trickplayManager">Instance of the <see cref="ITrickplayManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger<MoveTrickplayFiles> logger)
+ {
+ _trickplayManager = trickplayManager;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
+
+ /// <inheritdoc />
+ public string Name => "MoveTrickplayFiles";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => true;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 100;
+ int itemCount = 0, offset = 0, previousCount;
+
+ var sw = Stopwatch.StartNew();
+ var trickplayQuery = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false
+ };
+
+ do
+ {
+ var trickplayInfos = _trickplayManager.GetTrickplayItemsAsync(Limit, offset).GetAwaiter().GetResult();
+ previousCount = trickplayInfos.Count;
+ offset += Limit;
+
+ trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray();
+ var items = _libraryManager.GetItemList(trickplayQuery);
+ foreach (var trickplayInfo in trickplayInfos)
+ {
+ var item = items.OfType<Video>().FirstOrDefault(i => i.Id.Equals(trickplayInfo.ItemId));
+ if (item is null)
+ {
+ continue;
+ }
+
+ if (++itemCount % 1_000 == 0)
+ {
+ _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+
+ var oldPath = GetOldTrickplayDirectory(item, trickplayInfo.Width);
+ var newPath = _trickplayManager.GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false);
+ if (_fileSystem.DirectoryExists(oldPath))
+ {
+ _fileSystem.MoveDirectory(oldPath, newPath);
+ }
+ }
+ } while (previousCount == Limit);
+
+ _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+
+ private string GetOldTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+}
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index c3989751c..91ac827ca 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -68,6 +68,12 @@ namespace Jellyfin.Server
public string? PublishedServerUrl { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether the server should not detect network status change.
+ /// </summary>
+ [Option("nonetchange", Required = false, HelpText = "Indicates that the server should not detect network status change.")]
+ public bool NoDetectNetworkChange { get; set; }
+
+ /// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
/// </summary>
/// <returns>The configuration dictionary.</returns>
@@ -90,6 +96,11 @@ namespace Jellyfin.Server
config.Add(FfmpegPathKey, FFmpegPath);
}
+ if (NoDetectNetworkChange)
+ {
+ config.Add(DetectNetworkChangeKey, bool.FalseString);
+ }
+
return config;
}
}
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
index 635e4eb3d..daf4d9631 100644
--- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
+++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
@@ -1,20 +1,31 @@
#nullable disable
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
-namespace MediaBrowser.Controller.Authentication
+namespace MediaBrowser.Controller.Authentication;
+
+/// <summary>
+/// A class representing an authentication result.
+/// </summary>
+public class AuthenticationResult
{
- public class AuthenticationResult
- {
- public UserDto User { get; set; }
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ public UserDto User { get; set; }
- public SessionInfo SessionInfo { get; set; }
+ /// <summary>
+ /// Gets or sets the session info.
+ /// </summary>
+ public SessionInfoDto SessionInfo { get; set; }
- public string AccessToken { get; set; }
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
- public string ServerId { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ public string ServerId { get; set; }
}
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index 5566421cb..cade53d99 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -1,81 +1,117 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
using System;
using System.Threading.Tasks;
+using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
-namespace MediaBrowser.Controller.Devices
+namespace MediaBrowser.Controller.Devices;
+
+/// <summary>
+/// Device manager interface.
+/// </summary>
+public interface IDeviceManager
{
- public interface IDeviceManager
- {
- event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+ /// <summary>
+ /// Event handler for updated device options.
+ /// </summary>
+ event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+
+ /// <summary>
+ /// Creates a new device.
+ /// </summary>
+ /// <param name="device">The device to create.</param>
+ /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
+ Task<Device> CreateDevice(Device device);
- /// <summary>
- /// Creates a new device.
- /// </summary>
- /// <param name="device">The device to create.</param>
- /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
- Task<Device> CreateDevice(Device device);
+ /// <summary>
+ /// Saves the capabilities.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="capabilities">The capabilities.</param>
+ void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
- /// <summary>
- /// Saves the capabilities.
- /// </summary>
- /// <param name="deviceId">The device id.</param>
- /// <param name="capabilities">The capabilities.</param>
- void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
+ /// <summary>
+ /// Gets the capabilities.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <returns>ClientCapabilities.</returns>
+ ClientCapabilities GetCapabilities(string? deviceId);
- /// <summary>
- /// Gets the capabilities.
- /// </summary>
- /// <param name="deviceId">The device id.</param>
- /// <returns>ClientCapabilities.</returns>
- ClientCapabilities GetCapabilities(string deviceId);
+ /// <summary>
+ /// Gets the device information.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>DeviceInfoDto.</returns>
+ DeviceInfoDto? GetDevice(string id);
- /// <summary>
- /// Gets the device information.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <returns>DeviceInfo.</returns>
- DeviceInfo GetDevice(string id);
+ /// <summary>
+ /// Gets devices based on the provided query.
+ /// </summary>
+ /// <param name="query">The device query.</param>
+ /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
+ QueryResult<Device> GetDevices(DeviceQuery query);
- /// <summary>
- /// Gets devices based on the provided query.
- /// </summary>
- /// <param name="query">The device query.</param>
- /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
- QueryResult<Device> GetDevices(DeviceQuery query);
+ /// <summary>
+ /// Gets device infromation based on the provided query.
+ /// </summary>
+ /// <param name="query">The device query.</param>
+ /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns>
+ QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
- QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
+ /// <summary>
+ /// Gets the device information.
+ /// </summary>
+ /// <param name="userId">The user's id, or <c>null</c>.</param>
+ /// <returns>IEnumerable&lt;DeviceInfoDto&gt;.</returns>
+ QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId);
- /// <summary>
- /// Gets the devices.
- /// </summary>
- /// <param name="userId">The user's id, or <c>null</c>.</param>
- /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
- QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
+ /// <summary>
+ /// Deletes a device.
+ /// </summary>
+ /// <param name="device">The device.</param>
+ /// <returns>A <see cref="Task"/> representing the deletion of the device.</returns>
+ Task DeleteDevice(Device device);
- Task DeleteDevice(Device device);
+ /// <summary>
+ /// Updates a device.
+ /// </summary>
+ /// <param name="device">The device.</param>
+ /// <returns>A <see cref="Task"/> representing the update of the device.</returns>
+ Task UpdateDevice(Device device);
- Task UpdateDevice(Device device);
+ /// <summary>
+ /// Determines whether this instance [can access device] the specified user identifier.
+ /// </summary>
+ /// <param name="user">The user to test.</param>
+ /// <param name="deviceId">The device id to test.</param>
+ /// <returns>Whether the user can access the device.</returns>
+ bool CanAccessDevice(User user, string deviceId);
- /// <summary>
- /// Determines whether this instance [can access device] the specified user identifier.
- /// </summary>
- /// <param name="user">The user to test.</param>
- /// <param name="deviceId">The device id to test.</param>
- /// <returns>Whether the user can access the device.</returns>
- bool CanAccessDevice(User user, string deviceId);
+ /// <summary>
+ /// Updates the options of a device.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="deviceName">The device name.</param>
+ /// <returns>A <see cref="Task"/> representing the update of the device options.</returns>
+ Task UpdateDeviceOptions(string deviceId, string? deviceName);
- Task UpdateDeviceOptions(string deviceId, string deviceName);
+ /// <summary>
+ /// Gets the options of a device.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ /// <returns><see cref="DeviceOptions"/> of the device.</returns>
+ DeviceOptionsDto? GetDeviceOptions(string deviceId);
- DeviceOptions GetDeviceOptions(string deviceId);
- }
+ /// <summary>
+ /// Gets the dto for client capabilites.
+ /// </summary>
+ /// <param name="capabilities">The client capabilities.</param>
+ /// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns>
+ ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 125f8f225..eb605f6c8 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1087,12 +1087,7 @@ namespace MediaBrowser.Controller.Entities
return 1;
}).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
- .ThenByDescending(i =>
- {
- var stream = i.VideoStream;
-
- return stream is null || stream.Width is null ? 0 : stream.Width.Value;
- })
+ .ThenByDescending(i => i, new MediaSourceWidthComparator())
.ToList();
}
@@ -1185,28 +1180,29 @@ namespace MediaBrowser.Controller.Entities
return info;
}
- private string GetMediaSourceName(BaseItem item)
+ internal string GetMediaSourceName(BaseItem item)
{
var terms = new List<string>();
var path = item.Path;
if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
{
+ var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
if (HasLocalAlternateVersions)
{
- var displayName = System.IO.Path.GetFileNameWithoutExtension(path)
- .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase)
- .TrimStart(new char[] { ' ', '-' });
-
- if (!string.IsNullOrEmpty(displayName))
+ var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
+ if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase))
{
- terms.Add(displayName);
+ var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
+ if (!name.IsWhiteSpace())
+ {
+ terms.Add(name.ToString());
+ }
}
}
if (terms.Count == 0)
{
- var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
terms.Add(displayName);
}
}
@@ -1612,7 +1608,7 @@ namespace MediaBrowser.Controller.Entities
}
var parent = GetParents().FirstOrDefault() ?? this;
- if (parent is UserRootFolder or AggregateFolder)
+ if (parent is UserRootFolder or AggregateFolder or UserView)
{
return true;
}
diff --git a/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs b/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs
new file mode 100644
index 000000000..0224577a4
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Intrinsics.X86;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Entities;
+
+/// <summary>
+/// Compare MediaSource of the same file by Video width <see cref="IComparer{T}" />.
+/// </summary>
+public class MediaSourceWidthComparator : IComparer<MediaSourceInfo>
+{
+ /// <inheritdoc />
+ public int Compare(MediaSourceInfo? x, MediaSourceInfo? y)
+ {
+ if (x is null && y is null)
+ {
+ return 0;
+ }
+
+ if (x is null)
+ {
+ return -1;
+ }
+
+ if (y is null)
+ {
+ return 1;
+ }
+
+ if (string.Equals(x.Path, y.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ if (x.VideoStream is null && y.VideoStream is null)
+ {
+ return 0;
+ }
+
+ if (x.VideoStream is null)
+ {
+ return -1;
+ }
+
+ if (y.VideoStream is null)
+ {
+ return 1;
+ }
+
+ var xWidth = x.VideoStream.Width ?? 0;
+ var yWidth = y.VideoStream.Width ?? 0;
+
+ return xWidth - yWidth;
+ }
+
+ return 0;
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 5c54f014c..46bad3f3b 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -180,10 +180,7 @@ namespace MediaBrowser.Controller.Entities.TV
}
public string FindSeriesPresentationUniqueKey()
- {
- var series = Series;
- return series is null ? null : series.PresentationUniqueKey;
- }
+ => Series?.PresentationUniqueKey;
public string FindSeasonName()
{
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 2fda7ee6f..420349f35 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -430,8 +430,6 @@ namespace MediaBrowser.Controller.Entities
InternalItemsQuery query,
ILibraryManager libraryManager)
{
- var user = query.User;
-
// This must be the last filter
if (!query.AdjacentTo.IsNullOrEmpty())
{
diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs
index 357ef9406..1542c58b3 100644
--- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs
@@ -1,6 +1,5 @@
using System;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Events.Authentication;
@@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs
/// <summary>
/// Gets or sets the session information.
/// </summary>
- public SessionInfo? SessionInfo { get; set; }
+ public SessionInfoDto? SessionInfo { get; set; }
/// <summary>
/// Gets or sets the server id.
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 0aaf4fcd9..f8049cd48 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -40,6 +40,11 @@ namespace MediaBrowser.Controller.Extensions
public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration";
/// <summary>
+ /// The key for the FFmpeg image extraction performance tradeoff option.
+ /// </summary>
+ public const string FfmpegImgExtractPerfTradeoffKey = "FFmpeg:imgExtractPerfTradeoff";
+
+ /// <summary>
/// The key for the FFmpeg path option.
/// </summary>
public const string FfmpegPathKey = "ffmpeg";
@@ -70,6 +75,11 @@ namespace MediaBrowser.Controller.Extensions
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
+ /// The key for a setting that indicates whether the application should detect network status change.
+ /// </summary>
+ public const string DetectNetworkChangeKey = "DetectNetworkChange";
+
+ /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -103,6 +113,14 @@ namespace MediaBrowser.Controller.Extensions
=> configuration.GetValue<bool>(FfmpegSkipValidationKey);
/// <summary>
+ /// Gets a value indicating whether the server should trade off for performance during FFmpeg image extraction.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns><c>true</c> if the server should trade off for performance during FFmpeg image extraction, otherwise <c>false</c>.</returns>
+ public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
+ => configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
+
+ /// <summary>
/// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index f77186e25..20f51ddb7 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -193,6 +193,8 @@ namespace MediaBrowser.Controller.MediaEncoding
public bool EnableAudioVbrEncoding { get; set; }
+ public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
+
public string GetOption(string qualifier, string name)
{
var value = GetOption(qualifier + "-" + name);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 24cd141dc..c120e08fa 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -37,6 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
+ private const string _defaultMjpegEncoder = "mjpeg";
+
private const string QsvAlias = "qs";
private const string VaapiAlias = "va";
private const string D3d11vaAlias = "dx11";
@@ -69,11 +71,14 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1);
private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1);
+ private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1);
+ private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
+ private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
- private static readonly string[] _videoProfilesH264 = new[]
- {
+ private static readonly string[] _videoProfilesH264 =
+ [
"ConstrainedBaseline",
"Baseline",
"Extended",
@@ -82,20 +87,20 @@ namespace MediaBrowser.Controller.MediaEncoding
"ProgressiveHigh",
"ConstrainedHigh",
"High10"
- };
+ ];
- private static readonly string[] _videoProfilesH265 = new[]
- {
+ private static readonly string[] _videoProfilesH265 =
+ [
"Main",
"Main10"
- };
+ ];
- private static readonly string[] _videoProfilesAv1 = new[]
- {
+ private static readonly string[] _videoProfilesAv1 =
+ [
"Main",
"High",
"Professional",
- };
+ ];
private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
{
@@ -107,8 +112,8 @@ namespace MediaBrowser.Controller.MediaEncoding
"m4v",
};
- private static readonly string[] _legacyTonemapModes = new[] { "max", "rgb" };
- private static readonly string[] _advancedTonemapModes = new[] { "lum", "itp" };
+ private static readonly TonemappingMode[] _legacyTonemapModes = [TonemappingMode.max, TonemappingMode.rgb];
+ private static readonly TonemappingMode[] _advancedTonemapModes = [TonemappingMode.lum, TonemappingMode.itp];
// Set max transcoding channels for encoders that can't handle more than a set amount of channels
// AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels
@@ -123,23 +128,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
- private static readonly string _defaultMjpegEncoder = "mjpeg";
- private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
+ private static readonly Dictionary<HardwareAccelerationType, string> _mjpegCodecMap = new()
{
- { "vaapi", _defaultMjpegEncoder + "_vaapi" },
- { "qsv", _defaultMjpegEncoder + "_qsv" },
- { "videotoolbox", _defaultMjpegEncoder + "_videotoolbox" }
+ { HardwareAccelerationType.vaapi, _defaultMjpegEncoder + "_vaapi" },
+ { HardwareAccelerationType.qsv, _defaultMjpegEncoder + "_qsv" },
+ { HardwareAccelerationType.videotoolbox, _defaultMjpegEncoder + "_videotoolbox" },
+ { HardwareAccelerationType.rkmpp, _defaultMjpegEncoder + "_rkmpp" }
};
- public static readonly string[] LosslessAudioCodecs = new string[]
- {
+ public static readonly string[] LosslessAudioCodecs =
+ [
"alac",
"ape",
"flac",
"mlp",
"truehd",
"wavpack"
- };
+ ];
public EncodingHelper(
IApplicationPaths appPaths,
@@ -176,18 +181,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var hwType = encodingOptions.HardwareAccelerationType;
- var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ var codecMap = new Dictionary<HardwareAccelerationType, string>()
{
- { "amf", hwEncoder + "_amf" },
- { "nvenc", hwEncoder + "_nvenc" },
- { "qsv", hwEncoder + "_qsv" },
- { "vaapi", hwEncoder + "_vaapi" },
- { "videotoolbox", hwEncoder + "_videotoolbox" },
- { "v4l2m2m", hwEncoder + "_v4l2m2m" },
- { "rkmpp", hwEncoder + "_rkmpp" },
+ { HardwareAccelerationType.amf, hwEncoder + "_amf" },
+ { HardwareAccelerationType.nvenc, hwEncoder + "_nvenc" },
+ { HardwareAccelerationType.qsv, hwEncoder + "_qsv" },
+ { HardwareAccelerationType.vaapi, hwEncoder + "_vaapi" },
+ { HardwareAccelerationType.videotoolbox, hwEncoder + "_videotoolbox" },
+ { HardwareAccelerationType.v4l2m2m, hwEncoder + "_v4l2m2m" },
+ { HardwareAccelerationType.rkmpp, hwEncoder + "_rkmpp" },
};
- if (!string.IsNullOrEmpty(hwType)
+ if (hwType != HardwareAccelerationType.none
&& encodingOptions.EnableHardwareEncoding
&& codecMap.TryGetValue(hwType, out var preferredEncoder)
&& _mediaEncoder.SupportsEncoder(preferredEncoder))
@@ -205,7 +210,15 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var hwType = encodingOptions.HardwareAccelerationType;
- if (!string.IsNullOrEmpty(hwType)
+ // Only Intel has VA-API MJPEG encoder
+ if (hwType == HardwareAccelerationType.vaapi
+ && !(_mediaEncoder.IsVaapiDeviceInteliHD
+ || _mediaEncoder.IsVaapiDeviceInteli965))
+ {
+ return _defaultMjpegEncoder;
+ }
+
+ if (hwType != HardwareAccelerationType.none
&& encodingOptions.EnableHardwareEncoding
&& _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder)
&& _mediaEncoder.SupportsEncoder(preferredEncoder))
@@ -297,7 +310,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.VideoStream is null
|| !options.EnableTonemapping
- || GetVideoColorBitDepth(state) != 10
+ || GetVideoColorBitDepth(state) < 10
|| !_mediaEncoder.SupportsFilter("tonemapx"))
{
return false;
@@ -310,13 +323,12 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.VideoStream is null
|| !options.EnableTonemapping
- || GetVideoColorBitDepth(state) != 10)
+ || GetVideoColorBitDepth(state) < 10)
{
return false;
}
- if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
- && state.VideoStream.VideoRange == VideoRange.HDR
+ if (state.VideoStream.VideoRange == VideoRange.HDR
&& state.VideoStream.VideoRangeType == VideoRangeType.DOVI)
{
// Only native SW decoder and HW accelerator can parse dovi rpu.
@@ -353,7 +365,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.VideoStream is null
|| !options.EnableVppTonemapping
- || GetVideoColorBitDepth(state) != 10)
+ || GetVideoColorBitDepth(state) < 10)
{
return false;
}
@@ -361,7 +373,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefer 'tonemap_vaapi' over 'vpp_qsv' on Linux for supporting Gen9/KBLx.
// 'vpp_qsv' requires VPL, which is only supported on Gen12/TGLx and newer.
if (OperatingSystem.IsWindows()
- && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
+ && options.HardwareAccelerationType == HardwareAccelerationType.qsv
&& _mediaEncoder.EncoderVersion < _minFFmpegQsvVppTonemapOption)
{
return false;
@@ -376,7 +388,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.VideoStream is null
|| !options.EnableVideoToolboxTonemapping
- || GetVideoColorBitDepth(state) != 10)
+ || GetVideoColorBitDepth(state) < 10)
{
return false;
}
@@ -387,6 +399,25 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
}
+ private bool IsVideoStreamHevcRext(EncodingJobInfo state)
+ {
+ var videoStream = state.VideoStream;
+ if (videoStream is null)
+ {
+ return false;
+ }
+
+ return string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(videoStream.Profile, "Rext", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase));
+ }
+
/// <summary>
/// Gets the name of the output video codec.
/// </summary>
@@ -851,13 +882,15 @@ namespace MediaBrowser.Controller.MediaEncoding
options);
}
- private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
+ private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string vendorId, string srcDeviceAlias, string alias)
{
alias ??= VaapiAlias;
+ var haveVendorId = !string.IsNullOrEmpty(vendorId)
+ && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId;
- // 'renderNodePath' has higher priority than 'kernelDriver'
+ // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver'
var driverOpts = string.IsNullOrEmpty(renderNodePath)
- ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver)
+ ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}"))
: renderNodePath;
// 'driver' behaves similarly to env LIBVA_DRIVER_NAME
@@ -892,7 +925,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (OperatingSystem.IsLinux())
{
// derive qsv from vaapi device
- return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
+ return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", "0x8086", null, VaapiAlias) + arg + "@" + VaapiAlias;
}
if (OperatingSystem.IsWindows())
@@ -921,7 +954,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// DVBSUB uses the fixed canvas size 720x576
if (state.SubtitleStream is not null
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
+ && ShouldEncodeSubtitle(state)
&& !state.SubtitleStream.IsTextSubtitleStream
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
{
@@ -971,7 +1004,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isHwTonemapAvailable = IsHwTonemapAvailable(state, options);
- if (string.Equals(optHwaccelType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ if (optHwaccelType == HardwareAccelerationType.vaapi)
{
if (!isLinux || !_mediaEncoder.SupportsHwaccel("vaapi"))
{
@@ -987,14 +1020,14 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
- args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, null, VaapiAlias));
}
else if (_mediaEncoder.IsVaapiDeviceInteli965)
{
// Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
- args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, null, VaapiAlias));
}
var filterDevArgs = string.Empty;
@@ -1018,7 +1051,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
- args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(null, null, null, null, DrmAlias, VaapiAlias));
args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias));
// libplacebo wants an explicitly set vulkan filter device.
@@ -1026,7 +1059,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else
{
- args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, null, VaapiAlias));
filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
if (doOclTonemap)
@@ -1045,7 +1078,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(filterDevArgs);
}
- else if (string.Equals(optHwaccelType, "qsv", StringComparison.OrdinalIgnoreCase))
+ else if (optHwaccelType == HardwareAccelerationType.qsv)
{
if ((!isLinux && !isWindows) || !_mediaEncoder.SupportsHwaccel("qsv"))
{
@@ -1080,7 +1113,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(filterDevArgs);
}
- else if (string.Equals(optHwaccelType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ else if (optHwaccelType == HardwareAccelerationType.nvenc)
{
if ((!isLinux && !isWindows) || !IsCudaFullSupported())
{
@@ -1099,7 +1132,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(GetCudaDeviceArgs(0, CudaAlias))
.Append(GetFilterHwDeviceArgs(CudaAlias));
}
- else if (string.Equals(optHwaccelType, "amf", StringComparison.OrdinalIgnoreCase))
+ else if (optHwaccelType == HardwareAccelerationType.amf)
{
if (!isWindows || !_mediaEncoder.SupportsHwaccel("d3d11va"))
{
@@ -1124,7 +1157,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(filterDevArgs);
}
- else if (string.Equals(optHwaccelType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ else if (optHwaccelType == HardwareAccelerationType.videotoolbox)
{
if (!isMacOS || !_mediaEncoder.SupportsHwaccel("videotoolbox"))
{
@@ -1141,7 +1174,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// videotoolbox hw filter does not require device selection
args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias));
}
- else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ else if (optHwaccelType == HardwareAccelerationType.rkmpp)
{
if (!isLinux || !_mediaEncoder.SupportsHwaccel("rkmpp"))
{
@@ -1220,7 +1253,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sub2video for external graphical subtitles
if (state.SubtitleStream is not null
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
+ && ShouldEncodeSubtitle(state)
&& !state.SubtitleStream.IsTextSubtitleStream
&& state.SubtitleStream.IsExternal)
{
@@ -1414,6 +1447,148 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
+ private string GetEncoderParam(EncoderPreset? preset, EncoderPreset defaultPreset, EncodingOptions encodingOptions, string videoEncoder, bool isLibX265)
+ {
+ var param = string.Empty;
+ var encoderPreset = preset ?? defaultPreset;
+ if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265)
+ {
+ var presetString = encoderPreset switch
+ {
+ EncoderPreset.auto => EncoderPreset.veryfast.ToString().ToLowerInvariant(),
+ _ => encoderPreset.ToString().ToLowerInvariant()
+ };
+
+ param += " -preset " + presetString;
+
+ int encodeCrf = encodingOptions.H264Crf;
+ if (isLibX265)
+ {
+ encodeCrf = encodingOptions.H265Crf;
+ }
+
+ if (encodeCrf >= 0 && encodeCrf <= 51)
+ {
+ param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ string defaultCrf = "23";
+ if (isLibX265)
+ {
+ defaultCrf = "28";
+ }
+
+ param += " -crf " + defaultCrf;
+ }
+ }
+ else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Default to use the recommended preset 10.
+ // Omit presets < 5, which are too slow for on the fly encoding.
+ // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
+ param += encoderPreset switch
+ {
+ EncoderPreset.veryslow => " -preset 5",
+ EncoderPreset.slower => " -preset 6",
+ EncoderPreset.slow => " -preset 7",
+ EncoderPreset.medium => " -preset 8",
+ EncoderPreset.fast => " -preset 9",
+ EncoderPreset.faster => " -preset 10",
+ EncoderPreset.veryfast => " -preset 11",
+ EncoderPreset.superfast => " -preset 12",
+ EncoderPreset.ultrafast => " -preset 13",
+ _ => " -preset 10"
+ };
+ }
+ else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ // -compression_level is not reliable on AMD.
+ if (_mediaEncoder.IsVaapiDeviceInteliHD)
+ {
+ param += encoderPreset switch
+ {
+ EncoderPreset.veryslow => " -compression_level 1",
+ EncoderPreset.slower => " -compression_level 2",
+ EncoderPreset.slow => " -compression_level 3",
+ EncoderPreset.medium => " -compression_level 4",
+ EncoderPreset.fast => " -compression_level 5",
+ EncoderPreset.faster => " -compression_level 6",
+ EncoderPreset.veryfast => " -compression_level 7",
+ EncoderPreset.superfast => " -compression_level 7",
+ EncoderPreset.ultrafast => " -compression_level 7",
+ _ => string.Empty
+ };
+ }
+ }
+ else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
+ || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
+ {
+ EncoderPreset[] valid_presets = [EncoderPreset.veryslow, EncoderPreset.slower, EncoderPreset.slow, EncoderPreset.medium, EncoderPreset.fast, EncoderPreset.faster, EncoderPreset.veryfast];
+
+ param += " -preset " + (valid_presets.Contains(encoderPreset) ? encoderPreset : EncoderPreset.veryfast).ToString().ToLowerInvariant();
+ }
+ else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
+ || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) // av1 (av1_nvenc)
+ )
+ {
+ param += encoderPreset switch
+ {
+ EncoderPreset.veryslow => " -preset p7",
+ EncoderPreset.slower => " -preset p6",
+ EncoderPreset.slow => " -preset p5",
+ EncoderPreset.medium => " -preset p4",
+ EncoderPreset.fast => " -preset p3",
+ EncoderPreset.faster => " -preset p2",
+ _ => " -preset p1"
+ };
+ }
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase) // av1 (av1_amf)
+ )
+ {
+ param += encoderPreset switch
+ {
+ EncoderPreset.veryslow => " -quality quality",
+ EncoderPreset.slower => " -quality quality",
+ EncoderPreset.slow => " -quality quality",
+ EncoderPreset.medium => " -quality balanced",
+ _ => " -quality speed"
+ };
+
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -header_insertion_mode gop";
+ }
+
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -gops_per_idr 1";
+ }
+ }
+ else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox)
+ || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase) // hevc (hevc_videotoolbox)
+ )
+ {
+ param += encoderPreset switch
+ {
+ EncoderPreset.veryslow => " -prio_speed 0",
+ EncoderPreset.slower => " -prio_speed 0",
+ EncoderPreset.slow => " -prio_speed 0",
+ EncoderPreset.medium => " -prio_speed 0",
+ _ => " -prio_speed 1"
+ };
+ }
+
+ return param;
+ }
+
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
@@ -1508,13 +1683,15 @@ namespace MediaBrowser.Controller.MediaEncoding
setPtsParam);
}
- var mediaPath = state.MediaPath ?? string.Empty;
+ var subtitlePath = _subtitleEncoder.GetSubtitleFilePath(
+ state.SubtitleStream,
+ state.MediaSource,
+ CancellationToken.None).GetAwaiter().GetResult();
return string.Format(
CultureInfo.InvariantCulture,
- "subtitles=f='{0}':si={1}{2}{3}{4}{5}",
- _mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
- state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
+ "subtitles=f='{0}'{1}{2}{3}{4}",
+ _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
alphaParam,
sub2videoParam,
fontParam,
@@ -1534,7 +1711,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (maxrate.HasValue && state.VideoStream is not null)
{
- var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
+ var contentRate = state.VideoStream.ReferenceFrameRate;
if (contentRate.HasValue && contentRate.Value > maxrate.Value)
{
@@ -1626,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="encodingOptions">Encoding options.</param>
/// <param name="defaultPreset">Default present to use for encoding.</param>
/// <returns>Video bitrate.</returns>
- public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset)
+ public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, EncoderPreset defaultPreset)
{
var param = string.Empty;
@@ -1641,7 +1818,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// https://github.com/intel/media-driver/issues/1456
var enableWaFori915Hang = false;
- if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ var hardwareAccelerationType = encodingOptions.HardwareAccelerationType;
+
+ if (hardwareAccelerationType == HardwareAccelerationType.vaapi)
{
var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965;
@@ -1654,7 +1833,7 @@ namespace MediaBrowser.Controller.MediaEncoding
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder && isIntelVaapiDriver;
}
}
- else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ else if (hardwareAccelerationType == HardwareAccelerationType.qsv)
{
if (OperatingSystem.IsLinux())
{
@@ -1701,204 +1880,10 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -async_depth 1";
}
- var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
+ var encodingPreset = encodingOptions.EncoderPreset;
- if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265)
- {
- if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
- {
- param += " -preset " + encodingOptions.EncoderPreset;
- }
- else
- {
- param += " -preset " + defaultPreset;
- }
-
- int encodeCrf = encodingOptions.H264Crf;
- if (isLibX265)
- {
- encodeCrf = encodingOptions.H265Crf;
- }
-
- if (encodeCrf >= 0 && encodeCrf <= 51)
- {
- param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture);
- }
- else
- {
- string defaultCrf = "23";
- if (isLibX265)
- {
- defaultCrf = "28";
- }
-
- param += " -crf " + defaultCrf;
- }
- }
- else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
- {
- // Default to use the recommended preset 10.
- // Omit presets < 5, which are too slow for on the fly encoding.
- // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
- param += encodingOptions.EncoderPreset switch
- {
- "veryslow" => " -preset 5",
- "slower" => " -preset 6",
- "slow" => " -preset 7",
- "medium" => " -preset 8",
- "fast" => " -preset 9",
- "faster" => " -preset 10",
- "veryfast" => " -preset 11",
- "superfast" => " -preset 12",
- "ultrafast" => " -preset 13",
- _ => " -preset 10"
- };
- }
- else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
- {
- // -compression_level is not reliable on AMD.
- if (_mediaEncoder.IsVaapiDeviceInteliHD)
- {
- param += encodingOptions.EncoderPreset switch
- {
- "veryslow" => " -compression_level 1",
- "slower" => " -compression_level 2",
- "slow" => " -compression_level 3",
- "medium" => " -compression_level 4",
- "fast" => " -compression_level 5",
- "faster" => " -compression_level 6",
- "veryfast" => " -compression_level 7",
- "superfast" => " -compression_level 7",
- "ultrafast" => " -compression_level 7",
- _ => string.Empty
- };
- }
- }
- else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
- || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
- || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
- {
- string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
-
- if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
- {
- param += " -preset " + encodingOptions.EncoderPreset;
- }
- else
- {
- param += " -preset veryfast";
- }
- }
- else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
- || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc)
- {
- switch (encodingOptions.EncoderPreset)
- {
- case "veryslow":
- param += " -preset p7";
- break;
-
- case "slower":
- param += " -preset p6";
- break;
-
- case "slow":
- param += " -preset p5";
- break;
-
- case "medium":
- param += " -preset p4";
- break;
-
- case "fast":
- param += " -preset p3";
- break;
-
- case "faster":
- param += " -preset p2";
- break;
-
- case "veryfast":
- case "superfast":
- case "ultrafast":
- param += " -preset p1";
- break;
-
- default:
- param += " -preset p1";
- break;
- }
- }
- else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf)
- || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf)
- {
- switch (encodingOptions.EncoderPreset)
- {
- case "veryslow":
- case "slower":
- case "slow":
- param += " -quality quality";
- break;
-
- case "medium":
- param += " -quality balanced";
- break;
-
- case "fast":
- case "faster":
- case "veryfast":
- case "superfast":
- case "ultrafast":
- param += " -quality speed";
- break;
-
- default:
- param += " -quality speed";
- break;
- }
-
- if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
- {
- param += " -header_insertion_mode gop";
- }
-
- if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
- {
- param += " -gops_per_idr 1";
- }
- }
- else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox)
- || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_videotoolbox)
- {
- switch (encodingOptions.EncoderPreset)
- {
- case "veryslow":
- case "slower":
- case "slow":
- case "medium":
- param += " -prio_speed 0";
- break;
-
- case "fast":
- case "faster":
- case "veryfast":
- case "superfast":
- case "ultrafast":
- param += " -prio_speed 1";
- break;
-
- default:
- param += " -prio_speed 1";
- break;
- }
- }
-
+ param += GetEncoderParam(encodingPreset, defaultPreset, encodingOptions, videoEncoder, isLibX265);
param += GetVideoBitrateParam(state, videoEncoder);
var framerate = GetFramerateParam(state);
@@ -1915,7 +1900,26 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
- profile = WhiteSpaceRegex().Replace(profile, string.Empty);
+ profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant();
+
+ var videoProfiles = Array.Empty<string>();
+ if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ videoProfiles = _videoProfilesH264;
+ }
+ else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ videoProfiles = _videoProfilesH265;
+ }
+ else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ videoProfiles = _videoProfilesAv1;
+ }
+
+ if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase))
+ {
+ profile = string.Empty;
+ }
// We only transcode to HEVC 8-bit for now, force Main Profile.
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
@@ -2218,7 +2222,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedFramerate = request.MaxFramerate ?? request.Framerate;
if (requestedFramerate.HasValue)
{
- var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
+ var videoFrameRate = videoStream.ReferenceFrameRate;
if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
{
@@ -2415,7 +2419,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return 1;
}
- private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
+ public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
@@ -2439,6 +2443,12 @@ namespace MediaBrowser.Controller.MediaEncoding
{
scaleFactor = Math.Max(scaleFactor, 2);
}
+ else if (bitrate >= 30000000)
+ {
+ // Don't scale beyond 30Mbps, it is hardly visually noticeable for most codecs with our prefer speed encoding
+ // and will cause extremely high bitrate to be used for av1->h264 transcoding that will overload clients and encoders
+ scaleFactor = 1;
+ }
return Convert.ToInt32(scaleFactor * bitrate);
}
@@ -2569,7 +2579,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
- if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps)
+ if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state) && !isCopyingTimestamps)
{
var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds;
@@ -2770,7 +2780,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.AudioStream.IsExternal)
{
bool hasExternalGraphicsSubs = state.SubtitleStream is not null
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
+ && ShouldEncodeSubtitle(state)
&& state.SubtitleStream.IsExternal
&& !state.SubtitleStream.IsTextSubtitleStream;
int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
@@ -3014,6 +3024,8 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetGraphicalSubPreProcessFilters(
int? videoWidth,
int? videoHeight,
+ int? subtitleWidth,
+ int? subtitleHeight,
int? requestedWidth,
int? requestedHeight,
int? requestedMaxWidth,
@@ -3027,16 +3039,37 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxWidth,
requestedMaxHeight);
- if (outWidth.HasValue && outHeight.HasValue)
+ if (!outWidth.HasValue
+ || !outHeight.HasValue
+ || outWidth.Value <= 0
+ || outHeight.Value <= 0)
{
- return string.Format(
- CultureInfo.InvariantCulture,
- @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
- outWidth.Value,
- outHeight.Value);
+ return string.Empty;
}
- return string.Empty;
+ // Automatically add padding based on subtitle input
+ var filters = @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}";
+
+ if (subtitleWidth.HasValue
+ && subtitleHeight.HasValue
+ && subtitleWidth.Value > 0
+ && subtitleHeight.Value > 0)
+ {
+ var videoDar = (double)outWidth.Value / outHeight.Value;
+ var subtitleDar = (double)subtitleWidth.Value / subtitleHeight.Value;
+
+ // No need to add padding when DAR is the same -> 1080p PGSSUB on 2160p video
+ if (Math.Abs(videoDar - subtitleDar) < 0.01f)
+ {
+ filters = @"scale,scale={0}:{1}:fast_bilinear";
+ }
+ }
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ filters,
+ outWidth.Value,
+ outHeight.Value);
}
public static string GetAlphaSrcFilter(
@@ -3234,21 +3267,20 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetSwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options)
{
- var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.AverageFrameRate <= 30;
+ var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.ReferenceFrameRate <= 30;
return string.Format(
CultureInfo.InvariantCulture,
"{0}={1}:-1:0",
- string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) ? "bwdif" : "yadif",
+ options.DeinterlaceMethod.ToString().ToLowerInvariant(),
doubleRateDeint ? "1" : "0");
}
public string GetHwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options, string hwDeintSuffix)
{
- var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30;
+ var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30;
if (hwDeintSuffix.Contains("cuda", StringComparison.OrdinalIgnoreCase))
{
- var useBwdif = string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)
- && _mediaEncoder.SupportsFilter("bwdif_cuda");
+ var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_cuda");
return string.Format(
CultureInfo.InvariantCulture,
@@ -3272,16 +3304,19 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
{
+ var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_videotoolbox");
+
return string.Format(
CultureInfo.InvariantCulture,
- "yadif_videotoolbox={0}:-1:0",
+ "{0}_videotoolbox={1}:-1:0",
+ useBwdif ? "bwdif" : "yadif",
doubleRateDeint ? "1" : "0");
}
return string.Empty;
}
- public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
+ private string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat, bool forceFullRange)
{
if (string.IsNullOrEmpty(hwTonemapSuffix))
{
@@ -3289,7 +3324,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var args = string.Empty;
- var algorithm = options.TonemappingAlgorithm;
+ var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant();
+ var mode = options.TonemappingMode.ToString().ToLowerInvariant();
+ var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange;
+ var rangeString = range.ToString().ToLowerInvariant();
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{
@@ -3324,10 +3362,10 @@ namespace MediaBrowser.Controller.MediaEncoding
args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
var useLegacyTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode
- && _legacyTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase);
+ && _legacyTonemapModes.Contains(options.TonemappingMode);
var useAdvancedTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegAdvancedTonemapMode
- && _advancedTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase);
+ && _advancedTonemapModes.Contains(options.TonemappingMode);
if (useLegacyTonemapModes || useAdvancedTonemapModes)
{
@@ -3339,8 +3377,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += ":param={6}";
}
- if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
+ if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
args += ":range={7}";
}
@@ -3354,12 +3391,12 @@ namespace MediaBrowser.Controller.MediaEncoding
algorithm,
options.TonemappingPeak,
options.TonemappingDesat,
- options.TonemappingMode,
+ mode,
options.TonemappingParam,
- options.TonemappingRange);
+ rangeString);
}
- public string GetLibplaceboFilter(
+ private string GetLibplaceboFilter(
EncodingOptions options,
string videoFormat,
bool doTonemap,
@@ -3368,7 +3405,8 @@ namespace MediaBrowser.Controller.MediaEncoding
int? requestedWidth,
int? requestedHeight,
int? requestedMaxWidth,
- int? requestedMaxHeight)
+ int? requestedMaxHeight,
+ bool forceFullRange)
{
var (outWidth, outHeight) = GetFixedOutputSize(
videoWidth,
@@ -3391,24 +3429,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if (doTonemap)
{
var algorithm = options.TonemappingAlgorithm;
+ var algorithmString = "clip";
var mode = options.TonemappingMode;
- var range = options.TonemappingRange;
+ var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange;
- if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
+ if (algorithm == TonemappingAlgorithm.bt2390)
{
- algorithm = "bt.2390";
+ algorithmString = "bt.2390";
}
- else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase))
+ else if (algorithm != TonemappingAlgorithm.none)
{
- algorithm = "clip";
+ algorithmString = algorithm.ToString().ToLowerInvariant();
}
- tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+ tonemapArg = $":tonemapping={algorithmString}:peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
- if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
+ if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
- tonemapArg += ":range=" + range;
+ tonemapArg += ":range=" + range.ToString().ToLowerInvariant();
}
}
@@ -3463,7 +3501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doToneMap = IsSwTonemapAvailable(state, options);
var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI;
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
@@ -3512,8 +3550,8 @@ namespace MediaBrowser.Controller.MediaEncoding
tonemapArgs += $":param={options.TonemappingParam}";
}
- if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
+ var range = options.TonemappingRange;
+ if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
tonemapArgs += $":range={options.TonemappingRange}";
}
@@ -3537,7 +3575,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3557,7 +3597,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.nvenc)
{
return (null, null, null);
}
@@ -3596,20 +3636,23 @@ namespace MediaBrowser.Controller.MediaEncoding
var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isNvencEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isCuInCuOut = isNvDecoder && isNvencEncoder;
- var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30;
+ var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doCuTonemap = IsHwTonemapAvailable(state, options);
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -3662,7 +3705,8 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add($"transpose_cuda=dir={tranposeDir}");
}
- var outFormat = doCuTonemap ? string.Empty : "yuv420p";
+ var isRext = IsVideoStreamHevcRext(state);
+ var outFormat = doCuTonemap ? (isRext ? "p010" : string.Empty) : "yuv420p";
var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
// hw scale
mainFilters.Add(hwScaleFilter);
@@ -3671,7 +3715,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw tonemap
if (doCuTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p");
+ var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -3713,7 +3757,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
@@ -3738,7 +3782,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3759,7 +3803,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.amf)
{
return (null, null, null);
}
@@ -3800,6 +3844,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isAmfEncoder = vidEncoder.Contains("amf", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isAmfEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
@@ -3807,12 +3852,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doOclTonemap = IsHwTonemapAvailable(state, options);
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -3877,7 +3924,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -3927,7 +3974,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
@@ -3954,7 +4001,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3975,7 +4022,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.qsv)
{
return (null, null, null);
}
@@ -4036,6 +4083,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isHwDecoder = isD3d11vaDecoder || isQsvDecoder;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isQsvEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isQsvInQsvOut = isHwDecoder && isQsvEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
@@ -4045,12 +4093,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVppTonemap || doOclTonemap;
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -4076,6 +4126,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (isMjpegEncoder && !doOclTonemap)
+ {
+ // sw decoder + hw mjpeg encoder
+ swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc";
+ }
+
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add($"format={outFormat}");
@@ -4090,6 +4146,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isD3d11vaDecoder || isQsvDecoder)
{
+ var isRext = IsVideoStreamHevcRext(state);
+ var twoPassVppTonemap = isRext;
+ var doVppFullRangeOut = isMjpegEncoder
+ && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
+ var doVppScaleModeHq = isMjpegEncoder
+ && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
var doVppProcamp = false;
var procampParams = string.Empty;
if (doVppTonemap)
@@ -4099,34 +4161,39 @@ namespace MediaBrowser.Controller.MediaEncoding
&& options.VppTonemappingBrightness <= 100)
{
procampParams += $":brightness={options.VppTonemappingBrightness}";
- doVppProcamp = true;
+ twoPassVppTonemap = doVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
procampParams += $":contrast={options.VppTonemappingContrast}";
- doVppProcamp = true;
+ twoPassVppTonemap = doVppProcamp = true;
}
procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
}
- var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12";
- outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat;
+ var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
+ outFormat = twoPassVppTonemap ? "p010" : outFormat;
var swapOutputWandH = doVppTranspose && swapWAndH;
- var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale";
- var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
{
hwScaleFilter += $":transpose={tranposeDir}";
}
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
+ {
+ hwScaleFilter += (doVppFullRangeOut && !doOclTonemap) ? ":out_range=pc" : string.Empty;
+ hwScaleFilter += doVppScaleModeHq ? ":scale_mode=hq" : string.Empty;
+ }
+
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap)
{
- hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1";
+ hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1");
}
if (isD3d11vaDecoder)
@@ -4150,7 +4217,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(hwScaleFilter);
// hw tonemap(w/ procamp)
- if (doVppTonemap && doVppProcamp)
+ if (doVppTonemap && twoPassVppTonemap)
{
mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2");
}
@@ -4171,7 +4238,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -4223,7 +4290,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4259,7 +4326,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -4288,6 +4355,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isHwDecoder = isVaapiDecoder || isQsvDecoder;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isQsvEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isQsvInQsvOut = isHwDecoder && isQsvEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
@@ -4297,12 +4365,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var doTonemap = doVaVppTonemap || doOclTonemap;
var doDeintH2645 = doDeintH264 || doDeintHevc;
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -4328,6 +4398,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (isMjpegEncoder && !doOclTonemap)
+ {
+ // sw decoder + hw mjpeg encoder
+ swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc";
+ }
+
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add($"format={outFormat}");
@@ -4343,6 +4419,11 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isVaapiDecoder || isQsvDecoder)
{
var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv";
+ var isRext = IsVideoStreamHevcRext(state);
+ var doVppFullRangeOut = isMjpegEncoder
+ && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
+ var doVppScaleModeHq = isMjpegEncoder
+ && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
// INPUT vaapi/qsv surface(vram)
// hw deint
@@ -4358,9 +4439,9 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
}
- var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12";
+ var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12";
var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH;
- var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale";
+ var hwScalePrefix = isQsvDecoder ? "vpp" : "scale";
var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose)
@@ -4368,6 +4449,12 @@ namespace MediaBrowser.Controller.MediaEncoding
hwScaleFilter += $":transpose={tranposeDir}";
}
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
+ {
+ hwScaleFilter += ((isQsvDecoder && !doVppFullRangeOut) || doOclTonemap) ? string.Empty : ":out_range=pc";
+ hwScaleFilter += isQsvDecoder ? (doVppScaleModeHq ? ":scale_mode=hq" : string.Empty) : ":mode=hq";
+ }
+
// allocate extra pool sizes for vaapi vpp scale
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
{
@@ -4388,7 +4475,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add("format=vaapi");
}
- var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
if (isQsvDecoder)
@@ -4408,7 +4495,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// ocl tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -4469,7 +4556,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4504,7 +4591,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -4525,7 +4612,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.vaapi)
{
return (null, null, null);
}
@@ -4599,6 +4686,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isVaapiEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
@@ -4608,12 +4696,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var doTonemap = doVaVppTonemap || doOclTonemap;
var doDeintH2645 = doDeintH264 || doDeintHevc;
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -4639,6 +4729,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doOclTonemap ? "yuv420p10le" : "nv12";
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (isMjpegEncoder && !doOclTonemap)
+ {
+ // sw decoder + hw mjpeg encoder
+ swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc";
+ }
+
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add($"format={outFormat}");
@@ -4653,6 +4749,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isVaapiDecoder)
{
+ var isRext = IsVideoStreamHevcRext(state);
+
// INPUT vaapi surface(vram)
// hw deint
if (doDeintH2645)
@@ -4667,9 +4765,15 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
}
- var outFormat = doTonemap ? string.Empty : "nv12";
+ var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12";
var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
+ {
+ hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc";
+ hwScaleFilter += ":mode=hq";
+ }
+
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
{
@@ -4683,7 +4787,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// vaapi vpp tonemap
if (doVaVppTonemap && isVaapiDecoder)
{
- var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -4696,7 +4800,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// ocl tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -4755,7 +4859,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (hasGraphicalSubs)
{
// overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -4788,7 +4892,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
@@ -4820,13 +4924,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isVaapiEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doVkTonemap = IsVulkanHwTonemapAvailable(state, options);
var doDeintH2645 = doDeintH264 || doDeintHevc;
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
@@ -4915,6 +5020,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw scale
var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder && !doVkTonemap)
+ {
+ hwScaleFilter += ":out_range=pc:mode=hq";
+ }
+
mainFilters.Add(hwScaleFilter);
}
}
@@ -4935,7 +5046,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// vk libplacebo
if (doVkTonemap || hasSubs)
{
- var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, isMjpegEncoder);
mainFilters.Add(libplaceboFilter);
mainFilters.Add("format=vulkan");
}
@@ -4980,7 +5091,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5048,6 +5161,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isVaapiEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965;
var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd;
@@ -5057,7 +5171,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doOclTonemap = IsHwTonemapAvailable(state, options);
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
@@ -5084,6 +5198,12 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? "yuv420p10le" : "nv12";
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (isMjpegEncoder && !doOclTonemap)
+ {
+ // sw decoder + hw mjpeg encoder
+ swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc";
+ }
+
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add("format=" + outFormat);
@@ -5109,6 +5229,12 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
+ {
+ hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc";
+ hwScaleFilter += ":mode=hq";
+ }
+
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
{
@@ -5137,7 +5263,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// ocl tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -5203,7 +5329,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
@@ -5229,7 +5357,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)
{
return (null, null, null);
}
@@ -5261,6 +5389,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
@@ -5303,7 +5432,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
@@ -5342,7 +5471,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Metal tonemap
if (doMetalTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12");
+ var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -5354,7 +5483,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5418,7 +5549,7 @@ namespace MediaBrowser.Controller.MediaEncoding
EncodingOptions options,
string vidEncoder)
{
- if (!string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ if (options.HardwareAccelerationType != HardwareAccelerationType.rkmpp)
{
return (null, null, null);
}
@@ -5463,19 +5594,25 @@ namespace MediaBrowser.Controller.MediaEncoding
var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = !isRkmppDecoder;
var isSwEncoder = !isRkmppEncoder;
+ var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder;
+ var isEncoderSupportAfbc = isRkmppEncoder
+ && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase)
+ || vidEncoder.Contains("hevc", StringComparison.OrdinalIgnoreCase));
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doOclTonemap = IsHwTonemapAvailable(state, options);
- var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+ var subW = state.SubtitleStream?.Width;
+ var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -5501,6 +5638,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (isMjpegEncoder && !doOclTonemap)
+ {
+ // sw decoder + hw mjpeg encoder
+ swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc";
+ }
+
if (!string.IsNullOrEmpty(swScaleFilter))
{
swScaleFilter += ":flags=fast_bilinear";
@@ -5522,18 +5665,26 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// INPUT rkmpp/drm surface(gem/dma-heap)
- var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap;
+ var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
var swapOutputWandH = doRkVppTranspose && swapWAndH;
- var outFormat = doOclTonemap ? "p010" : "nv12";
- var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale";
- var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
- var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts
+ var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
if (!hasSubs
|| doRkVppTranspose
|| !isFullAfbcPipeline
- || !string.IsNullOrEmpty(hwScaleFilter2))
+ || !string.IsNullOrEmpty(doScaling))
{
+ // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
+ // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
+ if (!string.IsNullOrEmpty(doScaling)
+ && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
+ {
+ var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
+ mainFilters.Add(hwScaleFilterFirstPass);
+ }
+
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
{
hwScaleFilter += $":transpose={tranposeDir}";
@@ -5553,19 +5704,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (doOclTonemap && isRkmppDecoder)
{
// map from rkmpp/drm to opencl via drm-opencl interop.
- mainFilters.Add("hwmap=derive_device=opencl:mode=read");
+ mainFilters.Add("hwmap=derive_device=opencl");
}
// ocl tonemap
if (doOclTonemap)
{
- var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
- // enable tradeoffs for performance
- if (!string.IsNullOrEmpty(tonemapFilter))
- {
- tonemapFilter += ":tradeoff=1";
- }
-
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
}
@@ -5602,7 +5747,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// OUTPUT drm(nv12) surface(gem/dma-heap)
// reverse-mapping via drm-opencl interop.
- mainFilters.Add("hwmap=derive_device=rkmpp:mode=write:reverse=1");
+ mainFilters.Add("hwmap=derive_device=rkmpp:reverse=1");
mainFilters.Add("format=drm_prime");
}
}
@@ -5616,7 +5761,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
@@ -5636,14 +5781,20 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=rkmpp");
// try enabling AFBC to save DDR bandwidth
- overlayFilters.Add("overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12:afbc=1");
+ var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12";
+ if (isEncoderSupportAfbc)
+ {
+ hwOverlayFilter += ":afbc=1";
+ }
+
+ overlayFilters.Add(hwOverlayFilter);
}
}
else if (memoryOutput)
{
if (hasGraphicalSubs)
{
- var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -5670,7 +5821,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
@@ -5678,38 +5829,20 @@ namespace MediaBrowser.Controller.MediaEncoding
List<string> subFilters;
List<string> overlayFilters;
- if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
- {
- (mainFilters, subFilters, overlayFilters) = GetVaapiVidFilterChain(state, options, outputVideoCodec);
- }
- else if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
- {
- (mainFilters, subFilters, overlayFilters) = GetIntelVidFilterChain(state, options, outputVideoCodec);
- }
- else if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
- {
- (mainFilters, subFilters, overlayFilters) = GetNvidiaVidFilterChain(state, options, outputVideoCodec);
- }
- else if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
- {
- (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec);
- }
- else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ (mainFilters, subFilters, overlayFilters) = options.HardwareAccelerationType switch
{
- (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec);
- }
- else if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
- {
- (mainFilters, subFilters, overlayFilters) = GetRkmppVidFilterChain(state, options, outputVideoCodec);
- }
- else
- {
- (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec);
- }
+ HardwareAccelerationType.vaapi => GetVaapiVidFilterChain(state, options, outputVideoCodec),
+ HardwareAccelerationType.amf => GetAmdVidFilterChain(state, options, outputVideoCodec),
+ HardwareAccelerationType.qsv => GetIntelVidFilterChain(state, options, outputVideoCodec),
+ HardwareAccelerationType.nvenc => GetNvidiaVidFilterChain(state, options, outputVideoCodec),
+ HardwareAccelerationType.videotoolbox => GetAppleVidFilterChain(state, options, outputVideoCodec),
+ HardwareAccelerationType.rkmpp => GetRkmppVidFilterChain(state, options, outputVideoCodec),
+ _ => GetSwVidFilterChain(state, options, outputVideoCodec),
+ };
- mainFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
- subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
- overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
+ mainFilters?.RemoveAll(string.IsNullOrEmpty);
+ subFilters?.RemoveAll(string.IsNullOrEmpty);
+ overlayFilters?.RemoveAll(string.IsNullOrEmpty);
var framerate = GetFramerateParam(state);
if (framerate.HasValue)
@@ -5889,7 +6022,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(options.HardwareAccelerationType))
+ var hardwareAccelerationType = options.HardwareAccelerationType;
+
+ if (!string.IsNullOrEmpty(videoStream.Codec) && hardwareAccelerationType != HardwareAccelerationType.none)
{
var bitDepth = GetVideoColorBitDepth(state);
@@ -5901,10 +6036,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)))
{
// RKMPP has H.264 Hi10P decoder
- bool hasHardwareHi10P = string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase);
+ bool hasHardwareHi10P = hardwareAccelerationType == HardwareAccelerationType.rkmpp;
// VideoToolbox on Apple Silicon has H.264 Hi10P mode enabled after macOS 14.6
- if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox)
{
var ver = Environment.OSVersion.Version;
var arch = RuntimeInformation.OSArchitecture;
@@ -5921,50 +6056,23 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
- {
- return GetQsvHwVidDecoder(state, options, videoStream, bitDepth);
- }
-
- if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
- {
- return GetNvdecVidDecoder(state, options, videoStream, bitDepth);
- }
-
- if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
- {
- return GetAmfVidDecoder(state, options, videoStream, bitDepth);
- }
-
- if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
- {
- return GetVaapiVidDecoder(state, options, videoStream, bitDepth);
- }
-
- if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ var decoder = hardwareAccelerationType switch
{
- return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth);
- }
+ HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
+ HardwareAccelerationType.amf => GetAmfVidDecoder(state, options, videoStream, bitDepth),
+ HardwareAccelerationType.qsv => GetQsvHwVidDecoder(state, options, videoStream, bitDepth),
+ HardwareAccelerationType.nvenc => GetNvdecVidDecoder(state, options, videoStream, bitDepth),
+ HardwareAccelerationType.videotoolbox => GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth),
+ HardwareAccelerationType.rkmpp => GetRkmppVidDecoder(state, options, videoStream, bitDepth),
+ _ => string.Empty
+ };
- if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(decoder))
{
- return GetRkmppVidDecoder(state, options, videoStream, bitDepth);
+ return decoder;
}
}
- var whichCodec = videoStream.Codec;
- if (string.Equals(whichCodec, "avc", StringComparison.OrdinalIgnoreCase))
- {
- whichCodec = "h264";
- }
- else if (string.Equals(whichCodec, "h265", StringComparison.OrdinalIgnoreCase))
- {
- whichCodec = "hevc";
- }
-
- // Avoid a second attempt if no hardware acceleration is being used
- options.HardwareDecodingCodecs = Array.FindAll(options.HardwareDecodingCodecs, val => !string.Equals(val, whichCodec, StringComparison.OrdinalIgnoreCase));
-
// leave blank so ffmpeg will decide
return null;
}
@@ -5988,7 +6096,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var decoderName = decoderPrefix + '_' + decoderSuffix;
var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
- if (bitDepth == 10 && isCodecAvailable)
+
+ // VideoToolbox decoders have built-in SW fallback
+ if (bitDepth == 10
+ && isCodecAvailable
+ && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox))
{
if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
&& options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)
@@ -6044,6 +6156,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
var isRkmppSupported = isLinux && IsRkmppFullSupported();
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
+ var hardwareAccelerationType = options.HardwareAccelerationType;
var ffmpegVersion = _mediaEncoder.EncoderVersion;
@@ -6063,17 +6176,40 @@ namespace MediaBrowser.Controller.MediaEncoding
&& ffmpegVersion >= _minFFmpegDisplayRotationOption;
var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty;
- if (bitDepth == 10 && isCodecAvailable)
+ // VideoToolbox decoders have built-in SW fallback
+ if (isCodecAvailable
+ && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox))
{
if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)
- && !options.EnableDecodingColorDepth10Hevc)
+ && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase))
{
- return null;
+ if (IsVideoStreamHevcRext(state))
+ {
+ if (bitDepth <= 10 && !options.EnableDecodingColorDepth10HevcRext)
+ {
+ return null;
+ }
+
+ if (bitDepth == 12 && !options.EnableDecodingColorDepth12HevcRext)
+ {
+ return null;
+ }
+
+ if (hardwareAccelerationType == HardwareAccelerationType.vaapi
+ && !_mediaEncoder.IsVaapiDeviceInteliHD)
+ {
+ return null;
+ }
+ }
+ else if (bitDepth == 10 && !options.EnableDecodingColorDepth10Hevc)
+ {
+ return null;
+ }
}
if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
&& options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase)
+ && bitDepth == 10
&& !options.EnableDecodingColorDepth10Vp9)
{
return null;
@@ -6081,7 +6217,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Intel qsv/d3d11va/vaapi
- if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ if (hardwareAccelerationType == HardwareAccelerationType.qsv)
{
if (options.PreferSystemNativeHwDecoder)
{
@@ -6107,7 +6243,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Nvidia cuda
- if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ if (hardwareAccelerationType == HardwareAccelerationType.nvenc)
{
if (isCudaSupported && isCodecAvailable)
{
@@ -6124,7 +6260,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Amd d3d11va
- if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
+ if (hardwareAccelerationType == HardwareAccelerationType.amf)
{
if (isD3d11Supported && isCodecAvailable)
{
@@ -6134,7 +6270,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Vaapi
- if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
+ if (hardwareAccelerationType == HardwareAccelerationType.vaapi
&& isVaapiSupported
&& isCodecAvailable)
{
@@ -6143,7 +6279,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Apple videotoolbox
- if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
+ if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& isVideotoolboxSupported
&& isCodecAvailable)
{
@@ -6151,7 +6287,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Rockchip rkmpp
- if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
+ if (hardwareAccelerationType == HardwareAccelerationType.rkmpp
&& isRkmppSupported
&& isCodecAvailable)
{
@@ -6167,7 +6303,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isLinux = OperatingSystem.IsLinux();
if ((!isWindows && !isLinux)
- || !string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.qsv)
{
return null;
}
@@ -6185,6 +6321,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is8_10_12bitSwFormatsQsv = is8_10bitSwFormatsQsv
+ || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool
if (is8bitSwFormatsQsv)
@@ -6213,12 +6357,6 @@ namespace MediaBrowser.Controller.MediaEncoding
if (is8_10bitSwFormatsQsv)
{
- if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth);
- }
-
if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "qsv", "vp9", bitDepth);
@@ -6230,13 +6368,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ if (is8_10_12bitSwFormatsQsv)
+ {
+ if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth);
+ }
+ }
+
return null;
}
public string GetNvdecVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
{
if ((!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux())
- || !string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.nvenc)
{
return null;
}
@@ -6245,6 +6392,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is8_10_12bitSwFormatsNvdec = is8_10bitSwFormatsNvdec
+ || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool
if (is8bitSwFormatsNvdec)
@@ -6278,12 +6430,6 @@ namespace MediaBrowser.Controller.MediaEncoding
if (is8_10bitSwFormatsNvdec)
{
- if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth);
- }
-
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "cuvid", "vp9", bitDepth);
@@ -6295,13 +6441,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ if (is8_10_12bitSwFormatsNvdec)
+ {
+ if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth);
+ }
+ }
+
return null;
}
public string GetAmfVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
{
if (!OperatingSystem.IsWindows()
- || !string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.amf)
{
return null;
}
@@ -6357,7 +6512,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetVaapiVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
{
if (!OperatingSystem.IsLinux()
- || !string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.vaapi)
{
return null;
}
@@ -6369,6 +6524,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is8_10_12bitSwFormatsVaapi = is8_10bitSwFormatsVaapi
+ || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
if (is8bitSwFormatsVaapi)
{
@@ -6396,12 +6559,6 @@ namespace MediaBrowser.Controller.MediaEncoding
if (is8_10bitSwFormatsVaapi)
{
- if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface);
- }
-
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface);
@@ -6413,13 +6570,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ if (is8_10_12bitSwFormatsVaapi)
+ {
+ if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface);
+ }
+ }
+
return null;
}
public string GetVideotoolboxVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
{
if (!OperatingSystem.IsMacOS()
- || !string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)
{
return null;
}
@@ -6427,6 +6593,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is8_10_12bitSwFormatsVt = is8_10bitSwFormatsVt
+ || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
@@ -6447,15 +6621,18 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
}
- if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
- || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
+ return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
}
+ }
- if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ if (is8_10_12bitSwFormatsVt)
+ {
+ if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
+ return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
}
@@ -6467,7 +6644,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isLinux = OperatingSystem.IsLinux();
if (!isLinux
- || !string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ || options.HardwareAccelerationType != HardwareAccelerationType.rkmpp)
{
return null;
}
@@ -6680,7 +6857,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(state.InputVideoSync))
{
- inputModifier += " -vsync " + state.InputVideoSync;
+ inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
}
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
@@ -6731,7 +6908,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.IsVideoRequest)
{
- if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && encodingOptions.HardwareAccelerationType != HardwareAccelerationType.none)
{
var inputFormat = GetInputFormat(state.InputContainer);
if (!string.IsNullOrEmpty(inputFormat))
@@ -6847,7 +7024,7 @@ namespace MediaBrowser.Controller.MediaEncoding
state.SupportedAudioCodecs = supportedAudioCodecsList.ToArray();
- request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
+ request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(_mediaEncoder.CanEncodeToAudioCodec)
?? state.SupportedAudioCodecs.FirstOrDefault();
}
@@ -6973,7 +7150,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return " -codec:s:0 " + codec + " -disposition:s:0 default";
}
- public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string defaultPreset)
+ public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, EncoderPreset defaultPreset)
{
// Get the output codec name
var videoCodec = GetVideoEncoder(state, encodingOptions);
@@ -7024,7 +7201,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset)
+ public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, EncoderPreset defaultPreset)
{
var args = "-codec:v:0 " + videoCodec;
@@ -7065,7 +7242,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += keyFrameArg;
- var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state);
var hasCopyTs = false;
@@ -7103,7 +7280,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(state.OutputVideoSync))
{
- args += " -vsync " + state.OutputVideoSync;
+ args += GetVideoSyncOption(state.OutputVideoSync, _mediaEncoder.EncoderVersion);
}
args += GetOutputFFlags(state);
@@ -7270,5 +7447,39 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
}
+
+ private static bool ShouldEncodeSubtitle(EncodingJobInfo state)
+ {
+ return state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
+ || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
+ }
+
+ public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
+ {
+ if (string.IsNullOrEmpty(videoSync))
+ {
+ return string.Empty;
+ }
+
+ if (encoderVersion >= new Version(5, 1))
+ {
+ if (int.TryParse(videoSync, CultureInfo.InvariantCulture, out var vsync))
+ {
+ return vsync switch
+ {
+ -1 => " -fps_mode auto",
+ 0 => " -fps_mode passthrough",
+ 1 => " -fps_mode cfr",
+ 2 => " -fps_mode vfr",
+ _ => string.Empty
+ };
+ }
+
+ return string.Empty;
+ }
+
+ // -vsync is deprecated in FFmpeg 5.1 and will be removed in the future.
+ return $" -vsync {videoSync}";
+ }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 72df7151d..caa312987 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -305,7 +305,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (BaseRequest.Static
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
- return VideoStream is null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate);
+ return VideoStream?.ReferenceFrameRate;
}
return BaseRequest.MaxFramerate ?? BaseRequest.Framerate;
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
index 5bf83a9e3..9bf27b3b2 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -44,5 +44,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>System.String.</returns>
Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the path to a subtitle file.
+ /// </summary>
+ /// <param name="subtitleStream">The subtitle stream.</param>
+ /// <param name="mediaSource">The media source.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>System.String.</returns>
+ Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
index 67384f6f6..010d7edb4 100644
--- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -14,6 +15,15 @@ namespace MediaBrowser.Controller;
public interface IMediaSegmentManager
{
/// <summary>
+ /// Uses all segment providers enabled for the <see cref="BaseItem"/>'s library to get the Media Segments.
+ /// </summary>
+ /// <param name="baseItem">The Item to evaluate.</param>
+ /// <param name="overwrite">If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.</param>
+ /// <param name="cancellationToken">stop request token.</param>
+ /// <returns>A task that indicates the Operation is finished.</returns>
+ Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken);
+
+ /// <summary>
/// Returns if this item supports media segments.
/// </summary>
/// <param name="baseItem">The base Item to check.</param>
@@ -50,4 +60,11 @@ public interface IMediaSegmentManager
/// <returns>True if there are any segments stored for the item, otherwise false.</returns>
/// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson.
bool HasSegments(Guid itemId);
+
+ /// <summary>
+ /// Gets a list of all registered Segment Providers and their IDs.
+ /// </summary>
+ /// <param name="item">The media item that should be tested for providers.</param>
+ /// <returns>A list of all providers for the tested item.</returns>
+ IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item);
}
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs
new file mode 100644
index 000000000..39bb58bef
--- /dev/null
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model;
+using MediaBrowser.Model.MediaSegments;
+
+namespace MediaBrowser.Controller;
+
+/// <summary>
+/// Provides methods for Obtaining the Media Segments from an Item.
+/// </summary>
+public interface IMediaSegmentProvider
+{
+ /// <summary>
+ /// Gets the provider name.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Enumerates all Media Segments from an Media Item.
+ /// </summary>
+ /// <param name="request">Arguments to enumerate MediaSegments.</param>
+ /// <param name="cancellationToken">Abort token.</param>
+ /// <returns>A list of all MediaSegments found from this provider.</returns>
+ Task<IReadOnlyList<MediaSegmentDto>> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Should return support state for the given item.
+ /// </summary>
+ /// <param name="item">The base item to extract segments from.</param>
+ /// <returns>True if item is supported, otherwise false.</returns>
+ ValueTask<bool> Supports(BaseItem item);
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
index 3504831b8..833074541 100644
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
@@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sessions message.
/// </summary>
-public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>>
+public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfoDto>>
{
/// <summary>
/// Initializes a new instance of the <see cref="SessionsMessage"/> class.
/// </summary>
/// <param name="data">Session info.</param>
- public SessionsMessage(IReadOnlyList<SessionInfo> data)
+ public SessionsMessage(IReadOnlyList<SessionInfoDto> data)
: base(data)
{
}
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 9e91a8bcd..0bab2a6b9 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.Providers
IsAutomated = copy.IsAutomated;
ImageRefreshMode = copy.ImageRefreshMode;
ReplaceAllImages = copy.ReplaceAllImages;
+ RegenerateTrickplay = copy.RegenerateTrickplay;
ReplaceImages = copy.ReplaceImages;
SearchResult = copy.SearchResult;
RemoveOldMetadata = copy.RemoveOldMetadata;
@@ -47,6 +48,12 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public bool ReplaceAllMetadata { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether all existing trickplay images should be overwritten
+ /// when paired with MetadataRefreshMode=FullRefresh.
+ /// </summary>
+ public bool RegenerateTrickplay { get; set; }
+
public MetadataRefreshMode MetadataRefreshMode { get; set; }
public RemoteSearchResult SearchResult { get; set; }
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 5a47236f9..462a62455 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@@ -293,6 +294,17 @@ namespace MediaBrowser.Controller.Session
SessionInfo GetSession(string deviceId, string client, string version);
/// <summary>
+ /// Gets all sessions available to a user.
+ /// </summary>
+ /// <param name="userId">The session identifier.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="activeWithinSeconds">Active within session limit.</param>
+ /// <param name="controllableUserToCheck">Filter for sessions remote controllable for this user.</param>
+ /// <param name="isApiKey">Is the request authenticated with API key.</param>
+ /// <returns>IReadOnlyList{SessionInfoDto}.</returns>
+ IReadOnlyList<SessionInfoDto> GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck, bool isApiKey);
+
+ /// <summary>
/// Gets the session by authentication token.
/// </summary>
/// <param name="token">The token.</param>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 9e3358818..3ba1bfce4 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
- private readonly object _progressLock = new object();
+ private readonly object _progressLock = new();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
- private bool _disposed = false;
+ private bool _disposed;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionInfo"/> class.
+ /// </summary>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger"/> interface.</param>
public SessionInfo(ISessionManager sessionManager, ILogger logger)
{
_sessionManager = sessionManager;
_logger = logger;
- AdditionalUsers = Array.Empty<SessionUserInfo>();
+ AdditionalUsers = [];
PlayState = new PlayerStateInfo();
- SessionControllers = Array.Empty<ISessionController>();
- NowPlayingQueue = Array.Empty<QueueItem>();
- NowPlayingQueueFullItems = Array.Empty<BaseItemDto>();
+ SessionControllers = [];
+ NowPlayingQueue = [];
+ NowPlayingQueueFullItems = [];
}
+ /// <summary>
+ /// Gets or sets the play state.
+ /// </summary>
+ /// <value>The play state.</value>
public PlayerStateInfo PlayState { get; set; }
- public SessionUserInfo[] AdditionalUsers { get; set; }
+ /// <summary>
+ /// Gets or sets the additional users.
+ /// </summary>
+ /// <value>The additional users.</value>
+ public IReadOnlyList<SessionUserInfo> AdditionalUsers { get; set; }
+ /// <summary>
+ /// Gets or sets the client capabilities.
+ /// </summary>
+ /// <value>The client capabilities.</value>
public ClientCapabilities Capabilities { get; set; }
/// <summary>
@@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session
{
if (Capabilities is null)
{
- return Array.Empty<MediaType>();
+ return [];
}
return Capabilities.PlayableMediaTypes;
@@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session
/// <value>The now playing item.</value>
public BaseItemDto NowPlayingItem { get; set; }
+ /// <summary>
+ /// Gets or sets the now playing queue full items.
+ /// </summary>
+ /// <value>The now playing queue full items.</value>
[JsonIgnore]
public BaseItem FullNowPlayingItem { get; set; }
+ /// <summary>
+ /// Gets or sets the now viewing item.
+ /// </summary>
+ /// <value>The now viewing item.</value>
public BaseItemDto NowViewingItem { get; set; }
/// <summary>
@@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <value>The session controller.</value>
[JsonIgnore]
- public ISessionController[] SessionControllers { get; set; }
+ public IReadOnlyList<ISessionController> SessionControllers { get; set; }
+ /// <summary>
+ /// Gets or sets the transcoding info.
+ /// </summary>
+ /// <value>The transcoding info.</value>
public TranscodingInfo TranscodingInfo { get; set; }
/// <summary>
@@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session
}
}
- if (controllers.Length > 0)
+ if (controllers.Count > 0)
{
return false;
}
@@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session
}
}
+ /// <summary>
+ /// Gets a value indicating whether the session supports media control.
+ /// </summary>
+ /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value>
public bool SupportsMediaControl
{
get
@@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session
}
}
+ /// <summary>
+ /// Gets a value indicating whether the session supports remote control.
+ /// </summary>
+ /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value>
public bool SupportsRemoteControl
{
get
@@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session
}
}
+ /// <summary>
+ /// Gets or sets the now playing queue.
+ /// </summary>
+ /// <value>The now playing queue.</value>
public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; }
+ /// <summary>
+ /// Gets or sets the now playing queue full items.
+ /// </summary>
+ /// <value>The now playing queue full items.</value>
public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the session has a custom device name.
+ /// </summary>
+ /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
public bool HasCustomDeviceName { get; set; }
+ /// <summary>
+ /// Gets or sets the playlist item id.
+ /// </summary>
+ /// <value>The splaylist item id.</value>
public string PlaylistItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ /// <value>The server id.</value>
public string ServerId { get; set; }
+ /// <summary>
+ /// Gets or sets the user primary image tag.
+ /// </summary>
+ /// <value>The user primary image tag.</value>
public string UserPrimaryImageTag { get; set; }
/// <summary>
@@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <value>The supported commands.</value>
public IReadOnlyList<GeneralCommandType> SupportedCommands
- => Capabilities is null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
+ => Capabilities is null ? [] : Capabilities.SupportedCommands;
+ /// <summary>
+ /// Ensures a controller of type exists.
+ /// </summary>
+ /// <typeparam name="T">Class to register.</typeparam>
+ /// <param name="factory">The factory.</param>
+ /// <returns>Tuple{ISessionController, bool}.</returns>
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{
var controllers = SessionControllers.ToList();
@@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session
}
var newController = factory(this);
- _logger.LogDebug("Creating new {0}", newController.GetType().Name);
+ _logger.LogDebug("Creating new {Factory}", newController.GetType().Name);
controllers.Add(newController);
- SessionControllers = controllers.ToArray();
+ SessionControllers = [.. controllers];
return new Tuple<ISessionController, bool>(newController, true);
}
+ /// <summary>
+ /// Adds a controller to the session.
+ /// </summary>
+ /// <param name="controller">The controller.</param>
public void AddController(ISessionController controller)
{
- SessionControllers = [..SessionControllers, controller];
+ SessionControllers = [.. SessionControllers, controller];
}
+ /// <summary>
+ /// Gets a value indicating whether the session contains a user.
+ /// </summary>
+ /// <param name="userId">The user id to check.</param>
+ /// <returns><c>true</c> if this session contains the user; otherwise, <c>false</c>.</returns>
public bool ContainsUser(Guid userId)
{
if (UserId.Equals(userId))
@@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session
return false;
}
+ /// <summary>
+ /// Starts automatic progressing.
+ /// </summary>
+ /// <param name="progressInfo">The playback progress info.</param>
+ /// <value>The supported commands.</value>
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
{
if (_disposed)
@@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session
}
}
+ /// <summary>
+ /// Stops automatic progressing.
+ /// </summary>
public void StopAutomaticProgress()
{
lock (_progressLock)
@@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session
}
}
+ /// <summary>
+ /// Disposes the instance async.
+ /// </summary>
+ /// <returns>ValueTask.</returns>
public async ValueTask DisposeAsync()
{
_disposed = true;
@@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session
StopAutomaticProgress();
var controllers = SessionControllers.ToList();
- SessionControllers = Array.Empty<ISessionController>();
+ SessionControllers = [];
foreach (var controller in controllers)
{
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 0c41f3023..800317800 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -18,9 +18,10 @@ public interface ITrickplayManager
/// </summary>
/// <param name="video">The video.</param>
/// <param name="replace">Whether or not existing data should be replaced.</param>
+ /// <param name="libraryOptions">The library options.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Task.</returns>
- Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
+ Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
/// <summary>
/// Creates trickplay tiles out of individual thumbnails.
@@ -33,7 +34,7 @@ public interface ITrickplayManager
/// <remarks>
/// The output directory will be DELETED and replaced if it already exists.
/// </remarks>
- TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir);
+ TrickplayInfo CreateTiles(IReadOnlyList<string> images, int width, TrickplayOptions options, string outputDir);
/// <summary>
/// Get available trickplay resolutions and corresponding info.
@@ -43,6 +44,14 @@ public interface ITrickplayManager
Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
/// <summary>
+ /// Gets the item ids of all items with trickplay info.
+ /// </summary>
+ /// <param name="limit">The limit of items to return.</param>
+ /// <param name="offset">The offset to start the query at.</param>
+ /// <returns>The list of item ids that have trickplay info.</returns>
+ Task<IReadOnlyList<TrickplayInfo>> GetTrickplayItemsAsync(int limit, int offset);
+
+ /// <summary>
/// Saves trickplay info.
/// </summary>
/// <param name="info">The trickplay info.</param>
@@ -62,8 +71,29 @@ public interface ITrickplayManager
/// <param name="item">The item.</param>
/// <param name="width">The width of a single thumbnail.</param>
/// <param name="index">The tile's index.</param>
+ /// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
+ /// <returns>The absolute path.</returns>
+ Task<string> GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia);
+
+ /// <summary>
+ /// Gets the path to a trickplay tile image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="tileWidth">The amount of images for the tile width.</param>
+ /// <param name="tileHeight">The amount of images for the tile height.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
/// <returns>The absolute path.</returns>
- string GetTrickplayTilePath(BaseItem item, int width, int index);
+ string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false);
+
+ /// <summary>
+ /// Migrates trickplay images between local and media directories.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Task.</returns>
+ Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
/// <summary>
/// Gets the trickplay HLS playlist.
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 7e307286a..431fc0b17 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -284,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (extractableAttachmentIds.Count > 0)
{
- await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
+ await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -323,7 +323,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
processArgs += string.Format(
CultureInfo.InvariantCulture,
- " -i \"{0}\" -t 0 -f null null",
+ " -i {0} -t 0 -f null null",
inputFile);
int exitCode;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 2b6ed8fa0..b49fbf2ab 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -93,7 +93,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
"hevc_videotoolbox",
"mjpeg_videotoolbox",
"h264_rkmpp",
- "hevc_rkmpp"
+ "hevc_rkmpp",
+ "mjpeg_rkmpp"
};
private static readonly string[] _requiredFilters = new[]
@@ -136,6 +137,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"flip_vulkan",
// videotoolbox
"yadif_videotoolbox",
+ "bwdif_videotoolbox",
"scale_vt",
"transpose_vt",
"overlay_videotoolbox",
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 764230feb..826ffd0b7 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -224,7 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (OperatingSystem.IsLinux()
&& SupportsHwaccel("vaapi")
&& !string.IsNullOrEmpty(options.VaapiDevice)
- && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ && options.HardwareAccelerationType == HardwareAccelerationType.vaapi)
{
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
@@ -598,7 +598,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -610,7 +610,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false);
}
private string GetImageResolutionParameter()
@@ -636,10 +636,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
return imageResolutionParameter;
}
- private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
+ private async Task<string> ExtractImageInternal(
+ string inputPath,
+ string container,
+ MediaStream videoStream,
+ int? imageStreamIndex,
+ Video3DFormat? threedFormat,
+ TimeSpan? offset,
+ bool useIFrame,
+ ImageFormat? targetFormat,
+ bool isAudio,
+ CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(inputPath);
+ var useTradeoff = _config.GetFFmpegImgExtractPerfTradeoff();
+
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
@@ -674,7 +686,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
// mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed.
- var enableThumbnail = useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
+ var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase);
if (enableThumbnail)
{
var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
@@ -689,8 +701,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
if (SupportsFilter("tonemapx"))
{
+ var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100";
enableHdrExtraction = true;
- filters.Add("tonemapx=tonemap=bt2390:desat=0:peak=100:t=bt709:m=bt709:p=bt709:format=yuv420p");
+ filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p");
}
else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI)
{
@@ -701,13 +714,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
var vf = string.Join(',', filters);
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter());
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter());
if (offset.HasValue)
{
args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
}
+ if (useIFrame && useTradeoff)
+ {
+ args = "-skip_frame nokey " + args;
+ }
+
if (!string.IsNullOrWhiteSpace(container))
{
var inputFormat = EncodingHelper.GetInputFormat(container);
@@ -789,11 +807,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (allowHwAccel && enableKeyFrameOnlyExtraction)
{
- var supportsKeyFrameOnly = (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && options.EnableEnhancedNvdecDecoder)
- || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && OperatingSystem.IsWindows())
- || (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && options.PreferSystemNativeHwDecoder)
- || string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var hardwareAccelerationType = options.HardwareAccelerationType;
+ var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.EnableEnhancedNvdecDecoder)
+ || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSystem.IsWindows())
+ || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.PreferSystemNativeHwDecoder)
+ || hardwareAccelerationType == HardwareAccelerationType.vaapi
+ || hardwareAccelerationType == HardwareAccelerationType.videotoolbox
+ || hardwareAccelerationType == HardwareAccelerationType.rkmpp;
if (!supportsKeyFrameOnly)
{
// Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
@@ -807,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!allowHwAccel)
{
options.EnableHardwareEncoding = false;
- options.HardwareAccelerationType = string.Empty;
+ options.HardwareAccelerationType = HardwareAccelerationType.none;
options.EnableTonemapping = false;
}
@@ -851,7 +871,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
}
- if (options.HardwareAccelerationType.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase) && _isLowPriorityHwDecodeSupported)
+ if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSupported)
{
// VideoToolbox supports low priority decoding, which is useful for trickplay
inputArg = "-hwaccel_flags +low_priority " + inputArg;
@@ -885,21 +905,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("Empty or invalid input argument.");
}
- float? encoderQuality = qualityScale;
- if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
+ // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
+ // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
+ var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31);
+ var encoderQualityOption = "-qscale:v ";
+
+ if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
+ || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))
{
- // vaapi's mjpeg encoder uses jpeg quality divided by QP2LAMBDA (118) as input, instead of ffmpeg defined qscale
- // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
- // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
- encoderQuality = (100 - ((qualityScale - 1) * (100 / 30))) / 118;
+ // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale
+ encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30));
+ encoderQualityOption = "-global_quality:v ";
}
if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
{
// videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qscale
- // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
- // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
- encoderQuality = 118 - ((qualityScale - 1) * (118 / 30));
+ encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30));
+ }
+
+ if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qscale
+ encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30));
+ encoderQualityOption = "-qp_init:v ";
}
// Output arguments
@@ -910,13 +939,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Final command arguments
var args = string.Format(
CultureInfo.InvariantCulture,
- "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}-f {6} \"{7}\"",
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"",
inputArg,
filterParam,
outputThreads.GetValueOrDefault(_threads),
vidEncoder,
- qualityScale.HasValue ? "-qscale:v " + encoderQuality.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
+ encoderQualityOption + encoderQuality + " ",
vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs
+ EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim() + " ", // passthrough timestamp
"image2",
outputPath);
@@ -1082,7 +1112,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
// https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
// We need to double escape
- return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal);
+ return path
+ .Replace('\\', '/')
+ .Replace(":", "\\:", StringComparison.Ordinal)
+ .Replace("'", @"'\\\''", StringComparison.Ordinal)
+ .Replace("\"", "\\\"", StringComparison.Ordinal);
}
/// <inheritdoc />
@@ -1196,7 +1230,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Generate concat configuration entries for each file and write to file
Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath));
- using StreamWriter sw = new StreamWriter(concatFilePath);
+ using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture);
foreach (var path in files)
{
var mediaInfoResult = GetMediaInfo(
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 9ecbfa9cf..a731d4785 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -529,11 +529,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
List<MediaStream> subtitleStreams,
CancellationToken cancellationToken)
{
- var inputPath = mediaSource.Path;
+ var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
- "-i \"{0}\" -copyts",
+ "-i {0} -copyts",
inputPath);
foreach (var subtitleStream in subtitleStreams)
@@ -704,7 +704,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
+ "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath,
subtitleStreamIndex,
outputCodec,
@@ -902,6 +902,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
+ public async Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken)
+ .ConfigureAwait(false);
+ return info.Path;
+ }
+
/// <inheritdoc />
public void Dispose()
{
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 42f355b05..57557d55c 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -352,12 +352,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
{
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
- var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
- HardwareEncodingType? hardwareAccelerationType = null;
- if (Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
- {
- hardwareAccelerationType = parsedHardwareAccelerationType;
- }
+ var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 4c5213d4e..2720c0bdf 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays
+
#nullable disable
using MediaBrowser.Model.Entities;
@@ -30,9 +32,9 @@ public class EncodingOptions
EnableTonemapping = false;
EnableVppTonemapping = false;
EnableVideoToolboxTonemapping = false;
- TonemappingAlgorithm = "bt2390";
- TonemappingMode = "auto";
- TonemappingRange = "auto";
+ TonemappingAlgorithm = TonemappingAlgorithm.bt2390;
+ TonemappingMode = TonemappingMode.auto;
+ TonemappingRange = TonemappingRange.auto;
TonemappingDesat = 0;
TonemappingPeak = 100;
TonemappingParam = 0;
@@ -41,9 +43,11 @@ public class EncodingOptions
H264Crf = 23;
H265Crf = 28;
DeinterlaceDoubleRate = false;
- DeinterlaceMethod = "yadif";
+ DeinterlaceMethod = DeinterlaceMethod.yadif;
EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true;
+ EnableDecodingColorDepth10HevcRext = false;
+ EnableDecodingColorDepth12HevcRext = false;
// Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping.
EnableEnhancedNvdecDecoder = true;
PreferSystemNativeHwDecoder = true;
@@ -53,8 +57,8 @@ public class EncodingOptions
AllowHevcEncoding = false;
AllowAv1Encoding = false;
EnableSubtitleExtraction = true;
- AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
- HardwareDecodingCodecs = new string[] { "h264", "vc1" };
+ AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"];
+ HardwareDecodingCodecs = ["h264", "vc1"];
}
/// <summary>
@@ -120,7 +124,7 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the hardware acceleration type.
/// </summary>
- public string HardwareAccelerationType { get; set; }
+ public HardwareAccelerationType HardwareAccelerationType { get; set; }
/// <summary>
/// Gets or sets the FFmpeg path as set by the user via the UI.
@@ -160,17 +164,17 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the tone-mapping algorithm.
/// </summary>
- public string TonemappingAlgorithm { get; set; }
+ public TonemappingAlgorithm TonemappingAlgorithm { get; set; }
/// <summary>
/// Gets or sets the tone-mapping mode.
/// </summary>
- public string TonemappingMode { get; set; }
+ public TonemappingMode TonemappingMode { get; set; }
/// <summary>
/// Gets or sets the tone-mapping range.
/// </summary>
- public string TonemappingRange { get; set; }
+ public TonemappingRange TonemappingRange { get; set; }
/// <summary>
/// Gets or sets the tone-mapping desaturation.
@@ -210,7 +214,7 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the encoder preset.
/// </summary>
- public string EncoderPreset { get; set; }
+ public EncoderPreset? EncoderPreset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
@@ -220,7 +224,7 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the deinterlace method.
/// </summary>
- public string DeinterlaceMethod { get; set; }
+ public DeinterlaceMethod DeinterlaceMethod { get; set; }
/// <summary>
/// Gets or sets a value indicating whether 10bit HEVC decoding is enabled.
@@ -233,6 +237,16 @@ public class EncodingOptions
public bool EnableDecodingColorDepth10Vp9 { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled.
+ /// </summary>
+ public bool EnableDecodingColorDepth10HevcRext { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled.
+ /// </summary>
+ public bool EnableDecodingColorDepth12HevcRext { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether the enhanced NVDEC is enabled.
/// </summary>
public bool EnableEnhancedNvdecDecoder { get; set; }
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index b0f5c2a11..6054ba34e 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -2,15 +2,20 @@
using System;
using System.ComponentModel;
+using System.Linq;
namespace MediaBrowser.Model.Configuration
{
public class LibraryOptions
{
+ private static readonly string[] _defaultTagDelimiters = ["/", "|", ";", "\\"];
+
public LibraryOptions()
{
TypeOptions = Array.Empty<TypeOptions>();
DisabledSubtitleFetchers = Array.Empty<string>();
+ DisabledMediaSegmentProviders = Array.Empty<string>();
+ MediaSegmentProvideOrder = Array.Empty<string>();
SubtitleFetcherOrder = Array.Empty<string>();
DisabledLocalMetadataReaders = Array.Empty<string>();
DisabledLyricFetchers = Array.Empty<string>();
@@ -24,9 +29,15 @@ namespace MediaBrowser.Model.Configuration
EnablePhotos = true;
SaveSubtitlesWithMedia = true;
SaveLyricsWithMedia = false;
+ SaveTrickplayWithMedia = false;
PathInfos = Array.Empty<MediaPathInfo>();
EnableAutomaticSeriesGrouping = true;
SeasonZeroDisplayName = "Specials";
+
+ PreferNonstandardArtistsTag = false;
+ UseCustomTagDelimiters = false;
+ CustomTagDelimiters = _defaultTagDelimiters;
+ DelimiterWhitelist = Array.Empty<string>();
}
public bool Enabled { get; set; } = true;
@@ -86,6 +97,10 @@ namespace MediaBrowser.Model.Configuration
public string[] SubtitleFetcherOrder { get; set; }
+ public string[] DisabledMediaSegmentProviders { get; set; }
+
+ public string[] MediaSegmentProvideOrder { get; set; }
+
public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; }
public bool SkipSubtitlesIfAudioTrackMatches { get; set; }
@@ -99,10 +114,23 @@ namespace MediaBrowser.Model.Configuration
[DefaultValue(false)]
public bool SaveLyricsWithMedia { get; set; }
+ [DefaultValue(false)]
+ public bool SaveTrickplayWithMedia { get; set; }
+
public string[] DisabledLyricFetchers { get; set; }
public string[] LyricFetcherOrder { get; set; }
+ [DefaultValue(false)]
+ public bool PreferNonstandardArtistsTag { get; set; }
+
+ [DefaultValue(false)]
+ public bool UseCustomTagDelimiters { get; set; }
+
+ public string[] CustomTagDelimiters { get; set; }
+
+ public string[] DelimiterWhitelist { get; set; }
+
public bool AutomaticallyAddToCollection { get; set; }
public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
index a7bc43590..25a5d5606 100644
--- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs
+++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
@@ -16,7 +16,5 @@ namespace MediaBrowser.Model.Configuration
}
public string Path { get; set; }
-
- public string? NetworkPath { get; set; }
}
}
diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
index ef303726d..670d6e383 100644
--- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs
+++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration
MetadataFetcher,
MetadataSaver,
SubtitleFetcher,
- LyricFetcher
+ LyricFetcher,
+ MediaSegmentProvider
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 52f7e53b8..5ad588200 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -96,8 +96,6 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// <value>The metadata path.</value>
public string MetadataPath { get; set; } = string.Empty;
- public string MetadataNetworkPath { get; set; } = string.Empty;
-
/// <summary>
/// Gets or sets the preferred metadata language.
/// </summary>
diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs
index 4962992a0..115598613 100644
--- a/MediaBrowser.Model/Devices/DeviceInfo.cs
+++ b/MediaBrowser.Model/Devices/DeviceInfo.cs
@@ -1,69 +1,84 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
using MediaBrowser.Model.Session;
-namespace MediaBrowser.Model.Devices
+namespace MediaBrowser.Model.Devices;
+
+/// <summary>
+/// A class for device Information.
+/// </summary>
+public class DeviceInfo
{
- public class DeviceInfo
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceInfo"/> class.
+ /// </summary>
+ public DeviceInfo()
{
- public DeviceInfo()
- {
- Capabilities = new ClientCapabilities();
- }
+ Capabilities = new ClientCapabilities();
+ }
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string? Name { get; set; }
- public string CustomName { get; set; }
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ /// <value>The custom name.</value>
+ public string? CustomName { get; set; }
- /// <summary>
- /// Gets or sets the access token.
- /// </summary>
- public string AccessToken { get; set; }
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string? AccessToken { get; set; }
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string? Id { get; set; }
- /// <summary>
- /// Gets or sets the last name of the user.
- /// </summary>
- /// <value>The last name of the user.</value>
- public string LastUserName { get; set; }
+ /// <summary>
+ /// Gets or sets the last name of the user.
+ /// </summary>
+ /// <value>The last name of the user.</value>
+ public string? LastUserName { get; set; }
- /// <summary>
- /// Gets or sets the name of the application.
- /// </summary>
- /// <value>The name of the application.</value>
- public string AppName { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the application.
+ /// </summary>
+ /// <value>The name of the application.</value>
+ public string? AppName { get; set; }
- /// <summary>
- /// Gets or sets the application version.
- /// </summary>
- /// <value>The application version.</value>
- public string AppVersion { get; set; }
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string? AppVersion { get; set; }
- /// <summary>
- /// Gets or sets the last user identifier.
- /// </summary>
- /// <value>The last user identifier.</value>
- public Guid LastUserId { get; set; }
+ /// <summary>
+ /// Gets or sets the last user identifier.
+ /// </summary>
+ /// <value>The last user identifier.</value>
+ public Guid? LastUserId { get; set; }
- /// <summary>
- /// Gets or sets the date last modified.
- /// </summary>
- /// <value>The date last modified.</value>
- public DateTime DateLastActivity { get; set; }
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <value>The date last modified.</value>
+ public DateTime? DateLastActivity { get; set; }
- /// <summary>
- /// Gets or sets the capabilities.
- /// </summary>
- /// <value>The capabilities.</value>
- public ClientCapabilities Capabilities { get; set; }
+ /// <summary>
+ /// Gets or sets the capabilities.
+ /// </summary>
+ /// <value>The capabilities.</value>
+ public ClientCapabilities Capabilities { get; set; }
- public string IconUrl { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the icon URL.
+ /// </summary>
+ /// <value>The icon URL.</value>
+ public string? IconUrl { get; set; }
}
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs
index 07c1a29a4..da34eddcd 100644
--- a/MediaBrowser.Model/Dlna/CodecProfile.cs
+++ b/MediaBrowser.Model/Dlna/CodecProfile.cs
@@ -1,74 +1,94 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna;
-namespace MediaBrowser.Model.Dlna
+/// <summary>
+/// Defines the <see cref="CodecProfile"/>.
+/// </summary>
+public class CodecProfile
{
- public class CodecProfile
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CodecProfile"/> class.
+ /// </summary>
+ public CodecProfile()
{
- public CodecProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- ApplyConditions = Array.Empty<ProfileCondition>();
- }
-
- [XmlAttribute("type")]
- public CodecType Type { get; set; }
-
- public ProfileCondition[] Conditions { get; set; }
-
- public ProfileCondition[] ApplyConditions { get; set; }
-
- [XmlAttribute("codec")]
- public string Codec { get; set; }
+ Conditions = [];
+ ApplyConditions = [];
+ }
- [XmlAttribute("container")]
- public string Container { get; set; }
+ /// <summary>
+ /// Gets or sets the <see cref="CodecType"/> which this container must meet.
+ /// </summary>
+ [XmlAttribute("type")]
+ public CodecType Type { get; set; }
- [XmlAttribute("subcontainer")]
- public string SubContainer { get; set; }
+ /// <summary>
+ /// Gets or sets the list of <see cref="ProfileCondition"/> which this profile must meet.
+ /// </summary>
+ public ProfileCondition[] Conditions { get; set; }
- public string[] GetCodecs()
- {
- return ContainerProfile.SplitValue(Codec);
- }
+ /// <summary>
+ /// Gets or sets the list of <see cref="ProfileCondition"/> to apply if this profile is met.
+ /// </summary>
+ public ProfileCondition[] ApplyConditions { get; set; }
- private bool ContainsContainer(string container, bool useSubContainer = false)
- {
- var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
- return ContainerProfile.ContainsContainer(containerToCheck, container);
- }
+ /// <summary>
+ /// Gets or sets the codec(s) that this profile applies to.
+ /// </summary>
+ [XmlAttribute("codec")]
+ public string? Codec { get; set; }
- public bool ContainsAnyCodec(string codec, string container, bool useSubContainer = false)
- {
- return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container, useSubContainer);
- }
+ /// <summary>
+ /// Gets or sets the container(s) which this profile will be applied to.
+ /// </summary>
+ [XmlAttribute("container")]
+ public string? Container { get; set; }
- public bool ContainsAnyCodec(string[] codec, string container, bool useSubContainer = false)
- {
- if (!ContainsContainer(container, useSubContainer))
- {
- return false;
- }
+ /// <summary>
+ /// Gets or sets the sub-container(s) which this profile will be applied to.
+ /// </summary>
+ [XmlAttribute("subcontainer")]
+ public string? SubContainer { get; set; }
- var codecs = GetCodecs();
- if (codecs.Length == 0)
- {
- return true;
- }
+ /// <summary>
+ /// Checks to see whether the codecs and containers contain the given parameters.
+ /// </summary>
+ /// <param name="codecs">The codecs to match.</param>
+ /// <param name="container">The container to match.</param>
+ /// <param name="useSubContainer">Consider sub-containers.</param>
+ /// <returns>True if both conditions are met.</returns>
+ public bool ContainsAnyCodec(IReadOnlyList<string> codecs, string? container, bool useSubContainer = false)
+ {
+ var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+ return ContainerHelper.ContainsContainer(containerToCheck, container) && codecs.Any(c => ContainerHelper.ContainsContainer(Codec, false, c));
+ }
- foreach (var val in codec)
- {
- if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
+ /// <summary>
+ /// Checks to see whether the codecs and containers contain the given parameters.
+ /// </summary>
+ /// <param name="codec">The codec to match.</param>
+ /// <param name="container">The container to match.</param>
+ /// <param name="useSubContainer">Consider sub-containers.</param>
+ /// <returns>True if both conditions are met.</returns>
+ public bool ContainsAnyCodec(string? codec, string? container, bool useSubContainer = false)
+ {
+ return ContainsAnyCodec(codec.AsSpan(), container, useSubContainer);
+ }
- return false;
- }
+ /// <summary>
+ /// Checks to see whether the codecs and containers contain the given parameters.
+ /// </summary>
+ /// <param name="codec">The codec to match.</param>
+ /// <param name="container">The container to match.</param>
+ /// <param name="useSubContainer">Consider sub-containers.</param>
+ /// <returns>True if both conditions are met.</returns>
+ public bool ContainsAnyCodec(ReadOnlySpan<char> codec, string? container, bool useSubContainer = false)
+ {
+ var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+ return ContainerHelper.ContainsContainer(containerToCheck, container) && ContainerHelper.ContainsContainer(Codec, false, codec);
}
}
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index 978004268..a42179907 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -1,74 +1,49 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1819 // Properties should not return arrays
using System;
+using System.Collections.Generic;
using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// Defines the <see cref="ContainerProfile"/>.
+/// </summary>
+public class ContainerProfile
{
- public class ContainerProfile
+ /// <summary>
+ /// Gets or sets the <see cref="DlnaProfileType"/> which this container must meet.
+ /// </summary>
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of <see cref="ProfileCondition"/> which this container will be applied to.
+ /// </summary>
+ public ProfileCondition[] Conditions { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets the container(s) which this container must meet.
+ /// </summary>
+ [XmlAttribute("container")]
+ public string? Container { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sub container(s) which this container must meet.
+ /// </summary>
+ [XmlAttribute("subcontainer")]
+ public string? SubContainer { get; set; }
+
+ /// <summary>
+ /// Returns true if an item in <paramref name="container"/> appears in the <see cref="Container"/> property.
+ /// </summary>
+ /// <param name="container">The item to match.</param>
+ /// <param name="useSubContainer">Consider subcontainers.</param>
+ /// <returns>The result of the operation.</returns>
+ public bool ContainsContainer(ReadOnlySpan<char> container, bool useSubContainer = false)
{
- [XmlAttribute("type")]
- public DlnaProfileType Type { get; set; }
-
- public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>();
-
- [XmlAttribute("container")]
- public string Container { get; set; } = string.Empty;
-
- public static string[] SplitValue(string? value)
- {
- if (string.IsNullOrEmpty(value))
- {
- return Array.Empty<string>();
- }
-
- return value.Split(',', StringSplitOptions.RemoveEmptyEntries);
- }
-
- public bool ContainsContainer(string? container)
- {
- var containers = SplitValue(Container);
-
- return ContainsContainer(containers, container);
- }
-
- public static bool ContainsContainer(string? profileContainers, string? inputContainer)
- {
- var isNegativeList = false;
- if (profileContainers is not null && profileContainers.StartsWith('-'))
- {
- isNegativeList = true;
- profileContainers = profileContainers.Substring(1);
- }
-
- return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer);
- }
-
- public static bool ContainsContainer(string[]? profileContainers, string? inputContainer)
- {
- return ContainsContainer(profileContainers, false, inputContainer);
- }
-
- public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer)
- {
- if (profileContainers is null || profileContainers.Length == 0)
- {
- // Empty profiles always support all containers/codecs
- return true;
- }
-
- var allInputContainers = SplitValue(inputContainer);
-
- foreach (var container in allInputContainers)
- {
- if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase))
- {
- return !isNegativeList;
- }
- }
-
- return isNegativeList;
- }
+ var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container;
+ return ContainerHelper.ContainsContainer(containerToCheck, container);
}
}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 2addebbfc..995b7633a 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,74 +1,71 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
-using System.Xml.Serialization;
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play.
+/// <br/>
+/// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and
+/// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
+/// the device is able to direct play (without transcoding or remuxing),
+/// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
+/// </summary>
+public class DeviceProfile
{
/// <summary>
- /// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play.
- /// <br/>
- /// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and
- /// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
- /// the device is able to direct play (without transcoding or remuxing),
- /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
+ /// Gets or sets the name of this device profile. User profiles must have a unique name.
/// </summary>
- public class DeviceProfile
- {
- /// <summary>
- /// Gets or sets the name of this device profile.
- /// </summary>
- public string? Name { get; set; }
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets the Id.
- /// </summary>
- [XmlIgnore]
- public string? Id { get; set; }
+ /// <summary>
+ /// Gets or sets the unique internal identifier.
+ /// </summary>
+ public Guid? Id { get; set; }
- /// <summary>
- /// Gets or sets the maximum allowed bitrate for all streamed content.
- /// </summary>
- public int? MaxStreamingBitrate { get; set; } = 8000000;
+ /// <summary>
+ /// Gets or sets the maximum allowed bitrate for all streamed content.
+ /// </summary>
+ public int? MaxStreamingBitrate { get; set; } = 8000000;
- /// <summary>
- /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).
- /// </summary>
- public int? MaxStaticBitrate { get; set; } = 8000000;
+ /// <summary>
+ /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).
+ /// </summary>
+ public int? MaxStaticBitrate { get; set; } = 8000000;
- /// <summary>
- /// Gets or sets the maximum allowed bitrate for transcoded music streams.
- /// </summary>
- public int? MusicStreamingTranscodingBitrate { get; set; } = 128000;
+ /// <summary>
+ /// Gets or sets the maximum allowed bitrate for transcoded music streams.
+ /// </summary>
+ public int? MusicStreamingTranscodingBitrate { get; set; } = 128000;
- /// <summary>
- /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.
- /// </summary>
- public int? MaxStaticMusicBitrate { get; set; } = 8000000;
+ /// <summary>
+ /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.
+ /// </summary>
+ public int? MaxStaticMusicBitrate { get; set; } = 8000000;
- /// <summary>
- /// Gets or sets the direct play profiles.
- /// </summary>
- public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>();
+ /// <summary>
+ /// Gets or sets the direct play profiles.
+ /// </summary>
+ public DirectPlayProfile[] DirectPlayProfiles { get; set; } = [];
- /// <summary>
- /// Gets or sets the transcoding profiles.
- /// </summary>
- public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty<TranscodingProfile>();
+ /// <summary>
+ /// Gets or sets the transcoding profiles.
+ /// </summary>
+ public TranscodingProfile[] TranscodingProfiles { get; set; } = [];
- /// <summary>
- /// Gets or sets the container profiles.
- /// </summary>
- public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty<ContainerProfile>();
+ /// <summary>
+ /// Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur.
+ /// </summary>
+ public ContainerProfile[] ContainerProfiles { get; set; } = [];
- /// <summary>
- /// Gets or sets the codec profiles.
- /// </summary>
- public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>();
+ /// <summary>
+ /// Gets or sets the codec profiles.
+ /// </summary>
+ public CodecProfile[] CodecProfiles { get; set; } = [];
- /// <summary>
- /// Gets or sets the subtitle profiles.
- /// </summary>
- public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>();
- }
+ /// <summary>
+ /// Gets or sets the subtitle profiles.
+ /// </summary>
+ public SubtitleProfile[] SubtitleProfiles { get; set; } = [];
}
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index f68235d86..438df3441 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -1,36 +1,65 @@
-#pragma warning disable CS1591
-
using System.Xml.Serialization;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna;
-namespace MediaBrowser.Model.Dlna
+/// <summary>
+/// Defines the <see cref="DirectPlayProfile"/>.
+/// </summary>
+public class DirectPlayProfile
{
- public class DirectPlayProfile
- {
- [XmlAttribute("container")]
- public string? Container { get; set; }
+ /// <summary>
+ /// Gets or sets the container.
+ /// </summary>
+ [XmlAttribute("container")]
+ public string Container { get; set; } = string.Empty;
- [XmlAttribute("audioCodec")]
- public string? AudioCodec { get; set; }
+ /// <summary>
+ /// Gets or sets the audio codec.
+ /// </summary>
+ [XmlAttribute("audioCodec")]
+ public string? AudioCodec { get; set; }
- [XmlAttribute("videoCodec")]
- public string? VideoCodec { get; set; }
+ /// <summary>
+ /// Gets or sets the video codec.
+ /// </summary>
+ [XmlAttribute("videoCodec")]
+ public string? VideoCodec { get; set; }
- [XmlAttribute("type")]
- public DlnaProfileType Type { get; set; }
+ /// <summary>
+ /// Gets or sets the Dlna profile type.
+ /// </summary>
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
- public bool SupportsContainer(string? container)
- {
- return ContainerProfile.ContainsContainer(Container, container);
- }
+ /// <summary>
+ /// Returns whether the <see cref="Container"/> supports the <paramref name="container"/>.
+ /// </summary>
+ /// <param name="container">The container to match against.</param>
+ /// <returns>True if supported.</returns>
+ public bool SupportsContainer(string? container)
+ {
+ return ContainerHelper.ContainsContainer(Container, container);
+ }
- public bool SupportsVideoCodec(string? codec)
- {
- return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec);
- }
+ /// <summary>
+ /// Returns whether the <see cref="VideoCodec"/> supports the <paramref name="codec"/>.
+ /// </summary>
+ /// <param name="codec">The codec to match against.</param>
+ /// <returns>True if supported.</returns>
+ public bool SupportsVideoCodec(string? codec)
+ {
+ return Type == DlnaProfileType.Video && ContainerHelper.ContainsContainer(VideoCodec, codec);
+ }
- public bool SupportsAudioCodec(string? codec)
- {
- return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec);
- }
+ /// <summary>
+ /// Returns whether the <see cref="AudioCodec"/> supports the <paramref name="codec"/>.
+ /// </summary>
+ /// <param name="codec">The codec to match against.</param>
+ /// <returns>True if supported.</returns>
+ public bool SupportsAudioCodec(string? codec)
+ {
+ // Video profiles can have audio codec restrictions too, therefore incude Video as valid type.
+ return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec);
}
}
diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs
index eca971e95..6b26ca94b 100644
--- a/MediaBrowser.Model/Dlna/MediaOptions.cs
+++ b/MediaBrowser.Model/Dlna/MediaOptions.cs
@@ -50,6 +50,11 @@ namespace MediaBrowser.Model.Dlna
public bool AllowVideoStreamCopy { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ /// </summary>
+ public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
+
+ /// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid ItemId { get; set; }
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index 5d7daa81a..1a636b240 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -2,28 +2,33 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
namespace MediaBrowser.Model.Dlna
{
public static class ResolutionNormalizer
{
- private static readonly ResolutionConfiguration[] Configurations =
- new[]
- {
- new ResolutionConfiguration(426, 320000),
- new ResolutionConfiguration(640, 400000),
- new ResolutionConfiguration(720, 950000),
- new ResolutionConfiguration(1280, 2500000),
- new ResolutionConfiguration(1920, 4000000),
- new ResolutionConfiguration(2560, 20000000),
- new ResolutionConfiguration(3840, 35000000)
- };
+ // Please note: all bitrate here are in the scale of SDR h264 bitrate at 30fps
+ private static readonly ResolutionConfiguration[] _configurations =
+ [
+ new ResolutionConfiguration(416, 365000),
+ new ResolutionConfiguration(640, 730000),
+ new ResolutionConfiguration(768, 1100000),
+ new ResolutionConfiguration(960, 3000000),
+ new ResolutionConfiguration(1280, 6000000),
+ new ResolutionConfiguration(1920, 13500000),
+ new ResolutionConfiguration(2560, 28000000),
+ new ResolutionConfiguration(3840, 50000000)
+ ];
public static ResolutionOptions Normalize(
int? inputBitrate,
int outputBitrate,
+ int h264EquivalentOutputBitrate,
int? maxWidth,
- int? maxHeight)
+ int? maxHeight,
+ float? targetFps,
+ bool isHdr = false) // We are not doing HDR transcoding for now, leave for future use
{
// If the bitrate isn't changing, then don't downscale the resolution
if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value)
@@ -38,16 +43,26 @@ namespace MediaBrowser.Model.Dlna
}
}
- var resolutionConfig = GetResolutionConfiguration(outputBitrate);
- if (resolutionConfig is not null)
+ var referenceBitrate = h264EquivalentOutputBitrate * (30.0f / (targetFps ?? 30.0f));
+
+ if (isHdr)
{
- var originvalValue = maxWidth;
+ referenceBitrate *= 0.8f;
+ }
- maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth);
- if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value)
- {
- maxHeight = null;
- }
+ var resolutionConfig = GetResolutionConfiguration(Convert.ToInt32(referenceBitrate));
+
+ if (resolutionConfig is null)
+ {
+ return new ResolutionOptions { MaxWidth = maxWidth, MaxHeight = maxHeight };
+ }
+
+ var originWidthValue = maxWidth;
+
+ maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth);
+ if (!originWidthValue.HasValue || originWidthValue.Value != maxWidth.Value)
+ {
+ maxHeight = null;
}
return new ResolutionOptions
@@ -59,19 +74,7 @@ namespace MediaBrowser.Model.Dlna
private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate)
{
- ResolutionConfiguration previousOption = null;
-
- foreach (var config in Configurations)
- {
- if (outputBitrate <= config.MaxBitrate)
- {
- return previousOption ?? config;
- }
-
- previousOption = config;
- }
-
- return null;
+ return _configurations.FirstOrDefault(config => outputBitrate <= config.MaxBitrate);
}
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 7f387bfaa..a25ddc367 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -6,6 +6,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -19,15 +20,17 @@ namespace MediaBrowser.Model.Dlna
{
// Aliases
internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit;
- internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal;
- internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported;
- internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported;
+ internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal;
+ internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons;
+ internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported;
+ internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons;
+ internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported;
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
- private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "av1" };
- private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
- private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
+ private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
+ private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
+ private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -49,7 +52,7 @@ namespace MediaBrowser.Model.Dlna
{
ValidateMediaOptions(options, false);
- var streams = new List<StreamInfo>();
+ List<StreamInfo> streams = [];
foreach (var mediaSource in options.MediaSources)
{
if (!(string.IsNullOrEmpty(options.MediaSourceId)
@@ -62,7 +65,7 @@ namespace MediaBrowser.Model.Dlna
if (streamInfo is not null)
{
streamInfo.DeviceId = options.DeviceId;
- streamInfo.DeviceProfileId = options.Profile.Id;
+ streamInfo.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
streams.Add(streamInfo);
}
}
@@ -127,7 +130,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlayMethod is PlayMethod.DirectStream)
{
var remuxContainer = item.TranscodingContainer ?? "ts";
- var supportedHlsContainers = new[] { "ts", "mp4" };
+ string[] supportedHlsContainers = ["ts", "mp4"];
// If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference
// The client should be responsible to ensure this container is compatible
remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer;
@@ -224,7 +227,7 @@ namespace MediaBrowser.Model.Dlna
? options.MediaSources
: options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase));
- var streams = new List<StreamInfo>();
+ List<StreamInfo> streams = [];
foreach (var mediaSourceInfo in mediaSources)
{
var streamInfo = BuildVideoItem(mediaSourceInfo, options);
@@ -237,7 +240,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var stream in streams)
{
stream.DeviceId = options.DeviceId;
- stream.DeviceProfileId = options.Profile.Id;
+ stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
}
return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
@@ -352,7 +355,7 @@ namespace MediaBrowser.Model.Dlna
return TranscodeReason.VideoBitrateNotSupported;
case ProfileConditionValue.VideoCodecTag:
- return TranscodeReason.VideoCodecNotSupported;
+ return TranscodeReason.VideoCodecTagNotSupported;
case ProfileConditionValue.VideoFramerate:
return TranscodeReason.VideoFramerateNotSupported;
@@ -388,30 +391,31 @@ namespace MediaBrowser.Model.Dlna
/// <returns>The normalized input container.</returns>
public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
{
- if (string.IsNullOrEmpty(inputContainer))
+ // If the source is Live TV the inputContainer will be null until the mediasource is probed on first access
+ if (profile is null || string.IsNullOrEmpty(inputContainer) || !inputContainer.Contains(',', StringComparison.OrdinalIgnoreCase))
{
- return null;
+ return inputContainer;
}
- var formats = ContainerProfile.SplitValue(inputContainer);
-
- if (profile is not null)
+ var formats = ContainerHelper.Split(inputContainer);
+ var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile];
+ foreach (var format in formats)
{
- var playProfiles = playProfile is null ? profile.DirectPlayProfiles : new[] { playProfile };
- foreach (var format in formats)
+ foreach (var directPlayProfile in playProfiles)
{
- foreach (var directPlayProfile in playProfiles)
+ if (directPlayProfile.Type != type)
{
- if (directPlayProfile.Type == type
- && directPlayProfile.SupportsContainer(format))
- {
- return format;
- }
+ continue;
+ }
+
+ if (directPlayProfile.SupportsContainer(format))
+ {
+ return format;
}
}
}
- return formats[0];
+ return inputContainer;
}
private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
@@ -531,7 +535,6 @@ namespace MediaBrowser.Model.Dlna
private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
{
int highestScore = -1;
-
foreach (var stream in item.MediaStreams)
{
if (stream.Type == MediaStreamType.Subtitle
@@ -542,7 +545,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- var topStreams = new List<MediaStream>();
+ List<MediaStream> topStreams = [];
foreach (var stream in item.MediaStreams)
{
if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore)
@@ -621,8 +624,8 @@ namespace MediaBrowser.Model.Dlna
playlistItem.Container = container;
playlistItem.SubProtocol = protocol;
- playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
+ playlistItem.VideoCodecs = [item.VideoStream.Codec];
+ playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
}
private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
@@ -637,7 +640,8 @@ namespace MediaBrowser.Model.Dlna
RunTimeTicks = item.RunTimeTicks,
Context = options.Context,
DeviceProfile = options.Profile,
- SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles)
+ SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles),
+ AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding
};
var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
@@ -649,7 +653,7 @@ namespace MediaBrowser.Model.Dlna
}
// Collect candidate audio streams
- ICollection<MediaStream> candidateAudioStreams = audioStream is null ? Array.Empty<MediaStream>() : new[] { audioStream };
+ ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
{
if (audioStream?.IsDefault == true)
@@ -700,7 +704,8 @@ namespace MediaBrowser.Model.Dlna
directPlayProfile = directPlayInfo.Profile;
playlistItem.PlayMethod = directPlay.Value;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
- playlistItem.VideoCodecs = new[] { videoStream.Codec };
+ var videoCodec = videoStream?.Codec;
+ playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec];
if (directPlay == PlayMethod.DirectPlay)
{
@@ -711,7 +716,7 @@ namespace MediaBrowser.Model.Dlna
{
playlistItem.AudioStreamIndex = audioStreamIndex;
var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
- playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec };
+ playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec];
}
}
else if (directPlay == PlayMethod.DirectStream)
@@ -719,7 +724,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream?.Index;
if (audioStream is not null)
{
- playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
+ playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
}
SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
@@ -751,8 +756,9 @@ namespace MediaBrowser.Model.Dlna
{
// Can't direct play, find the transcoding profile
// If we do this for direct-stream we will overwrite the info
- var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem);
- if (transcodingProfile is not null)
+ var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, playlistItem);
+
+ if (transcodingProfile is not null && playMethod.HasValue)
{
SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
@@ -763,10 +769,9 @@ namespace MediaBrowser.Model.Dlna
if (subtitleStream is not null)
{
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
-
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
- playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
+ playlistItem.SubtitleCodecs = [subtitleProfile.Format];
}
if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
@@ -790,58 +795,94 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
- private TranscodingProfile? GetVideoTranscodeProfile(
+ private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile(
MediaSourceInfo item,
MediaOptions options,
MediaStream? videoStream,
MediaStream? audioStream,
- IEnumerable<MediaStream> candidateAudioStreams,
- MediaStream? subtitleStream,
StreamInfo playlistItem)
{
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
- return null;
+ return (null, null);
}
var transcodingProfiles = options.Profile.TranscodingProfiles
.Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context);
- if (options.AllowVideoStreamCopy)
+ if (item.UseMostCompatibleTranscodingProfile)
{
- // prefer direct copy profile
- float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
- TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
- int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
- int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
+ transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase));
+ }
+
+ var videoCodec = videoStream?.Codec;
+ float videoFramerate = videoStream?.ReferenceFrameRate ?? 0;
+ TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
+ int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
+
+ var audioCodec = audioStream?.Codec;
+ var audioProfile = audioStream?.Profile;
+ var audioChannels = audioStream?.Channels;
+ var audioBitrate = audioStream?.BitRate;
+ var audioSampleRate = audioStream?.SampleRate;
+ var audioBitDepth = audioStream?.BitDepth;
- transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile =>
+ var analyzedProfiles = transcodingProfiles
+ .Select(transcodingProfile =>
{
- var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
+ var rank = (Video: 3, Audio: 3);
+
+ var container = transcodingProfile.Container;
+
+ if (options.AllowVideoStreamCopy)
+ {
+ if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
+ {
+ var appliedVideoConditions = options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.Video &&
+ i.ContainsAnyCodec(videoCodec, container) &&
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
+ .Select(i =>
+ i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
+
+ // An empty appliedVideoConditions means that the codec has no conditions for the current video stream
+ var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
+ rank.Video = conditionsSatisfied ? 1 : 2;
+ }
+ }
- if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
+ if (options.AllowAudioStreamCopy)
{
- var videoCodec = videoStream?.Codec;
- var container = transcodingProfile.Container;
- var appliedVideoConditions = options.Profile.CodecProfiles
- .Where(i => i.Type == CodecType.Video &&
- i.ContainsAnyCodec(videoCodec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
- .Select(i =>
- i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
-
- // An empty appliedVideoConditions means that the codec has no conditions for the current video stream
- var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
- return conditionsSatisfied ? 1 : 2;
+ if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
+ {
+ var appliedVideoConditions = options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.VideoAudio &&
+ i.ContainsAnyCodec(audioCodec, container) &&
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
+ .Select(i =>
+ i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
+
+ // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
+ var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
+ rank.Audio = conditionsSatisfied ? 1 : 2;
+ }
+ }
+
+ PlayMethod playMethod = PlayMethod.Transcode;
+
+ if (rank.Video == 1)
+ {
+ playMethod = PlayMethod.DirectStream;
}
- return 3;
+ return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank);
})
- .OrderBy(lookup => lookup.Key)
- .SelectMany(lookup => lookup);
- }
+ .OrderBy(analysis => analysis.Rank);
- return transcodingProfiles.FirstOrDefault();
+ var profileMatch = analyzedProfiles.FirstOrDefault();
+
+ return (profileMatch.Profile, profileMatch.PlayMethod);
}
private void BuildStreamVideoItem(
@@ -856,26 +897,24 @@ namespace MediaBrowser.Model.Dlna
string? audioCodec)
{
// Prefer matching video codecs
- var videoCodecs = ContainerProfile.SplitValue(videoCodec);
+ var videoCodecs = ContainerHelper.Split(videoCodec).ToList();
- // Enforce HLS video codec restrictions
- if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
+ if (videoCodecs.Count == 0 && videoStream is not null)
{
- videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
+ // Add the original codec if no codec is specified
+ videoCodecs.Add(videoStream.Codec);
}
- var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
- if (directVideoCodec is not null)
+ // Enforce HLS video codec restrictions
+ if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
- // merge directVideoCodec to videoCodecs
- Array.Resize(ref videoCodecs, videoCodecs.Length + 1);
- videoCodecs[^1] = directVideoCodec;
+ videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList();
}
playlistItem.VideoCodecs = videoCodecs;
// Copy video codec options as a starting point, this applies to transcode and direct-stream
- playlistItem.MaxFramerate = videoStream?.AverageFrameRate;
+ playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate;
var qualifier = videoStream?.Codec;
if (videoStream?.Level is not null)
{
@@ -893,22 +932,28 @@ namespace MediaBrowser.Model.Dlna
}
// Prefer matching audio codecs, could do better here
- var audioCodecs = ContainerProfile.SplitValue(audioCodec);
+ var audioCodecs = ContainerHelper.Split(audioCodec).ToList();
+
+ if (audioCodecs.Count == 0 && audioStream is not null)
+ {
+ // Add the original codec if no codec is specified
+ audioCodecs.Add(audioStream.Codec);
+ }
// Enforce HLS audio codec restrictions
if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
- audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray();
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList();
}
else
{
- audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray();
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList();
}
}
- var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault();
+ var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
@@ -925,7 +970,8 @@ namespace MediaBrowser.Model.Dlna
{
audioStream = directAudioStream;
playlistItem.AudioStreamIndex = audioStream.Index;
- playlistItem.AudioCodecs = new[] { audioStream.Codec };
+ audioCodecs = [audioStream.Codec];
+ playlistItem.AudioCodecs = audioCodecs;
// Copy matching audio codec options
playlistItem.AudioSampleRate = audioStream.SampleRate;
@@ -949,7 +995,7 @@ namespace MediaBrowser.Model.Dlna
double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile;
VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
- float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
@@ -966,19 +1012,17 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
- i.ContainsAnyCodec(videoStream?.Codec, container, useSubContainer) &&
+ i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
-
- foreach (var i in appliedVideoConditions)
+ foreach (var condition in appliedVideoConditions)
{
- var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
- foreach (var transcodingVideoCodec in transcodingVideoCodecs)
+ foreach (var transcodingVideoCodec in playlistItem.VideoCodecs)
{
- if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
+ if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
{
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
+ ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true);
continue;
}
}
@@ -999,15 +1043,14 @@ namespace MediaBrowser.Model.Dlna
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
- i.ContainsAnyCodec(audioStream?.Codec, container) &&
+ i.ContainsAnyCodec(playlistItem.AudioCodecs, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var codecProfile in appliedAudioConditions)
{
- var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
- foreach (var transcodingAudioCodec in transcodingAudioCodecs)
+ foreach (var transcodingAudioCodec in playlistItem.AudioCodecs)
{
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{
@@ -1077,9 +1120,9 @@ namespace MediaBrowser.Model.Dlna
return 192000;
}
- private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
+ private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
{
- string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+ string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
@@ -1096,7 +1139,7 @@ namespace MediaBrowser.Model.Dlna
&& audioStream.Channels.HasValue
&& audioStream.Channels.Value > targetAudioChannels.Value)
{
- // Reduce the bitrate if we're downmixing.
+ // Reduce the bitrate if we're down mixing.
defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
}
else if (targetAudioChannels.HasValue
@@ -1104,8 +1147,8 @@ namespace MediaBrowser.Model.Dlna
&& audioStream.Channels.Value <= targetAudioChannels.Value
&& !string.IsNullOrEmpty(audioStream.Codec)
&& targetAudioCodecs is not null
- && targetAudioCodecs.Length > 0
- && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
+ && targetAudioCodecs.Count > 0
+ && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
{
// Shift the bitrate if we're transcoding to a different audio codec.
defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
@@ -1208,7 +1251,7 @@ namespace MediaBrowser.Model.Dlna
double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile;
VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
- float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
@@ -1244,7 +1287,7 @@ namespace MediaBrowser.Model.Dlna
!checkVideoConditions(codecProfile.ApplyConditions).Any())
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
- // Check audiocandidates profile conditions
+ // Check audio candidates profile conditions
var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream));
TranscodeReason subtitleProfileReasons = 0;
@@ -1261,25 +1304,8 @@ namespace MediaBrowser.Model.Dlna
}
}
- var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons };
- var rank = (ref TranscodeReason a) =>
- {
- var index = 1;
- foreach (var flag in rankings)
- {
- var reason = a & flag;
- if (reason != 0)
- {
- return index;
- }
-
- index++;
- }
-
- return index;
- };
-
var containerSupported = false;
+ TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons];
// Check DirectPlay profiles to see if it can be direct played
var analyzedProfiles = profile.DirectPlayProfiles
@@ -1345,7 +1371,8 @@ namespace MediaBrowser.Model.Dlna
playMethod = PlayMethod.DirectStream;
}
- var ranked = rank(ref failureReasons);
+ var ranked = GetRank(ref failureReasons, rankings);
+
return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
})
.OrderByDescending(analysis => analysis.Result.PlayMethod)
@@ -1364,7 +1391,7 @@ namespace MediaBrowser.Model.Dlna
var failureReasons = analyzedProfiles[false]
.Select(analysis => analysis.Result)
- .Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0)
+ .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupported))
.FirstOrDefault().TranscodeReason;
if (failureReasons == 0)
{
@@ -1420,7 +1447,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
/// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
/// <param name="outputContainer">The output container.</param>
- /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
+ /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
/// <returns>The normalized input container.</returns>
public static SubtitleProfile GetSubtitleProfile(
MediaSourceInfo mediaSource,
@@ -1446,7 +1473,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+ if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
{
continue;
}
@@ -1475,7 +1502,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+ if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
{
continue;
}
@@ -1506,17 +1533,12 @@ namespace MediaBrowser.Model.Dlna
{
if (!string.IsNullOrEmpty(transcodingContainer))
{
- string[] normalizedContainers = ContainerProfile.SplitValue(transcodingContainer);
-
- if (ContainerProfile.ContainsContainer(normalizedContainers, "ts")
- || ContainerProfile.ContainsContainer(normalizedContainers, "mpegts")
- || ContainerProfile.ContainsContainer(normalizedContainers, "mp4"))
+ if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4"))
{
return false;
}
- if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv")
- || ContainerProfile.ContainsContainer(normalizedContainers, "matroska"))
+ if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska"))
{
return true;
}
@@ -2219,5 +2241,22 @@ namespace MediaBrowser.Model.Dlna
return false;
}
+
+ private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
+ {
+ var index = 1;
+ foreach (var flag in rankings)
+ {
+ var reason = a & flag;
+ if (reason != 0)
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return index;
+ }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index c8a341d41..1ae4e1962 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -1,9 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -11,1007 +8,1308 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// Class holding information on a stream.
+/// </summary>
+public class StreamInfo
{
/// <summary>
- /// Class StreamInfo.
+ /// Initializes a new instance of the <see cref="StreamInfo"/> class.
/// </summary>
- public class StreamInfo
+ public StreamInfo()
{
- public StreamInfo()
- {
- AudioCodecs = Array.Empty<string>();
- VideoCodecs = Array.Empty<string>();
- SubtitleCodecs = Array.Empty<string>();
- StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
+ AudioCodecs = [];
+ VideoCodecs = [];
+ SubtitleCodecs = [];
+ StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
- public Guid ItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ /// <value>The item id.</value>
+ public Guid ItemId { get; set; }
- public PlayMethod PlayMethod { get; set; }
+ /// <summary>
+ /// Gets or sets the play method.
+ /// </summary>
+ /// <value>The play method.</value>
+ public PlayMethod PlayMethod { get; set; }
- public EncodingContext Context { get; set; }
+ /// <summary>
+ /// Gets or sets the encoding context.
+ /// </summary>
+ /// <value>The encoding context.</value>
+ public EncodingContext Context { get; set; }
- public DlnaProfileType MediaType { get; set; }
+ /// <summary>
+ /// Gets or sets the media type.
+ /// </summary>
+ /// <value>The media type.</value>
+ public DlnaProfileType MediaType { get; set; }
- public string? Container { get; set; }
+ /// <summary>
+ /// Gets or sets the container.
+ /// </summary>
+ /// <value>The container.</value>
+ public string? Container { get; set; }
- public MediaStreamProtocol SubProtocol { get; set; }
+ /// <summary>
+ /// Gets or sets the sub protocol.
+ /// </summary>
+ /// <value>The sub protocol.</value>
+ public MediaStreamProtocol SubProtocol { get; set; }
- public long StartPositionTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the start position ticks.
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ public long StartPositionTicks { get; set; }
- public int? SegmentLength { get; set; }
+ /// <summary>
+ /// Gets or sets the segment length.
+ /// </summary>
+ /// <value>The segment length.</value>
+ public int? SegmentLength { get; set; }
- public int? MinSegments { get; set; }
+ /// <summary>
+ /// Gets or sets the minimum segments count.
+ /// </summary>
+ /// <value>The minimum segments count.</value>
+ public int? MinSegments { get; set; }
- public bool BreakOnNonKeyFrames { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the stream can be broken on non-keyframes.
+ /// </summary>
+ public bool BreakOnNonKeyFrames { get; set; }
- public bool RequireAvc { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the stream requires AVC.
+ /// </summary>
+ public bool RequireAvc { get; set; }
- public bool RequireNonAnamorphic { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the stream requires AVC.
+ /// </summary>
+ public bool RequireNonAnamorphic { get; set; }
- public bool CopyTimestamps { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether timestamps should be copied.
+ /// </summary>
+ public bool CopyTimestamps { get; set; }
- public bool EnableMpegtsM2TsMode { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether timestamps should be copied.
+ /// </summary>
+ public bool EnableMpegtsM2TsMode { get; set; }
- public bool EnableSubtitlesInManifest { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the subtitle manifest is enabled.
+ /// </summary>
+ public bool EnableSubtitlesInManifest { get; set; }
- public string[] AudioCodecs { get; set; }
+ /// <summary>
+ /// Gets or sets the audio codecs.
+ /// </summary>
+ /// <value>The audio codecs.</value>
+ public IReadOnlyList<string> AudioCodecs { get; set; }
- public string[] VideoCodecs { get; set; }
+ /// <summary>
+ /// Gets or sets the video codecs.
+ /// </summary>
+ /// <value>The video codecs.</value>
+ public IReadOnlyList<string> VideoCodecs { get; set; }
- public int? AudioStreamIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the audio stream index.
+ /// </summary>
+ /// <value>The audio stream index.</value>
+ public int? AudioStreamIndex { get; set; }
- public int? SubtitleStreamIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the video stream index.
+ /// </summary>
+ /// <value>The subtitle stream index.</value>
+ public int? SubtitleStreamIndex { get; set; }
- public int? TranscodingMaxAudioChannels { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum transcoding audio channels.
+ /// </summary>
+ /// <value>The maximum transcoding audio channels.</value>
+ public int? TranscodingMaxAudioChannels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the global maximum audio channels.
+ /// </summary>
+ /// <value>The global maximum audio channels.</value>
+ public int? GlobalMaxAudioChannels { get; set; }
- public int? GlobalMaxAudioChannels { get; set; }
+ /// <summary>
+ /// Gets or sets the audio bitrate.
+ /// </summary>
+ /// <value>The audio bitrate.</value>
+ public int? AudioBitrate { get; set; }
- public int? AudioBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the audio sample rate.
+ /// </summary>
+ /// <value>The audio sample rate.</value>
+ public int? AudioSampleRate { get; set; }
- public int? AudioSampleRate { get; set; }
+ /// <summary>
+ /// Gets or sets the video bitrate.
+ /// </summary>
+ /// <value>The video bitrate.</value>
+ public int? VideoBitrate { get; set; }
- public int? VideoBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum output width.
+ /// </summary>
+ /// <value>The output width.</value>
+ public int? MaxWidth { get; set; }
- public int? MaxWidth { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum output height.
+ /// </summary>
+ /// <value>The maximum output height.</value>
+ public int? MaxHeight { get; set; }
- public int? MaxHeight { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum framerate.
+ /// </summary>
+ /// <value>The maximum framerate.</value>
+ public float? MaxFramerate { get; set; }
- public float? MaxFramerate { get; set; }
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
+ /// <value>The device profile.</value>
+ public required DeviceProfile DeviceProfile { get; set; }
- public required DeviceProfile DeviceProfile { get; set; }
+ /// <summary>
+ /// Gets or sets the device profile id.
+ /// </summary>
+ /// <value>The device profile id.</value>
+ public string? DeviceProfileId { get; set; }
- public string? DeviceProfileId { get; set; }
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ /// <value>The device id.</value>
+ public string? DeviceId { get; set; }
- public string? DeviceId { get; set; }
+ /// <summary>
+ /// Gets or sets the runtime ticks.
+ /// </summary>
+ /// <value>The runtime ticks.</value>
+ public long? RunTimeTicks { get; set; }
- public long? RunTimeTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the transcode seek info.
+ /// </summary>
+ /// <value>The transcode seek info.</value>
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
- public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether content length should be estimated.
+ /// </summary>
+ public bool EstimateContentLength { get; set; }
- public bool EstimateContentLength { get; set; }
+ /// <summary>
+ /// Gets or sets the media source info.
+ /// </summary>
+ /// <value>The media source info.</value>
+ public MediaSourceInfo? MediaSource { get; set; }
- public MediaSourceInfo? MediaSource { get; set; }
+ /// <summary>
+ /// Gets or sets the subtitle codecs.
+ /// </summary>
+ /// <value>The subtitle codecs.</value>
+ public IReadOnlyList<string> SubtitleCodecs { get; set; }
- public string[] SubtitleCodecs { get; set; }
+ /// <summary>
+ /// Gets or sets the subtitle delivery method.
+ /// </summary>
+ /// <value>The subtitle delivery method.</value>
+ public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
- public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+ /// <summary>
+ /// Gets or sets the subtitle format.
+ /// </summary>
+ /// <value>The subtitle format.</value>
+ public string? SubtitleFormat { get; set; }
- public string? SubtitleFormat { get; set; }
+ /// <summary>
+ /// Gets or sets the play session id.
+ /// </summary>
+ /// <value>The play session id.</value>
+ public string? PlaySessionId { get; set; }
- public string? PlaySessionId { get; set; }
+ /// <summary>
+ /// Gets or sets the transcode reasons.
+ /// </summary>
+ /// <value>The transcode reasons.</value>
+ public TranscodeReason TranscodeReasons { get; set; }
- public TranscodeReason TranscodeReasons { get; set; }
+ /// <summary>
+ /// Gets the stream options.
+ /// </summary>
+ /// <value>The stream options.</value>
+ public Dictionary<string, string> StreamOptions { get; private set; }
- public Dictionary<string, string> StreamOptions { get; private set; }
+ /// <summary>
+ /// Gets the media source id.
+ /// </summary>
+ /// <value>The media source id.</value>
+ public string? MediaSourceId => MediaSource?.Id;
- public string? MediaSourceId => MediaSource?.Id;
+ /// <summary>
+ /// Gets or sets a value indicating whether audio VBR encoding is enabled.
+ /// </summary>
+ public bool EnableAudioVbrEncoding { get; set; }
- public bool EnableAudioVbrEncoding { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ /// </summary>
+ public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
- public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
- && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
+ /// <summary>
+ /// Gets a value indicating whether the stream is direct.
+ /// </summary>
+ public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
+ && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
- /// <summary>
- /// Gets the audio stream that will be used.
- /// </summary>
- public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
+ /// <summary>
+ /// Gets the audio stream that will be used in the output stream.
+ /// </summary>
+ /// <value>The audio stream.</value>
+ public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
- /// <summary>
- /// Gets the video stream that will be used.
- /// </summary>
- public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
+ /// <summary>
+ /// Gets the video stream that will be used in the output stream.
+ /// </summary>
+ /// <value>The video stream.</value>
+ public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public int? TargetAudioSampleRate
+ /// <summary>
+ /// Gets the audio sample rate that will be in the output stream.
+ /// </summary>
+ /// <value>The target audio sample rate.</value>
+ public int? TargetAudioSampleRate
+ {
+ get
{
- get
- {
- var stream = TargetAudioStream;
- return AudioSampleRate.HasValue && !IsDirectStream
- ? AudioSampleRate
- : stream?.SampleRate;
- }
+ var stream = TargetAudioStream;
+ return AudioSampleRate.HasValue && !IsDirectStream
+ ? AudioSampleRate
+ : stream?.SampleRate;
}
+ }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public int? TargetAudioBitDepth
+ /// <summary>
+ /// Gets the audio bit depth that will be in the output stream.
+ /// </summary>
+ /// <value>The target bit depth.</value>
+ public int? TargetAudioBitDepth
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return TargetAudioStream?.BitDepth;
- }
-
- var targetAudioCodecs = TargetAudioCodec;
- var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
- if (!string.IsNullOrEmpty(audioCodec))
- {
- return GetTargetAudioBitDepth(audioCodec);
- }
-
return TargetAudioStream?.BitDepth;
}
- }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public int? TargetVideoBitDepth
- {
- get
+ var targetAudioCodecs = TargetAudioCodec;
+ var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
+ if (!string.IsNullOrEmpty(audioCodec))
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.BitDepth;
- }
-
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
- {
- return GetTargetVideoBitDepth(videoCodec);
- }
-
- return TargetVideoStream?.BitDepth;
+ return GetTargetAudioBitDepth(audioCodec);
}
+
+ return TargetAudioStream?.BitDepth;
}
+ }
- /// <summary>
- /// Gets the target reference frames.
- /// </summary>
- /// <value>The target reference frames.</value>
- public int? TargetRefFrames
+ /// <summary>
+ /// Gets the video bit depth that will be in the output stream.
+ /// </summary>
+ /// <value>The target video bit depth.</value>
+ public int? TargetVideoBitDepth
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.RefFrames;
- }
-
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
- {
- return GetTargetRefFrames(videoCodec);
- }
+ return TargetVideoStream?.BitDepth;
+ }
- return TargetVideoStream?.RefFrames;
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetTargetVideoBitDepth(videoCodec);
}
+
+ return TargetVideoStream?.BitDepth;
}
+ }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public float? TargetFramerate
+ /// <summary>
+ /// Gets the target reference frames that will be in the output stream.
+ /// </summary>
+ /// <value>The target reference frames.</value>
+ public int? TargetRefFrames
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- var stream = TargetVideoStream;
- return MaxFramerate.HasValue && !IsDirectStream
- ? MaxFramerate
- : stream is null ? null : stream.AverageFrameRate ?? stream.RealFrameRate;
+ return TargetVideoStream?.RefFrames;
}
- }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public double? TargetVideoLevel
- {
- get
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.Level;
- }
+ return GetTargetRefFrames(videoCodec);
+ }
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
- {
- return GetTargetVideoLevel(videoCodec);
- }
+ return TargetVideoStream?.RefFrames;
+ }
+ }
- return TargetVideoStream?.Level;
- }
+ /// <summary>
+ /// Gets the target framerate that will be in the output stream.
+ /// </summary>
+ /// <value>The target framerate.</value>
+ public float? TargetFramerate
+ {
+ get
+ {
+ var stream = TargetVideoStream;
+ return MaxFramerate.HasValue && !IsDirectStream
+ ? MaxFramerate
+ : stream?.ReferenceFrameRate;
}
+ }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public int? TargetPacketLength
+ /// <summary>
+ /// Gets the target video level that will be in the output stream.
+ /// </summary>
+ /// <value>The target video level.</value>
+ public double? TargetVideoLevel
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- var stream = TargetVideoStream;
- return !IsDirectStream
- ? null
- : stream?.PacketLength;
+ return TargetVideoStream?.Level;
}
- }
- /// <summary>
- /// Gets the audio sample rate that will be in the output stream.
- /// </summary>
- public string? TargetVideoProfile
- {
- get
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.Profile;
- }
+ return GetTargetVideoLevel(videoCodec);
+ }
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
- {
- return GetOption(videoCodec, "profile");
- }
+ return TargetVideoStream?.Level;
+ }
+ }
- return TargetVideoStream?.Profile;
- }
+ /// <summary>
+ /// Gets the target packet length that will be in the output stream.
+ /// </summary>
+ /// <value>The target packet length.</value>
+ public int? TargetPacketLength
+ {
+ get
+ {
+ var stream = TargetVideoStream;
+ return !IsDirectStream
+ ? null
+ : stream?.PacketLength;
}
+ }
- /// <summary>
- /// Gets the target video range type that will be in the output stream.
- /// </summary>
- public VideoRangeType TargetVideoRangeType
+ /// <summary>
+ /// Gets the target video profile that will be in the output stream.
+ /// </summary>
+ /// <value>The target video profile.</value>
+ public string? TargetVideoProfile
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
- }
-
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec)
- && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
- {
- return videoRangeType;
- }
+ return TargetVideoStream?.Profile;
+ }
- return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetOption(videoCodec, "profile");
}
+
+ return TargetVideoStream?.Profile;
}
+ }
- /// <summary>
- /// Gets the target video codec tag.
- /// </summary>
- /// <value>The target video codec tag.</value>
- public string? TargetVideoCodecTag
+ /// <summary>
+ /// Gets the target video range type that will be in the output stream.
+ /// </summary>
+ /// <value>The video range type.</value>
+ public VideoRangeType TargetVideoRangeType
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- var stream = TargetVideoStream;
- return !IsDirectStream
- ? null
- : stream?.CodecTag;
+ return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
- }
- /// <summary>
- /// Gets the audio bitrate that will be in the output stream.
- /// </summary>
- public int? TargetAudioBitrate
- {
- get
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec)
+ && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
{
- var stream = TargetAudioStream;
- return AudioBitrate.HasValue && !IsDirectStream
- ? AudioBitrate
- : stream?.BitRate;
+ return videoRangeType;
}
+
+ return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
+ }
- /// <summary>
- /// Gets the audio channels that will be in the output stream.
- /// </summary>
- public int? TargetAudioChannels
+ /// <summary>
+ /// Gets the target video codec tag.
+ /// </summary>
+ /// <value>The video codec tag.</value>
+ public string? TargetVideoCodecTag
+ {
+ get
{
- get
- {
- if (IsDirectStream)
- {
- return TargetAudioStream?.Channels;
- }
+ var stream = TargetVideoStream;
+ return !IsDirectStream
+ ? null
+ : stream?.CodecTag;
+ }
+ }
- var targetAudioCodecs = TargetAudioCodec;
- var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
- if (!string.IsNullOrEmpty(codec))
- {
- return GetTargetRefFrames(codec);
- }
+ /// <summary>
+ /// Gets the audio bitrate that will be in the output stream.
+ /// </summary>
+ /// <value>The audio bitrate.</value>
+ public int? TargetAudioBitrate
+ {
+ get
+ {
+ var stream = TargetAudioStream;
+ return AudioBitrate.HasValue && !IsDirectStream
+ ? AudioBitrate
+ : stream?.BitRate;
+ }
+ }
+ /// <summary>
+ /// Gets the amount of audio channels that will be in the output stream.
+ /// </summary>
+ /// <value>The target audio channels.</value>
+ public int? TargetAudioChannels
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
return TargetAudioStream?.Channels;
}
+
+ var targetAudioCodecs = TargetAudioCodec;
+ var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
+ if (!string.IsNullOrEmpty(codec))
+ {
+ return GetTargetRefFrames(codec);
+ }
+
+ return TargetAudioStream?.Channels;
}
+ }
- /// <summary>
- /// Gets the audio codec that will be in the output stream.
- /// </summary>
- public string[] TargetAudioCodec
+ /// <summary>
+ /// Gets the audio codec that will be in the output stream.
+ /// </summary>
+ /// <value>The audio codec.</value>
+ public IReadOnlyList<string> TargetAudioCodec
+ {
+ get
{
- get
- {
- var stream = TargetAudioStream;
+ var stream = TargetAudioStream;
- string? inputCodec = stream?.Codec;
+ string? inputCodec = stream?.Codec;
- if (IsDirectStream)
- {
- return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
- }
+ if (IsDirectStream)
+ {
+ return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
+ }
- foreach (string codec in AudioCodecs)
+ foreach (string codec in AudioCodecs)
+ {
+ if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
- {
- return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
- }
+ return string.IsNullOrEmpty(codec) ? [] : [codec];
}
-
- return AudioCodecs;
}
+
+ return AudioCodecs;
}
+ }
- public string[] TargetVideoCodec
+ /// <summary>
+ /// Gets the video codec that will be in the output stream.
+ /// </summary>
+ /// <value>The target video codec.</value>
+ public IReadOnlyList<string> TargetVideoCodec
+ {
+ get
{
- get
- {
- var stream = TargetVideoStream;
+ var stream = TargetVideoStream;
- string? inputCodec = stream?.Codec;
+ string? inputCodec = stream?.Codec;
- if (IsDirectStream)
- {
- return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
- }
+ if (IsDirectStream)
+ {
+ return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
+ }
- foreach (string codec in VideoCodecs)
+ foreach (string codec in VideoCodecs)
+ {
+ if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
- {
- return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
- }
+ return string.IsNullOrEmpty(codec) ? [] : [codec];
}
-
- return VideoCodecs;
}
+
+ return VideoCodecs;
}
+ }
- /// <summary>
- /// Gets the audio channels that will be in the output stream.
- /// </summary>
- public long? TargetSize
+ /// <summary>
+ /// Gets the target size of the output stream.
+ /// </summary>
+ /// <value>The target size.</value>
+ public long? TargetSize
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return MediaSource?.Size;
- }
-
- if (RunTimeTicks.HasValue)
- {
- int? totalBitrate = TargetTotalBitrate;
+ return MediaSource?.Size;
+ }
- double totalSeconds = RunTimeTicks.Value;
- // Convert to ms
- totalSeconds /= 10000;
- // Convert to seconds
- totalSeconds /= 1000;
+ if (RunTimeTicks.HasValue)
+ {
+ int? totalBitrate = TargetTotalBitrate;
- return totalBitrate.HasValue ?
- Convert.ToInt64(totalBitrate.Value * totalSeconds) :
- null;
- }
+ double totalSeconds = RunTimeTicks.Value;
+ // Convert to ms
+ totalSeconds /= 10000;
+ // Convert to seconds
+ totalSeconds /= 1000;
- return null;
+ return totalBitrate.HasValue ?
+ Convert.ToInt64(totalBitrate.Value * totalSeconds) :
+ null;
}
+
+ return null;
}
+ }
- public int? TargetVideoBitrate
+ /// <summary>
+ /// Gets the target video bitrate of the output stream.
+ /// </summary>
+ /// <value>The video bitrate.</value>
+ public int? TargetVideoBitrate
+ {
+ get
{
- get
- {
- var stream = TargetVideoStream;
+ var stream = TargetVideoStream;
- return VideoBitrate.HasValue && !IsDirectStream
- ? VideoBitrate
- : stream?.BitRate;
- }
+ return VideoBitrate.HasValue && !IsDirectStream
+ ? VideoBitrate
+ : stream?.BitRate;
}
+ }
- public TransportStreamTimestamp TargetTimestamp
+ /// <summary>
+ /// Gets the target timestamp of the output stream.
+ /// </summary>
+ /// <value>The target timestamp.</value>
+ public TransportStreamTimestamp TargetTimestamp
+ {
+ get
{
- get
- {
- var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
- ? TransportStreamTimestamp.Valid
- : TransportStreamTimestamp.None;
+ var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
+ ? TransportStreamTimestamp.Valid
+ : TransportStreamTimestamp.None;
- return !IsDirectStream
- ? defaultValue
- : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
- }
+ return !IsDirectStream
+ ? defaultValue
+ : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
}
+ }
- public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
+ /// <summary>
+ /// Gets the target total bitrate of the output stream.
+ /// </summary>
+ /// <value>The target total bitrate.</value>
+ public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
- public bool? IsTargetAnamorphic
+ /// <summary>
+ /// Gets a value indicating whether the output stream is anamorphic.
+ /// </summary>
+ public bool? IsTargetAnamorphic
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.IsAnamorphic;
- }
-
- return false;
+ return TargetVideoStream?.IsAnamorphic;
}
+
+ return false;
}
+ }
- public bool? IsTargetInterlaced
+ /// <summary>
+ /// Gets a value indicating whether the output stream is interlaced.
+ /// </summary>
+ public bool? IsTargetInterlaced
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return TargetVideoStream?.IsInterlaced;
- }
-
- var targetVideoCodecs = TargetVideoCodec;
- var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
- {
- if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
- }
-
return TargetVideoStream?.IsInterlaced;
}
- }
- public bool? IsTargetAVC
- {
- get
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
{
- if (IsDirectStream)
+ if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
{
- return TargetVideoStream?.IsAVC;
+ return false;
}
-
- return true;
}
+
+ return TargetVideoStream?.IsInterlaced;
}
+ }
- public int? TargetWidth
+ /// <summary>
+ /// Gets a value indicating whether the output stream is AVC.
+ /// </summary>
+ public bool? IsTargetAVC
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- var videoStream = TargetVideoStream;
-
- if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
- {
- ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
-
- size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
-
- return size.Width;
- }
-
- return MaxWidth;
+ return TargetVideoStream?.IsAVC;
}
+
+ return true;
}
+ }
- public int? TargetHeight
+ /// <summary>
+ /// Gets the target width of the output stream.
+ /// </summary>
+ /// <value>The target width.</value>
+ public int? TargetWidth
+ {
+ get
{
- get
- {
- var videoStream = TargetVideoStream;
-
- if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
- {
- ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
+ var videoStream = TargetVideoStream;
- size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+ if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+ {
+ ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
- return size.Height;
- }
+ size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
- return MaxHeight;
+ return size.Width;
}
+
+ return MaxWidth;
}
+ }
- public int? TargetVideoStreamCount
+ /// <summary>
+ /// Gets the target height of the output stream.
+ /// </summary>
+ /// <value>The target height.</value>
+ public int? TargetHeight
+ {
+ get
{
- get
+ var videoStream = TargetVideoStream;
+
+ if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
{
- if (IsDirectStream)
- {
- return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
- }
+ ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
- return GetMediaStreamCount(MediaStreamType.Video, 1);
+ size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
+
+ return size.Height;
}
+
+ return MaxHeight;
}
+ }
- public int? TargetAudioStreamCount
+ /// <summary>
+ /// Gets the target video stream count of the output stream.
+ /// </summary>
+ /// <value>The target video stream count.</value>
+ public int? TargetVideoStreamCount
+ {
+ get
{
- get
+ if (IsDirectStream)
{
- if (IsDirectStream)
- {
- return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
- }
-
- return GetMediaStreamCount(MediaStreamType.Audio, 1);
+ return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
}
+
+ return GetMediaStreamCount(MediaStreamType.Video, 1);
}
+ }
- public void SetOption(string? qualifier, string name, string value)
+ /// <summary>
+ /// Gets the target audio stream count of the output stream.
+ /// </summary>
+ /// <value>The target audio stream count.</value>
+ public int? TargetAudioStreamCount
+ {
+ get
{
- if (string.IsNullOrEmpty(qualifier))
- {
- SetOption(name, value);
- }
- else
+ if (IsDirectStream)
{
- SetOption(qualifier + "-" + name, value);
+ return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
}
+
+ return GetMediaStreamCount(MediaStreamType.Audio, 1);
}
+ }
- public void SetOption(string name, string value)
+ /// <summary>
+ /// Sets a stream option.
+ /// </summary>
+ /// <param name="qualifier">The qualifier.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="value">The value.</param>
+ public void SetOption(string? qualifier, string name, string value)
+ {
+ if (string.IsNullOrEmpty(qualifier))
{
- StreamOptions[name] = value;
+ SetOption(name, value);
}
-
- public string? GetOption(string? qualifier, string name)
+ else
{
- var value = GetOption(qualifier + "-" + name);
+ SetOption(qualifier + "-" + name, value);
+ }
+ }
- if (string.IsNullOrEmpty(value))
- {
- value = GetOption(name);
- }
+ /// <summary>
+ /// Sets a stream option.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="value">The value.</param>
+ public void SetOption(string name, string value)
+ {
+ StreamOptions[name] = value;
+ }
- return value;
- }
+ /// <summary>
+ /// Gets a stream option.
+ /// </summary>
+ /// <param name="qualifier">The qualifier.</param>
+ /// <param name="name">The name.</param>
+ /// <returns>The value.</returns>
+ public string? GetOption(string? qualifier, string name)
+ {
+ var value = GetOption(qualifier + "-" + name);
- public string? GetOption(string name)
+ if (string.IsNullOrEmpty(value))
{
- if (StreamOptions.TryGetValue(name, out var value))
- {
- return value;
- }
-
- return null;
+ value = GetOption(name);
}
- public string ToUrl(string baseUrl, string? accessToken)
+ return value;
+ }
+
+ /// <summary>
+ /// Gets a stream option.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>The value.</returns>
+ public string? GetOption(string name)
+ {
+ if (StreamOptions.TryGetValue(name, out var value))
{
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ return value;
+ }
- var list = new List<string>();
- foreach (NameValuePair pair in BuildParams(this, accessToken))
- {
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
+ return null;
+ }
- // Try to keep the url clean by omitting defaults
- if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ /// <summary>
+ /// Returns this output stream URL for this class.
+ /// </summary>
+ /// <param name="baseUrl">The base Url.</param>
+ /// <param name="accessToken">The access Token.</param>
+ /// <returns>A querystring representation of this object.</returns>
+ public string ToUrl(string baseUrl, string? accessToken)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(baseUrl);
- if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ List<string> list = [];
+ foreach (NameValuePair pair in BuildParams(this, accessToken))
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
- if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ // Try to keep the url clean by omitting defaults
+ if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
+ if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+ if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
}
- string queryString = string.Join('&', list);
+ var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
- return GetUrl(baseUrl, queryString);
+ list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
}
- private string GetUrl(string baseUrl, string queryString)
- {
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ string queryString = string.Join('&', list);
- string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+ return GetUrl(baseUrl, queryString);
+ }
- baseUrl = baseUrl.TrimEnd('/');
+ private string GetUrl(string baseUrl, string queryString)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(baseUrl);
- if (MediaType == DlnaProfileType.Audio)
- {
- if (SubProtocol == MediaStreamProtocol.hls)
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
- }
+ string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
- }
+ baseUrl = baseUrl.TrimEnd('/');
+ if (MediaType == DlnaProfileType.Audio)
+ {
if (SubProtocol == MediaStreamProtocol.hls)
{
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
- private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
+ if (SubProtocol == MediaStreamProtocol.hls)
{
- var list = new List<NameValuePair>();
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
- string audioCodecs = item.AudioCodecs.Length == 0 ?
- string.Empty :
- string.Join(',', item.AudioCodecs);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
- string videoCodecs = item.VideoCodecs.Length == 0 ?
- string.Empty :
- string.Join(',', item.VideoCodecs);
+ private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
+ {
+ List<NameValuePair> list = [];
+
+ string audioCodecs = item.AudioCodecs.Count == 0 ?
+ string.Empty :
+ string.Join(',', item.AudioCodecs);
+
+ string videoCodecs = item.VideoCodecs.Count == 0 ?
+ string.Empty :
+ string.Join(',', item.VideoCodecs);
+
+ list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+ list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+ list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+ list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ list.Add(new NameValuePair("VideoCodec", videoCodecs));
+ list.Add(new NameValuePair("AudioCodec", audioCodecs));
+ list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ long startPositionTicks = item.StartPositionTicks;
+
+ if (item.SubProtocol == MediaStreamProtocol.hls)
+ {
+ list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ }
+ else
+ {
+ list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ }
- list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
- list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
- list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
- list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- list.Add(new NameValuePair("VideoCodec", videoCodecs));
- list.Add(new NameValuePair("AudioCodec", audioCodecs));
- list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+ list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
- list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ string? liveStreamId = item.MediaSource?.LiveStreamId;
+ list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
- long startPositionTicks = item.StartPositionTicks;
+ list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
- if (item.SubProtocol == MediaStreamProtocol.hls)
- {
- list.Add(new NameValuePair("StartTimeTicks", string.Empty));
- }
- else
+ if (!item.IsDirectStream)
+ {
+ if (item.RequireNonAnamorphic)
{
- list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
}
- list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
- list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
-
- string? liveStreamId = item.MediaSource?.LiveStreamId;
- list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+ list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
-
- if (!item.IsDirectStream)
+ if (item.EnableSubtitlesInManifest)
{
- if (item.RequireNonAnamorphic)
- {
- list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
- if (item.EnableSubtitlesInManifest)
- {
- list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- if (item.EnableMpegtsM2TsMode)
- {
- list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- if (item.EstimateContentLength)
- {
- list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
+ list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
- if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
- {
- list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
- }
+ if (item.EnableMpegtsM2TsMode)
+ {
+ list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
- if (item.CopyTimestamps)
- {
- list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
+ if (item.EstimateContentLength)
+ {
+ list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
- list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ {
+ list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+ }
- list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ if (item.CopyTimestamps)
+ {
+ list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
}
- list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
+ list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
- string.Empty :
- string.Join(",", item.SubtitleCodecs);
+ list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
- list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+ list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
- if (item.SubProtocol == MediaStreamProtocol.hls)
- {
- list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+ string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
+ string.Empty :
+ string.Join(",", item.SubtitleCodecs);
- if (item.SegmentLength.HasValue)
- {
- list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
- if (item.MinSegments.HasValue)
- {
- list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ if (item.SubProtocol == MediaStreamProtocol.hls)
+ {
+ list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
- list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ if (item.SegmentLength.HasValue)
+ {
+ list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
}
- foreach (var pair in item.StreamOptions)
+ if (item.MinSegments.HasValue)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
-
- // strip spaces to avoid having to encode h264 profile names
- list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+ list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
}
- if (!item.IsDirectStream)
+ list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ foreach (var pair in item.StreamOptions)
+ {
+ if (string.IsNullOrEmpty(pair.Value))
{
- list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
+ continue;
}
- return list;
+ // strip spaces to avoid having to encode h264 profile names
+ list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
}
- public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
+ if (!item.IsDirectStream)
{
- return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
}
- public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the subtitle profiles.
+ /// </summary>
+ /// <param name="transcoderSupport">The transcoder support.</param>
+ /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
+ /// <param name="baseUrl">The base URL.</param>
+ /// <param name="accessToken">The access token.</param>
+ /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
+ {
+ return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ }
+
+ /// <summary>
+ /// Gets the subtitle profiles.
+ /// </summary>
+ /// <param name="transcoderSupport">The transcoder support.</param>
+ /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
+ /// <param name="enableAllProfiles">If all profiles are enabled.</param>
+ /// <param name="baseUrl">The base URL.</param>
+ /// <param name="accessToken">The access token.</param>
+ /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
+ {
+ if (MediaSource is null)
{
- if (MediaSource is null)
- {
- return Enumerable.Empty<SubtitleStreamInfo>();
- }
+ return [];
+ }
- var list = new List<SubtitleStreamInfo>();
+ List<SubtitleStreamInfo> list = [];
- // HLS will preserve timestamps so we can just grab the full subtitle stream
- long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
- ? 0
- : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+ // HLS will preserve timestamps so we can just grab the full subtitle stream
+ long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
+ ? 0
+ : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
- // First add the selected track
- if (SubtitleStreamIndex.HasValue)
+ // First add the selected track
+ if (SubtitleStreamIndex.HasValue)
+ {
+ foreach (var stream in MediaSource.MediaStreams)
{
- foreach (var stream in MediaSource.MediaStreams)
+ if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
{
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
- {
- AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
- }
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
}
}
+ }
- if (!includeSelectedTrackOnly)
+ if (!includeSelectedTrackOnly)
+ {
+ foreach (var stream in MediaSource.MediaStreams)
{
- foreach (var stream in MediaSource.MediaStreams)
+ if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
{
- if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
- {
- AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
- }
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
}
}
-
- return list;
}
- private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
+ return list;
+ }
+
+ private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
+ {
+ if (enableAllProfiles)
{
- if (enableAllProfiles)
+ foreach (var profile in DeviceProfile.SubtitleProfiles)
{
- foreach (var profile in DeviceProfile.SubtitleProfiles)
- {
- var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
- if (info is not null)
- {
- list.Add(info);
- }
- }
- }
- else
- {
- var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+ var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
if (info is not null)
{
list.Add(info);
}
}
}
-
- private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+ else
{
- if (MediaSource is null)
+ var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+ if (info is not null)
{
- return null;
+ list.Add(info);
}
+ }
+ }
- var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
- var info = new SubtitleStreamInfo
- {
- IsForced = stream.IsForced,
- Language = stream.Language,
- Name = stream.Language ?? "Unknown",
- Format = subtitleProfile.Format,
- Index = stream.Index,
- DeliveryMethod = subtitleProfile.Method,
- DisplayTitle = stream.DisplayTitle
- };
+ private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+ {
+ if (MediaSource is null)
+ {
+ return null;
+ }
- if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+ var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
+ var info = new SubtitleStreamInfo
+ {
+ IsForced = stream.IsForced,
+ Language = stream.Language,
+ Name = stream.Language ?? "Unknown",
+ Format = subtitleProfile.Format,
+ Index = stream.Index,
+ DeliveryMethod = subtitleProfile.Method,
+ DisplayTitle = stream.DisplayTitle
+ };
+
+ if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
{
- if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
+ info.Url = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+ baseUrl,
+ ItemId,
+ MediaSourceId,
+ stream.Index.ToString(CultureInfo.InvariantCulture),
+ startPositionTicks.ToString(CultureInfo.InvariantCulture),
+ subtitleProfile.Format);
+
+ if (!string.IsNullOrEmpty(accessToken))
{
- info.Url = string.Format(
- CultureInfo.InvariantCulture,
- "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
- baseUrl,
- ItemId,
- MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- startPositionTicks.ToString(CultureInfo.InvariantCulture),
- subtitleProfile.Format);
-
- if (!string.IsNullOrEmpty(accessToken))
- {
- info.Url += "?api_key=" + accessToken;
- }
-
- info.IsExternalUrl = false;
+ info.Url += "?api_key=" + accessToken;
}
- else
- {
- info.Url = stream.Path;
- info.IsExternalUrl = true;
- }
- }
-
- return info;
- }
-
- public int? GetTargetVideoBitDepth(string? codec)
- {
- var value = GetOption(codec, "videobitdepth");
- if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+ info.IsExternalUrl = false;
+ }
+ else
{
- return result;
+ info.Url = stream.Path;
+ info.IsExternalUrl = true;
}
-
- return null;
}
- public int? GetTargetAudioBitDepth(string? codec)
- {
- var value = GetOption(codec, "audiobitdepth");
+ return info;
+ }
- if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
+ /// <summary>
+ /// Gets the target video bit depth.
+ /// </summary>
+ /// <param name="codec">The codec.</param>
+ /// <returns>The target video bit depth.</returns>
+ public int? GetTargetVideoBitDepth(string? codec)
+ {
+ var value = GetOption(codec, "videobitdepth");
- return null;
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
}
- public double? GetTargetVideoLevel(string? codec)
- {
- var value = GetOption(codec, "level");
+ return null;
+ }
- if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
+ /// <summary>
+ /// Gets the target audio bit depth.
+ /// </summary>
+ /// <param name="codec">The codec.</param>
+ /// <returns>The target audio bit depth.</returns>
+ public int? GetTargetAudioBitDepth(string? codec)
+ {
+ var value = GetOption(codec, "audiobitdepth");
- return null;
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
}
- public int? GetTargetRefFrames(string? codec)
- {
- var value = GetOption(codec, "maxrefframes");
+ return null;
+ }
- if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
+ /// <summary>
+ /// Gets the target video level.
+ /// </summary>
+ /// <param name="codec">The codec.</param>
+ /// <returns>The target video level.</returns>
+ public double? GetTargetVideoLevel(string? codec)
+ {
+ var value = GetOption(codec, "level");
- return null;
+ if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
}
- public int? GetTargetAudioChannels(string? codec)
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the target reference frames.
+ /// </summary>
+ /// <param name="codec">The codec.</param>
+ /// <returns>The target reference frames.</returns>
+ public int? GetTargetRefFrames(string? codec)
+ {
+ var value = GetOption(codec, "maxrefframes");
+
+ if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
{
- var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
+ return result;
+ }
- var value = GetOption(codec, "audiochannels");
- if (string.IsNullOrEmpty(value))
- {
- return defaultValue;
- }
+ return null;
+ }
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return Math.Min(result, defaultValue ?? result);
- }
+ /// <summary>
+ /// Gets the target audio channels.
+ /// </summary>
+ /// <param name="codec">The codec.</param>
+ /// <returns>The target audio channels.</returns>
+ public int? GetTargetAudioChannels(string? codec)
+ {
+ var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
+ var value = GetOption(codec, "audiochannels");
+ if (string.IsNullOrEmpty(value))
+ {
return defaultValue;
}
- private int? GetMediaStreamCount(MediaStreamType type, int limit)
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
- var count = MediaSource?.GetStreamCount(type);
+ return Math.Min(result, defaultValue ?? result);
+ }
- if (count.HasValue)
- {
- count = Math.Min(count.Value, limit);
- }
+ return defaultValue;
+ }
+
+ /// <summary>
+ /// Gets the media stream count.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="limit">The limit.</param>
+ /// <returns>The media stream count.</returns>
+ private int? GetMediaStreamCount(MediaStreamType type, int limit)
+ {
+ var count = MediaSource?.GetStreamCount(type);
- return count;
+ if (count.HasValue)
+ {
+ count = Math.Min(count.Value, limit);
}
+
+ return count;
}
}
diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
index 9ebde25ff..1879f2dd2 100644
--- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs
+++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
@@ -1,48 +1,62 @@
#nullable disable
-#pragma warning disable CS1591
-using System;
using System.Xml.Serialization;
-using Jellyfin.Extensions;
+using MediaBrowser.Model.Extensions;
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A class for subtitle profile information.
+/// </summary>
+public class SubtitleProfile
{
- public class SubtitleProfile
+ /// <summary>
+ /// Gets or sets the format.
+ /// </summary>
+ [XmlAttribute("format")]
+ public string Format { get; set; }
+
+ /// <summary>
+ /// Gets or sets the delivery method.
+ /// </summary>
+ [XmlAttribute("method")]
+ public SubtitleDeliveryMethod Method { get; set; }
+
+ /// <summary>
+ /// Gets or sets the DIDL mode.
+ /// </summary>
+ [XmlAttribute("didlMode")]
+ public string DidlMode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ [XmlAttribute("language")]
+ public string Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the container.
+ /// </summary>
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ /// <summary>
+ /// Checks if a language is supported.
+ /// </summary>
+ /// <param name="subLanguage">The language to check for support.</param>
+ /// <returns><c>true</c> if supported.</returns>
+ public bool SupportsLanguage(string subLanguage)
{
- [XmlAttribute("format")]
- public string Format { get; set; }
-
- [XmlAttribute("method")]
- public SubtitleDeliveryMethod Method { get; set; }
-
- [XmlAttribute("didlMode")]
- public string DidlMode { get; set; }
-
- [XmlAttribute("language")]
- public string Language { get; set; }
-
- [XmlAttribute("container")]
- public string Container { get; set; }
-
- public string[] GetLanguages()
+ if (string.IsNullOrEmpty(Language))
{
- return ContainerProfile.SplitValue(Language);
+ return true;
}
- public bool SupportsLanguage(string subLanguage)
+ if (string.IsNullOrEmpty(subLanguage))
{
- if (string.IsNullOrEmpty(Language))
- {
- return true;
- }
-
- if (string.IsNullOrEmpty(subLanguage))
- {
- subLanguage = "und";
- }
-
- var languages = GetLanguages();
- return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase);
+ subLanguage = "und";
}
+
+ return ContainerHelper.ContainsContainer(Language, subLanguage);
}
}
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index a556799de..5a9fa22ae 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,82 +1,130 @@
-#pragma warning disable CS1591
-
-using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
-namespace MediaBrowser.Model.Dlna
+namespace MediaBrowser.Model.Dlna;
+
+/// <summary>
+/// A class for transcoding profile information.
+/// </summary>
+public class TranscodingProfile
{
- public class TranscodingProfile
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingProfile" /> class.
+ /// </summary>
+ public TranscodingProfile()
{
- public TranscodingProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- }
-
- [XmlAttribute("container")]
- public string Container { get; set; } = string.Empty;
-
- [XmlAttribute("type")]
- public DlnaProfileType Type { get; set; }
-
- [XmlAttribute("videoCodec")]
- public string VideoCodec { get; set; } = string.Empty;
-
- [XmlAttribute("audioCodec")]
- public string AudioCodec { get; set; } = string.Empty;
-
- [XmlAttribute("protocol")]
- public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
-
- [DefaultValue(false)]
- [XmlAttribute("estimateContentLength")]
- public bool EstimateContentLength { get; set; }
-
- [DefaultValue(false)]
- [XmlAttribute("enableMpegtsM2TsMode")]
- public bool EnableMpegtsM2TsMode { get; set; }
-
- [DefaultValue(TranscodeSeekInfo.Auto)]
- [XmlAttribute("transcodeSeekInfo")]
- public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
-
- [DefaultValue(false)]
- [XmlAttribute("copyTimestamps")]
- public bool CopyTimestamps { get; set; }
-
- [DefaultValue(EncodingContext.Streaming)]
- [XmlAttribute("context")]
- public EncodingContext Context { get; set; }
-
- [DefaultValue(false)]
- [XmlAttribute("enableSubtitlesInManifest")]
- public bool EnableSubtitlesInManifest { get; set; }
-
- [XmlAttribute("maxAudioChannels")]
- public string? MaxAudioChannels { get; set; }
-
- [DefaultValue(0)]
- [XmlAttribute("minSegments")]
- public int MinSegments { get; set; }
-
- [DefaultValue(0)]
- [XmlAttribute("segmentLength")]
- public int SegmentLength { get; set; }
-
- [DefaultValue(false)]
- [XmlAttribute("breakOnNonKeyFrames")]
- public bool BreakOnNonKeyFrames { get; set; }
-
- public ProfileCondition[] Conditions { get; set; }
-
- [DefaultValue(true)]
- [XmlAttribute("enableAudioVbrEncoding")]
- public bool EnableAudioVbrEncoding { get; set; } = true;
-
- public string[] GetAudioCodecs()
- {
- return ContainerProfile.SplitValue(AudioCodec);
- }
+ Conditions = [];
}
+
+ /// <summary>
+ /// Gets or sets the container.
+ /// </summary>
+ [XmlAttribute("container")]
+ public string Container { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the DLNA profile type.
+ /// </summary>
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video codec.
+ /// </summary>
+ [XmlAttribute("videoCodec")]
+ public string VideoCodec { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the audio codec.
+ /// </summary>
+ [XmlAttribute("audioCodec")]
+ public string AudioCodec { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the protocol.
+ /// </summary>
+ [XmlAttribute("protocol")]
+ public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the content length should be estimated.
+ /// </summary>
+ [DefaultValue(false)]
+ [XmlAttribute("estimateContentLength")]
+ public bool EstimateContentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether M2TS mode is enabled.
+ /// </summary>
+ [DefaultValue(false)]
+ [XmlAttribute("enableMpegtsM2TsMode")]
+ public bool EnableMpegtsM2TsMode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transcoding seek info mode.
+ /// </summary>
+ [DefaultValue(TranscodeSeekInfo.Auto)]
+ [XmlAttribute("transcodeSeekInfo")]
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether timestamps should be copied.
+ /// </summary>
+ [DefaultValue(false)]
+ [XmlAttribute("copyTimestamps")]
+ public bool CopyTimestamps { get; set; }
+
+ /// <summary>
+ /// Gets or sets the encoding context.
+ /// </summary>
+ [DefaultValue(EncodingContext.Streaming)]
+ [XmlAttribute("context")]
+ public EncodingContext Context { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether subtitles are allowed in the manifest.
+ /// </summary>
+ [DefaultValue(false)]
+ [XmlAttribute("enableSubtitlesInManifest")]
+ public bool EnableSubtitlesInManifest { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum audio channels.
+ /// </summary>
+ [XmlAttribute("maxAudioChannels")]
+ public string? MaxAudioChannels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum amount of segments.
+ /// </summary>
+ [DefaultValue(0)]
+ [XmlAttribute("minSegments")]
+ public int MinSegments { get; set; }
+
+ /// <summary>
+ /// Gets or sets the segment length.
+ /// </summary>
+ [DefaultValue(0)]
+ [XmlAttribute("segmentLength")]
+ public int SegmentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported.
+ /// </summary>
+ [DefaultValue(false)]
+ [XmlAttribute("breakOnNonKeyFrames")]
+ public bool BreakOnNonKeyFrames { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile conditions.
+ /// </summary>
+ public ProfileCondition[] Conditions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether variable bitrate encoding is supported.
+ /// </summary>
+ [DefaultValue(true)]
+ [XmlAttribute("enableAudioVbrEncoding")]
+ public bool EnableAudioVbrEncoding { get; set; } = true;
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs
index c699c469d..5963ed270 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs
@@ -1,13 +1,11 @@
-using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
-namespace Jellyfin.Api.Models.SessionDtos;
+namespace MediaBrowser.Model.Dto;
/// <summary>
/// Client capabilities dto.
@@ -18,13 +16,13 @@ public class ClientCapabilitiesDto
/// Gets or sets the list of playable media types.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = Array.Empty<MediaType>();
+ public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
/// <summary>
/// Gets or sets the list of supported commands.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
+ public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether session supports media control.
@@ -51,18 +49,6 @@ public class ClientCapabilitiesDto
/// </summary>
public string? IconUrl { get; set; }
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsContentUploading { get; set; } = false;
-
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsSync { get; set; } = false;
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-
/// <summary>
/// Convert the dto to the full <see cref="ClientCapabilities"/> model.
/// </summary>
diff --git a/MediaBrowser.Model/Dto/DeviceInfoDto.cs b/MediaBrowser.Model/Dto/DeviceInfoDto.cs
new file mode 100644
index 000000000..ac7a731a9
--- /dev/null
+++ b/MediaBrowser.Model/Dto/DeviceInfoDto.cs
@@ -0,0 +1,83 @@
+using System;
+
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// A DTO representing device information.
+/// </summary>
+public class DeviceInfoDto
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceInfoDto"/> class.
+ /// </summary>
+ public DeviceInfoDto()
+ {
+ Capabilities = new ClientCapabilitiesDto();
+ }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ /// <value>The custom name.</value>
+ public string? CustomName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string? AccessToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string? Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last name of the user.
+ /// </summary>
+ /// <value>The last name of the user.</value>
+ public string? LastUserName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the application.
+ /// </summary>
+ /// <value>The name of the application.</value>
+ public string? AppName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string? AppVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last user identifier.
+ /// </summary>
+ /// <value>The last user identifier.</value>
+ public Guid? LastUserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <value>The date last modified.</value>
+ public DateTime? DateLastActivity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the capabilities.
+ /// </summary>
+ /// <value>The capabilities.</value>
+ public ClientCapabilitiesDto Capabilities { get; set; }
+
+ /// <summary>
+ /// Gets or sets the icon URL.
+ /// </summary>
+ /// <value>The icon URL.</value>
+ public string? IconUrl { get; set; }
+}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 1c6037325..eff2e09da 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
@@ -24,6 +25,7 @@ namespace MediaBrowser.Model.Dto
SupportsDirectStream = true;
SupportsDirectPlay = true;
SupportsProbing = true;
+ UseMostCompatibleTranscodingProfile = false;
}
public MediaProtocol Protocol { get; set; }
@@ -70,6 +72,9 @@ namespace MediaBrowser.Model.Dto
public bool IsInfiniteStream { get; set; }
+ [DefaultValue(false)]
+ public bool UseMostCompatibleTranscodingProfile { get; set; }
+
public bool RequiresOpening { get; set; }
public string OpenToken { get; set; }
@@ -98,6 +103,8 @@ namespace MediaBrowser.Model.Dto
public int? Bitrate { get; set; }
+ public int? FallbackMaxStreamingBitrate { get; set; }
+
public TransportStreamTimestamp? Timestamp { get; set; }
public Dictionary<string, string> RequiredHttpHeaders { get; set; }
diff --git a/MediaBrowser.Model/Dto/PlaylistDto.cs b/MediaBrowser.Model/Dto/PlaylistDto.cs
new file mode 100644
index 000000000..d4de75a78
--- /dev/null
+++ b/MediaBrowser.Model/Dto/PlaylistDto.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// DTO for playlists.
+/// </summary>
+public class PlaylistDto
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is publicly readable.
+ /// </summary>
+ public bool OpenAccess { get; set; }
+
+ /// <summary>
+ /// Gets or sets the share permissions.
+ /// </summary>
+ public required IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item ids.
+ /// </summary>
+ public required IReadOnlyList<Guid> ItemIds { get; set; }
+}
diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs
new file mode 100644
index 000000000..2496c933a
--- /dev/null
+++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// Session info DTO.
+/// </summary>
+public class SessionInfoDto
+{
+ /// <summary>
+ /// Gets or sets the play state.
+ /// </summary>
+ /// <value>The play state.</value>
+ public PlayerStateInfo? PlayState { get; set; }
+
+ /// <summary>
+ /// Gets or sets the additional users.
+ /// </summary>
+ /// <value>The additional users.</value>
+ public IReadOnlyList<SessionUserInfo>? AdditionalUsers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client capabilities.
+ /// </summary>
+ /// <value>The client capabilities.</value>
+ public ClientCapabilitiesDto? Capabilities { get; set; }
+
+ /// <summary>
+ /// Gets or sets the remote end point.
+ /// </summary>
+ /// <value>The remote end point.</value>
+ public string? RemoteEndPoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playable media types.
+ /// </summary>
+ /// <value>The playable media types.</value>
+ public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string? Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the username.
+ /// </summary>
+ /// <value>The username.</value>
+ public string? UserName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the client.
+ /// </summary>
+ /// <value>The type of the client.</value>
+ public string? Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime LastActivityDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last playback check in.
+ /// </summary>
+ /// <value>The last playback check in.</value>
+ public DateTime LastPlaybackCheckIn { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last paused date.
+ /// </summary>
+ /// <value>The last paused date.</value>
+ public DateTime? LastPausedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the device.
+ /// </summary>
+ /// <value>The name of the device.</value>
+ public string? DeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the device.
+ /// </summary>
+ /// <value>The type of the device.</value>
+ public string? DeviceType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing item.
+ /// </summary>
+ /// <value>The now playing item.</value>
+ public BaseItemDto? NowPlayingItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now viewing item.
+ /// </summary>
+ /// <value>The now viewing item.</value>
+ public BaseItemDto? NowViewingItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ /// <value>The device id.</value>
+ public string? DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string? ApplicationVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transcoding info.
+ /// </summary>
+ /// <value>The transcoding info.</value>
+ public TranscodingInfo? TranscodingInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this session is active.
+ /// </summary>
+ /// <value><c>true</c> if this session is active; otherwise, <c>false</c>.</value>
+ public bool IsActive { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the session supports media control.
+ /// </summary>
+ /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value>
+ public bool SupportsMediaControl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the session supports remote control.
+ /// </summary>
+ /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value>
+ public bool SupportsRemoteControl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing queue.
+ /// </summary>
+ /// <value>The now playing queue.</value>
+ public IReadOnlyList<QueueItem>? NowPlayingQueue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing queue full items.
+ /// </summary>
+ /// <value>The now playing queue full items.</value>
+ public IReadOnlyList<BaseItemDto>? NowPlayingQueueFullItems { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the session has a custom device name.
+ /// </summary>
+ /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
+ public bool HasCustomDeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist item id.
+ /// </summary>
+ /// <value>The splaylist item id.</value>
+ public string? PlaylistItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ /// <value>The server id.</value>
+ public string? ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user primary image tag.
+ /// </summary>
+ /// <value>The user primary image tag.</value>
+ public string? UserPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the supported commands.
+ /// </summary>
+ /// <value>The supported commands.</value>
+ public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
+}
diff --git a/MediaBrowser.Model/Entities/DeinterlaceMethod.cs b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs
new file mode 100644
index 000000000..d05aac433
--- /dev/null
+++ b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs
@@ -0,0 +1,19 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing deinterlace methods.
+/// </summary>
+public enum DeinterlaceMethod
+{
+ /// <summary>
+ /// YADIF.
+ /// </summary>
+ yadif = 0,
+
+ /// <summary>
+ /// BWDIF.
+ /// </summary>
+ bwdif = 1
+}
diff --git a/MediaBrowser.Model/Entities/EncoderPreset.cs b/MediaBrowser.Model/Entities/EncoderPreset.cs
new file mode 100644
index 000000000..74c071433
--- /dev/null
+++ b/MediaBrowser.Model/Entities/EncoderPreset.cs
@@ -0,0 +1,64 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing encoder presets.
+/// </summary>
+public enum EncoderPreset
+{
+ /// <summary>
+ /// Auto preset.
+ /// </summary>
+ auto = 0,
+
+ /// <summary>
+ /// Placebo preset.
+ /// </summary>
+ placebo = 1,
+
+ /// <summary>
+ /// Veryslow preset.
+ /// </summary>
+ veryslow = 2,
+
+ /// <summary>
+ /// Slower preset.
+ /// </summary>
+ slower = 3,
+
+ /// <summary>
+ /// Slow preset.
+ /// </summary>
+ slow = 4,
+
+ /// <summary>
+ /// Medium preset.
+ /// </summary>
+ medium = 5,
+
+ /// <summary>
+ /// Fast preset.
+ /// </summary>
+ fast = 6,
+
+ /// <summary>
+ /// Faster preset.
+ /// </summary>
+ faster = 7,
+
+ /// <summary>
+ /// Veryfast preset.
+ /// </summary>
+ veryfast = 8,
+
+ /// <summary>
+ /// Superfast preset.
+ /// </summary>
+ superfast = 9,
+
+ /// <summary>
+ /// Ultrafast preset.
+ /// </summary>
+ ultrafast = 10
+}
diff --git a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs
new file mode 100644
index 000000000..198a2e00f
--- /dev/null
+++ b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs
@@ -0,0 +1,49 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing hardware acceleration types.
+/// </summary>
+public enum HardwareAccelerationType
+{
+ /// <summary>
+ /// Software accelleration.
+ /// </summary>
+ none = 0,
+
+ /// <summary>
+ /// AMD AMF.
+ /// </summary>
+ amf = 1,
+
+ /// <summary>
+ /// Intel Quick Sync Video.
+ /// </summary>
+ qsv = 2,
+
+ /// <summary>
+ /// NVIDIA NVENC.
+ /// </summary>
+ nvenc = 3,
+
+ /// <summary>
+ /// Video4Linux2 V4L2M2M.
+ /// </summary>
+ v4l2m2m = 4,
+
+ /// <summary>
+ /// Video Acceleration API (VAAPI).
+ /// </summary>
+ vaapi = 5,
+
+ /// <summary>
+ /// Video ToolBox.
+ /// </summary>
+ videotoolbox = 6,
+
+ /// <summary>
+ /// Rockchip Media Process Platform (RKMPP).
+ /// </summary>
+ rkmpp = 7
+}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index a0e8c39be..85c1f797b 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -200,7 +200,8 @@ namespace MediaBrowser.Model.Entities
|| dvProfile == 5
|| dvProfile == 7
|| dvProfile == 8
- || dvProfile == 9))
+ || dvProfile == 9
+ || dvProfile == 10))
{
var title = "Dolby Vision Profile " + dvProfile;
@@ -526,6 +527,23 @@ namespace MediaBrowser.Model.Entities
public float? RealFrameRate { get; set; }
/// <summary>
+ /// Gets the framerate used as reference.
+ /// Prefer AverageFrameRate, if that is null or an unrealistic value
+ /// then fallback to RealFrameRate.
+ /// </summary>
+ /// <value>The reference frame rate.</value>
+ public float? ReferenceFrameRate
+ {
+ get
+ {
+ // In some cases AverageFrameRate for videos will be read as 1000fps even if it is not.
+ // This is probably due to a library compatability issue.
+ // See https://github.com/jellyfin/jellyfin/pull/12603#discussion_r1748044018 for more info.
+ return AverageFrameRate < 1000 ? AverageFrameRate : RealFrameRate;
+ }
+ }
+
+ /// <summary>
/// Gets or sets the profile.
/// </summary>
/// <value>The profile.</value>
@@ -760,7 +778,7 @@ namespace MediaBrowser.Model.Entities
var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId;
- var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
+ var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10;
var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6);
if ((isDoViProfile && isDoViFlag)
@@ -783,6 +801,17 @@ namespace MediaBrowser.Model.Entities
_ => (VideoRange.SDR, VideoRangeType.SDR)
},
7 => (VideoRange.HDR, VideoRangeType.HDR10),
+ 10 => dvBlCompatId switch
+ {
+ 0 => (VideoRange.HDR, VideoRangeType.DOVI),
+ 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
+ 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
+ 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
+ // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist.
+ 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
+ // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
+ _ => (VideoRange.SDR, VideoRangeType.SDR)
+ },
_ => (VideoRange.SDR, VideoRangeType.SDR)
};
}
diff --git a/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs
new file mode 100644
index 000000000..488006e0b
--- /dev/null
+++ b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs
@@ -0,0 +1,49 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing tonemapping algorithms.
+/// </summary>
+public enum TonemappingAlgorithm
+{
+ /// <summary>
+ /// None.
+ /// </summary>
+ none = 0,
+
+ /// <summary>
+ /// Clip.
+ /// </summary>
+ clip = 1,
+
+ /// <summary>
+ /// Linear.
+ /// </summary>
+ linear = 2,
+
+ /// <summary>
+ /// Gamma.
+ /// </summary>
+ gamma = 3,
+
+ /// <summary>
+ /// Reinhard.
+ /// </summary>
+ reinhard = 4,
+
+ /// <summary>
+ /// Hable.
+ /// </summary>
+ hable = 5,
+
+ /// <summary>
+ /// Mobius.
+ /// </summary>
+ mobius = 6,
+
+ /// <summary>
+ /// BT2390.
+ /// </summary>
+ bt2390 = 7
+}
diff --git a/MediaBrowser.Model/Entities/TonemappingMode.cs b/MediaBrowser.Model/Entities/TonemappingMode.cs
new file mode 100644
index 000000000..e10a0b4ad
--- /dev/null
+++ b/MediaBrowser.Model/Entities/TonemappingMode.cs
@@ -0,0 +1,34 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing tonemapping modes.
+/// </summary>
+public enum TonemappingMode
+{
+ /// <summary>
+ /// Auto.
+ /// </summary>
+ auto = 0,
+
+ /// <summary>
+ /// Max.
+ /// </summary>
+ max = 1,
+
+ /// <summary>
+ /// RGB.
+ /// </summary>
+ rgb = 2,
+
+ /// <summary>
+ /// Lum.
+ /// </summary>
+ lum = 3,
+
+ /// <summary>
+ /// ITP.
+ /// </summary>
+ itp = 4
+}
diff --git a/MediaBrowser.Model/Entities/TonemappingRange.cs b/MediaBrowser.Model/Entities/TonemappingRange.cs
new file mode 100644
index 000000000..b1446b81c
--- /dev/null
+++ b/MediaBrowser.Model/Entities/TonemappingRange.cs
@@ -0,0 +1,24 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Enum containing tonemapping ranges.
+/// </summary>
+public enum TonemappingRange
+{
+ /// <summary>
+ /// Auto.
+ /// </summary>
+ auto = 0,
+
+ /// <summary>
+ /// TV.
+ /// </summary>
+ tv = 1,
+
+ /// <summary>
+ /// PC.
+ /// </summary>
+ pc = 2
+}
diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs
new file mode 100644
index 000000000..c86328ba6
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Extensions;
+
+namespace MediaBrowser.Model.Extensions;
+
+/// <summary>
+/// Defines the <see cref="ContainerHelper"/> class.
+/// </summary>
+public static class ContainerHelper
+{
+ /// <summary>
+ /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists
+ /// in <paramref name="profileContainers"/>.
+ /// </summary>
+ /// <param name="profileContainers">The comma-delimited string being searched.
+ /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+ /// <param name="inputContainer">The comma-delimited string being matched.</param>
+ /// <returns>The result of the operation.</returns>
+ public static bool ContainsContainer(string? profileContainers, string? inputContainer)
+ {
+ var isNegativeList = false;
+ if (profileContainers != null && profileContainers.StartsWith('-'))
+ {
+ isNegativeList = true;
+ profileContainers = profileContainers[1..];
+ }
+
+ return ContainsContainer(profileContainers, isNegativeList, inputContainer);
+ }
+
+ /// <summary>
+ /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists
+ /// in <paramref name="profileContainers"/>.
+ /// </summary>
+ /// <param name="profileContainers">The comma-delimited string being searched.
+ /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+ /// <param name="inputContainer">The comma-delimited string being matched.</param>
+ /// <returns>The result of the operation.</returns>
+ public static bool ContainsContainer(string? profileContainers, ReadOnlySpan<char> inputContainer)
+ {
+ var isNegativeList = false;
+ if (profileContainers != null && profileContainers.StartsWith('-'))
+ {
+ isNegativeList = true;
+ profileContainers = profileContainers[1..];
+ }
+
+ return ContainsContainer(profileContainers, isNegativeList, inputContainer);
+ }
+
+ /// <summary>
+ /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+ /// does not exist in <paramref name="profileContainers"/>.
+ /// </summary>
+ /// <param name="profileContainers">The comma-delimited string being searched.</param>
+ /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+ /// <param name="inputContainer">The comma-delimited string being matched.</param>
+ /// <returns>The result of the operation.</returns>
+ public static bool ContainsContainer(string? profileContainers, bool isNegativeList, string? inputContainer)
+ {
+ if (string.IsNullOrEmpty(inputContainer))
+ {
+ return isNegativeList;
+ }
+
+ return ContainsContainer(profileContainers, isNegativeList, inputContainer.AsSpan());
+ }
+
+ /// <summary>
+ /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+ /// does not exist in <paramref name="profileContainers"/>.
+ /// </summary>
+ /// <param name="profileContainers">The comma-delimited string being searched.</param>
+ /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+ /// <param name="inputContainer">The comma-delimited string being matched.</param>
+ /// <returns>The result of the operation.</returns>
+ public static bool ContainsContainer(string? profileContainers, bool isNegativeList, ReadOnlySpan<char> inputContainer)
+ {
+ if (string.IsNullOrEmpty(profileContainers))
+ {
+ // Empty profiles always support all containers/codecs.
+ return true;
+ }
+
+ var allInputContainers = inputContainer.Split(',');
+ var allProfileContainers = profileContainers.SpanSplit(',');
+ foreach (var container in allInputContainers)
+ {
+ if (!container.IsEmpty)
+ {
+ foreach (var profile in allProfileContainers)
+ {
+ if (!profile.IsEmpty && container.Equals(profile, StringComparison.OrdinalIgnoreCase))
+ {
+ return !isNegativeList;
+ }
+ }
+ }
+ }
+
+ return isNegativeList;
+ }
+
+ /// <summary>
+ /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
+ /// does not exist in <paramref name="profileContainers"/>.
+ /// </summary>
+ /// <param name="profileContainers">The profile containers being matched searched.</param>
+ /// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
+ /// <param name="inputContainer">The comma-delimited string being matched.</param>
+ /// <returns>The result of the operation.</returns>
+ public static bool ContainsContainer(IReadOnlyList<string>? profileContainers, bool isNegativeList, string inputContainer)
+ {
+ if (profileContainers is null)
+ {
+ // Empty profiles always support all containers/codecs.
+ return true;
+ }
+
+ var allInputContainers = Split(inputContainer);
+ foreach (var container in allInputContainers)
+ {
+ foreach (var profile in profileContainers)
+ {
+ if (string.Equals(profile, container, StringComparison.OrdinalIgnoreCase))
+ {
+ return !isNegativeList;
+ }
+ }
+ }
+
+ return isNegativeList;
+ }
+
+ /// <summary>
+ /// Splits and input string.
+ /// </summary>
+ /// <param name="input">The input string.</param>
+ /// <returns>The result of the operation.</returns>
+ public static string[] Split(string? input)
+ {
+ return input?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? [];
+ }
+}
diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
new file mode 100644
index 000000000..4a814f22a
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Linq;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Model.Extensions;
+
+/// <summary>
+/// Extensions for <see cref="LibraryOptions"/>.
+/// </summary>
+public static class LibraryOptionsExtension
+{
+ /// <summary>
+ /// Get the custom tag delimiters.
+ /// </summary>
+ /// <param name="options">This LibraryOptions.</param>
+ /// <returns>CustomTagDelimiters in char[].</returns>
+ public static char[] GetCustomTagDelimiters(this LibraryOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ return options.CustomTagDelimiters.Select<string, char?>(x =>
+ {
+ var isChar = char.TryParse(x, out var c);
+ if (isChar)
+ {
+ return c;
+ }
+
+ return null;
+ }).Where(x => x is not null).Select(x => x!.Value).ToArray();
+ }
+}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index ec381d423..2085328dd 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -34,6 +34,13 @@ namespace MediaBrowser.Model.IO
string MakeAbsolutePath(string folderPath, string filePath);
/// <summary>
+ /// Moves a directory to a new location.
+ /// </summary>
+ /// <param name="source">Source directory.</param>
+ /// <param name="destination">Destination directory.</param>
+ void MoveDirectory(string source, string destination);
+
+ /// <summary>
/// Returns a <see cref="FileSystemMetadata" /> object for the specified file or directory path.
/// </summary>
/// <param name="path">A path to a file or directory.</param>
diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
index a832169c2..a355387b1 100644
--- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
+++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
@@ -9,6 +9,9 @@ namespace MediaBrowser.Model.LiveTv
{
AllowHWTranscoding = true;
IgnoreDts = true;
+ AllowStreamSharing = true;
+ AllowFmp4TranscodingContainer = false;
+ FallbackMaxStreamingBitrate = 30000000;
}
public string Id { get; set; }
@@ -25,6 +28,12 @@ namespace MediaBrowser.Model.LiveTv
public bool AllowHWTranscoding { get; set; }
+ public bool AllowFmp4TranscodingContainer { get; set; }
+
+ public bool AllowStreamSharing { get; set; }
+
+ public int FallbackMaxStreamingBitrate { get; set; }
+
public bool EnableStreamLooping { get; set; }
public string Source { get; set; }
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
index 24eab1a74..92f467eb0 100644
--- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Model.MediaInfo
{
EnableDirectPlay = true;
EnableDirectStream = true;
+ AlwaysBurnInSubtitleWhenTranscoding = false;
DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
}
@@ -40,6 +41,8 @@ namespace MediaBrowser.Model.MediaInfo
public bool EnableDirectStream { get; set; }
+ public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
+
public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; }
}
}
diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs
new file mode 100644
index 000000000..8c1f44de8
--- /dev/null
+++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Model;
+
+/// <summary>
+/// Model containing the arguments for enumerating the requested media item.
+/// </summary>
+public record MediaSegmentGenerationRequest
+{
+ /// <summary>
+ /// Gets the Id to the BaseItem the segments should be extracted from.
+ /// </summary>
+ public Guid ItemId { get; init; }
+}
diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs
deleted file mode 100644
index cf424fef5..000000000
--- a/MediaBrowser.Model/Session/HardwareEncodingType.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-namespace MediaBrowser.Model.Session
-{
- /// <summary>
- /// Enum HardwareEncodingType.
- /// </summary>
- public enum HardwareEncodingType
- {
- /// <summary>
- /// AMD AMF.
- /// </summary>
- AMF = 0,
-
- /// <summary>
- /// Intel Quick Sync Video.
- /// </summary>
- QSV = 1,
-
- /// <summary>
- /// NVIDIA NVENC.
- /// </summary>
- NVENC = 2,
-
- /// <summary>
- /// Video4Linux2 V4L2.
- /// </summary>
- V4L2M2M = 3,
-
- /// <summary>
- /// Video Acceleration API (VAAPI).
- /// </summary>
- VAAPI = 4,
-
- /// <summary>
- /// Video ToolBox.
- /// </summary>
- VideoToolBox = 5,
-
- /// <summary>
- /// Rockchip Media Process Platform (RKMPP).
- /// </summary>
- RKMPP = 6
- }
-}
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index bbdf4536b..39c5ac8fa 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -18,6 +18,7 @@ namespace MediaBrowser.Model.Session
// Video Constraints
VideoProfileNotSupported = 1 << 6,
VideoRangeTypeNotSupported = 1 << 24,
+ VideoCodecTagNotSupported = 1 << 25,
VideoLevelNotSupported = 1 << 7,
VideoResolutionNotSupported = 1 << 8,
VideoBitDepthNotSupported = 1 << 9,
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index 000cbd4c5..ae25267ac 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -1,34 +1,76 @@
#nullable disable
-#pragma warning disable CS1591
-namespace MediaBrowser.Model.Session
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Session;
+
+/// <summary>
+/// Class holding information on a runnning transcode.
+/// </summary>
+public class TranscodingInfo
{
- public class TranscodingInfo
- {
- public string AudioCodec { get; set; }
+ /// <summary>
+ /// Gets or sets the thread count used for encoding.
+ /// </summary>
+ public string AudioCodec { get; set; }
- public string VideoCodec { get; set; }
+ /// <summary>
+ /// Gets or sets the thread count used for encoding.
+ /// </summary>
+ public string VideoCodec { get; set; }
- public string Container { get; set; }
+ /// <summary>
+ /// Gets or sets the thread count used for encoding.
+ /// </summary>
+ public string Container { get; set; }
- public bool IsVideoDirect { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the video is passed through.
+ /// </summary>
+ public bool IsVideoDirect { get; set; }
- public bool IsAudioDirect { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the audio is passed through.
+ /// </summary>
+ public bool IsAudioDirect { get; set; }
- public int? Bitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the bitrate.
+ /// </summary>
+ public int? Bitrate { get; set; }
- public float? Framerate { get; set; }
+ /// <summary>
+ /// Gets or sets the framerate.
+ /// </summary>
+ public float? Framerate { get; set; }
- public double? CompletionPercentage { get; set; }
+ /// <summary>
+ /// Gets or sets the completion percentage.
+ /// </summary>
+ public double? CompletionPercentage { get; set; }
- public int? Width { get; set; }
+ /// <summary>
+ /// Gets or sets the video width.
+ /// </summary>
+ public int? Width { get; set; }
- public int? Height { get; set; }
+ /// <summary>
+ /// Gets or sets the video height.
+ /// </summary>
+ public int? Height { get; set; }
- public int? AudioChannels { get; set; }
+ /// <summary>
+ /// Gets or sets the audio channels.
+ /// </summary>
+ public int? AudioChannels { get; set; }
- public HardwareEncodingType? HardwareAccelerationType { get; set; }
+ /// <summary>
+ /// Gets or sets the hardware acceleration type.
+ /// </summary>
+ public HardwareAccelerationType? HardwareAccelerationType { get; set; }
- public TranscodeReason TranscodeReasons { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the transcode reasons.
+ /// </summary>
+ public TranscodeReason TranscodeReasons { get; set; }
}
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index df9d15ec2..32ab7716f 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -54,7 +54,14 @@ namespace MediaBrowser.Providers.BoxSets
if (mergeMetadataSettings)
{
- targetItem.LinkedChildren = sourceItem.LinkedChildren;
+ if (replaceData || targetItem.LinkedChildren.Length == 0)
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren;
+ }
+ else
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
+ }
}
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 1bb7ffcce..36a7c2fab 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -68,16 +68,22 @@ namespace MediaBrowser.Providers.Manager
/// Removes all existing images from the provided item.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/> to remove images from.</param>
+ /// <param name="canDeleteLocal">Whether removing images outside metadata folder is allowed.</param>
/// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
- public bool RemoveImages(BaseItem item)
+ public bool RemoveImages(BaseItem item, bool canDeleteLocal = false)
{
var singular = new List<ItemImageInfo>();
+ var itemMetadataPath = item.GetInternalMetadataPath();
for (var i = 0; i < _singularImages.Length; i++)
{
var currentImage = item.GetImageInfo(_singularImages[i], 0);
if (currentImage is not null)
{
- singular.Add(currentImage);
+ var imageInMetadataFolder = currentImage.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase);
+ if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled())
+ {
+ singular.Add(currentImage);
+ }
}
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 8af4ed2a8..7203bf115 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -675,6 +675,7 @@ namespace MediaBrowser.Providers.Manager
};
temp.Item.Path = item.Path;
temp.Item.Id = item.Id;
+ temp.Item.ParentIndexNumber = item.ParentIndexNumber;
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
@@ -728,7 +729,7 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
- MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
+ MergeData(localItem, temp, [], false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
break;
@@ -768,7 +769,7 @@ namespace MediaBrowser.Providers.Manager
if (!options.RemoveOldMetadata)
{
// Add existing metadata to provider result if it does not exist there
- MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
+ MergeData(metadata, temp, [], false, false);
}
if (isLocalLocked)
@@ -837,7 +838,7 @@ namespace MediaBrowser.Providers.Manager
{
result.Provider = provider.Name;
- MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
+ MergeData(result, temp, [], replaceData, false);
MergeNewData(temp.Item, id);
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 60d89a51b..81a9af68b 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Manager
private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new();
private readonly IMemoryCache _memoryCache;
-
+ private readonly IMediaSegmentManager _mediaSegmentManager;
private readonly AsyncKeyedLocker<string> _imageSaveLock = new(o =>
{
o.PoolSize = 20;
@@ -92,6 +92,7 @@ namespace MediaBrowser.Providers.Manager
/// <param name="baseItemManager">The BaseItem manager.</param>
/// <param name="lyricManager">The lyric manager.</param>
/// <param name="memoryCache">The memory cache.</param>
+ /// <param name="mediaSegmentManager">The media segment manager.</param>
public ProviderManager(
IHttpClientFactory httpClientFactory,
ISubtitleManager subtitleManager,
@@ -103,7 +104,8 @@ namespace MediaBrowser.Providers.Manager
ILibraryManager libraryManager,
IBaseItemManager baseItemManager,
ILyricManager lyricManager,
- IMemoryCache memoryCache)
+ IMemoryCache memoryCache,
+ IMediaSegmentManager mediaSegmentManager)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
@@ -116,6 +118,7 @@ namespace MediaBrowser.Providers.Manager
_baseItemManager = baseItemManager;
_lyricManager = lyricManager;
_memoryCache = memoryCache;
+ _mediaSegmentManager = mediaSegmentManager;
}
/// <inheritdoc/>
@@ -572,6 +575,14 @@ namespace MediaBrowser.Providers.Manager
Type = MetadataPluginType.LyricFetcher
}));
+ // Media segment providers
+ var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy);
+ pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin
+ {
+ Name = i.Name,
+ Type = MetadataPluginType.MediaSegmentProvider
+ }));
+
summary.Plugins = pluginList.ToArray();
var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 7e0773b6d..27f6d120f 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks;
using ATL;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -17,6 +16,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -28,6 +28,7 @@ namespace MediaBrowser.Providers.MediaInfo
public class AudioFileProber
{
private const char InternalValueSeparator = '\u001F';
+
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly ILibraryManager _libraryManager;
@@ -63,6 +64,8 @@ namespace MediaBrowser.Providers.MediaInfo
_lyricResolver = lyricResolver;
_lyricManager = lyricManager;
ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
+ ATL.Settings.UseFileNameWhenNoTitle = false;
+ ATL.Settings.ID3v2_separatev2v3Values = false;
}
/// <summary>
@@ -158,14 +161,16 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
{
+ var libraryOptions = _libraryManager.GetLibraryOptions(audio);
Track track = new Track(audio.Path);
- // ATL will fall back to filename as title when it does not understand the metadata
- if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT)))
+ if (track.MetadataFormats
+ .All(mf => string.Equals(mf.ShortName, "ID3v1", StringComparison.OrdinalIgnoreCase)))
{
- track.Title = mediaInfo.Name;
+ _logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
}
+ track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
track.Year ??= mediaInfo.ProductionYear;
track.TrackNumber ??= mediaInfo.IndexNumber;
@@ -175,6 +180,12 @@ namespace MediaBrowser.Providers.MediaInfo
{
var people = new List<PersonInfo>();
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
+
+ if (libraryOptions.UseCustomTagDelimiters)
+ {
+ albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
+ }
+
foreach (var albumArtist in albumArtists)
{
if (!string.IsNullOrEmpty(albumArtist))
@@ -187,7 +198,26 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
+ string[]? performers = null;
+ if (libraryOptions.PreferNonstandardArtistsTag)
+ {
+ track.AdditionalFields.TryGetValue("ARTISTS", out var artistsTagString);
+ if (artistsTagString is not null)
+ {
+ performers = artistsTagString.Split(InternalValueSeparator);
+ }
+ }
+
+ if (performers is null || performers.Length == 0)
+ {
+ performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
+ }
+
+ if (libraryOptions.UseCustomTagDelimiters)
+ {
+ performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
+ }
+
foreach (var performer in performers)
{
if (!string.IsNullOrEmpty(performer))
@@ -285,7 +315,13 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
- audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
+
+ if (libraryOptions.UseCustomTagDelimiters)
+ {
+ genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
+ }
+
+ audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
? genres
: audio.Genres;
}
@@ -379,5 +415,31 @@ namespace MediaBrowser.Providers.MediaInfo
currentStreams.Add(externalLyricFiles[0]);
}
}
+
+ private List<string> SplitWithCustomDelimiter(string val, char[] tagDelimiters, string[] whitelist)
+ {
+ var items = new List<string>();
+ var temp = val;
+ foreach (var whitelistItem in whitelist)
+ {
+ if (string.IsNullOrWhiteSpace(whitelistItem))
+ {
+ continue;
+ }
+
+ var originalTemp = temp;
+ temp = temp.Replace(whitelistItem, string.Empty, StringComparison.OrdinalIgnoreCase);
+
+ if (!string.Equals(temp, originalTemp, StringComparison.OrdinalIgnoreCase))
+ {
+ items.Add(whitelistItem);
+ }
+ }
+
+ var items2 = temp.Split(tagDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).DistinctNames();
+ items.AddRange(items2);
+
+ return items;
+ }
}
}
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
index d8855ec93..9b4793ee6 100644
--- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
+++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
@@ -12,8 +10,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.TV
{
+ /// <summary>
+ /// Service to manage episode metadata.
+ /// </summary>
public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public EpisodeMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<EpisodeMetadataService> logger,
@@ -94,6 +103,11 @@ namespace MediaBrowser.Providers.TV
{
targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
}
+
+ if (replaceData || !targetItem.ParentIndexNumber.HasValue)
+ {
+ targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber;
+ }
}
}
}
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 54dcee41e..8b690193e 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,8 +13,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.TV
{
+ /// <summary>
+ /// Service to manage season metadata.
+ /// </summary>
public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public SeasonMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<SeasonMetadataService> logger,
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 80c56351c..f4aede463 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -20,10 +18,22 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.TV
{
+ /// <summary>
+ /// Service to manage series metadata.
+ /// </summary>
public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
private readonly ILocalizationManager _localizationManager;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<SeriesMetadataService> logger,
@@ -36,6 +46,7 @@ namespace MediaBrowser.Providers.TV
_localizationManager = localizationManager;
}
+ /// <inheritdoc />
public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
if (item is Series series)
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 90c2ff8dd..31c0eeb31 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -98,7 +98,8 @@ public class TrickplayImagesTask : IScheduledTask
try
{
- await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false);
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ await _trickplayManager.RefreshTrickplayDataAsync(video, false, libraryOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
new file mode 100644
index 000000000..c0b8a8c75
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+/// <summary>
+/// Class TrickplayMoveImagesTask.
+/// </summary>
+public class TrickplayMoveImagesTask : IScheduledTask
+{
+ private readonly ILogger<TrickplayMoveImagesTask> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayMoveImagesTask"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ public TrickplayMoveImagesTask(
+ ILogger<TrickplayMoveImagesTask> logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskMoveTrickplayImages");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskMoveTrickplayImagesDescription");
+
+ /// <inheritdoc />
+ public string Key => "MoveTrickplayImages";
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => [];
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ const int Limit = 100;
+ int itemCount = 0, offset = 0, previousCount;
+
+ // This count may not be accurate, but just get something to show progress on the dashboard.
+ var totalVideoCount = _libraryManager.GetCount(new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true
+ });
+
+ var trickplayQuery = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false
+ };
+
+ do
+ {
+ var trickplayInfos = await _trickplayManager.GetTrickplayItemsAsync(Limit, offset).ConfigureAwait(false);
+ previousCount = trickplayInfos.Count;
+ offset += Limit;
+
+ trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray();
+ var items = _libraryManager.GetItemList(trickplayQuery);
+ foreach (var trickplayInfo in trickplayInfos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var video = items.OfType<Video>().FirstOrDefault(i => i.Id.Equals(trickplayInfo.ItemId));
+ if (video is null)
+ {
+ continue;
+ }
+
+ itemCount++;
+ try
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ await _trickplayManager.MoveGeneratedTrickplayDataAsync(video, libraryOptions, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error moving trickplay files for {ItemName}", video.Name);
+ }
+ }
+
+ progress.Report(100d * itemCount / totalVideoCount);
+ } while (previousCount == Limit);
+
+ progress.Report(100);
+ }
+}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index 9dc4446fc..2c74e5f70 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -99,7 +99,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
{
var libraryOptions = _libraryManager.GetLibraryOptions(video);
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
- bool replace = options.ReplaceAllImages;
+ bool replace = options.RegenerateTrickplay && options.MetadataRefreshMode > MetadataRefreshMode.Default;
if (!enableDuringScan.GetValueOrDefault(false))
{
@@ -108,11 +108,11 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
{
- await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ await _trickplayManager.RefreshTrickplayDataAsync(video, replace, libraryOptions, cancellationToken).ConfigureAwait(false);
}
else
{
- _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, libraryOptions, cancellationToken).ConfigureAwait(false);
}
// The core doesn't need to trigger any save operations over this
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index a3c200447..79e9e7503 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -348,7 +348,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("aspectratio", stream.AspectRatio);
}
- var framerate = stream.AverageFrameRate ?? stream.RealFrameRate;
+ var framerate = stream.ReferenceFrameRate;
if (framerate.HasValue)
{
diff --git a/src/Jellyfin.Extensions/FormattingStreamWriter.cs b/src/Jellyfin.Extensions/FormattingStreamWriter.cs
new file mode 100644
index 000000000..40e3c5a68
--- /dev/null
+++ b/src/Jellyfin.Extensions/FormattingStreamWriter.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// A custom StreamWriter which supports setting a IFormatProvider.
+/// </summary>
+public class FormattingStreamWriter : StreamWriter
+{
+ private readonly IFormatProvider _formatProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
+ /// </summary>
+ /// <param name="stream">The stream to write to.</param>
+ /// <param name="formatProvider">The format provider to use.</param>
+ public FormattingStreamWriter(Stream stream, IFormatProvider formatProvider)
+ : base(stream)
+ {
+ _formatProvider = formatProvider;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FormattingStreamWriter"/> class.
+ /// </summary>
+ /// <param name="path">The complete file path to write to.</param>
+ /// <param name="formatProvider">The format provider to use.</param>
+ public FormattingStreamWriter(string path, IFormatProvider formatProvider)
+ : base(path)
+ {
+ _formatProvider = formatProvider;
+ }
+
+ /// <inheritdoc />
+ public override IFormatProvider FormatProvider
+ => _formatProvider;
+}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index 1466d3a71..936a5a97c 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -35,38 +37,27 @@ namespace Jellyfin.Extensions.Json.Converters
var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
if (stringEntries.Length == 0)
{
- return Array.Empty<T>();
+ return [];
}
- var parsedValues = new object[stringEntries.Length];
- var convertedCount = 0;
+ var typedValues = new List<T>();
for (var i = 0; i < stringEntries.Length; i++)
{
try
{
- parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException();
- convertedCount++;
+ var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim());
+ if (parsedValue is not null)
+ {
+ typedValues.Add((T)parsedValue);
+ }
}
catch (FormatException)
{
- // TODO log when upgraded to .Net6
- // https://github.com/dotnet/runtime/issues/42975
- // _logger.LogDebug(e, "Error converting value.");
+ // Ignore unconvertable inputs
}
}
- var typedValues = new T[convertedCount];
- var typedValueIndex = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- if (parsedValues[i] is not null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
- }
-
- return typedValues;
+ return typedValues.ToArray();
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
@@ -75,7 +66,39 @@ namespace Jellyfin.Extensions.Json.Converters
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
{
- throw new NotImplementedException();
+ if (value is not null)
+ {
+ writer.WriteStartArray();
+ if (value.Length > 0)
+ {
+ var toWrite = value.Length - 1;
+ foreach (var it in value)
+ {
+ var wrote = false;
+ if (it is not null)
+ {
+ writer.WriteStringValue(it.ToString());
+ wrote = true;
+ }
+
+ if (toWrite > 0)
+ {
+ if (wrote)
+ {
+ writer.WriteStringValue(Delimiter.ToString());
+ }
+
+ toWrite--;
+ }
+ }
+ }
+
+ writer.WriteEndArray();
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
}
}
}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 8cfebd594..4b9677d9f 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -9,8 +9,21 @@ namespace Jellyfin.Extensions
/// </summary>
public static partial class StringExtensions
{
- private static readonly Lazy<Transliterator> _transliterator = new(() => Transliterator.GetInstance(
- "Any-Latin; Latin-Ascii; Lower; NFD; [:Nonspacing Mark:] Remove; [:Punctuation:] Remove;"));
+ private static readonly Lazy<string> _transliteratorId = new(() =>
+ Environment.GetEnvironmentVariable("JELLYFIN_TRANSLITERATOR_ID")
+ ?? "Any-Latin; Latin-Ascii; Lower; NFD; [:Nonspacing Mark:] Remove; [:Punctuation:] Remove;");
+
+ private static readonly Lazy<Transliterator?> _transliterator = new(() =>
+ {
+ try
+ {
+ return Transliterator.GetInstance(_transliteratorId.Value);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ });
// Matches non-conforming unicode chars
// https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
@@ -108,7 +121,7 @@ namespace Jellyfin.Extensions
/// <returns>The transliterated string.</returns>
public static string Transliterated(this string text)
{
- return _transliterator.Value.Transliterate(text);
+ return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text);
}
}
}
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index ff00c8999..0c660637f 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -130,7 +130,7 @@ namespace Jellyfin.LiveTv.IO
const int MaxBitrate = 25000000;
videoArgs = string.Format(
CultureInfo.InvariantCulture,
- "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41",
+ "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -profile:v high -level 41",
GetOutputSizeParam(),
MaxBitrate);
}
@@ -157,7 +157,7 @@ namespace Jellyfin.LiveTv.IO
flags.Add("+genpts");
}
- var inputModifier = "-async 1 -vsync -1";
+ var inputModifier = "-async 1";
if (flags.Count > 0)
{
diff --git a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index fef84dd00..e1f87a7bd 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -331,6 +331,8 @@ namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
SupportsTranscoding = true,
IsInfiniteStream = true,
IgnoreDts = true,
+ UseMostCompatibleTranscodingProfile = true, // All HDHR tuners require this
+ FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate,
// IgnoreIndex = true,
// ReadAtNativeFramerate = true
};
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
index 365f0188d..be81171a0 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
@@ -94,7 +94,7 @@ namespace Jellyfin.LiveTv.TunerHosts
var mediaSource = sources[0];
- if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
+ if (tunerHost.AllowStreamSharing && mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
@@ -200,7 +200,9 @@ namespace Jellyfin.LiveTv.TunerHosts
SupportsDirectPlay = supportsDirectPlay,
SupportsDirectStream = supportsDirectStream,
- RequiredHttpHeaders = httpHeaders
+ RequiredHttpHeaders = httpHeaders,
+ UseMostCompatibleTranscodingProfile = !info.AllowFmp4TranscodingContainer,
+ FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate
};
mediaSource.InferTotalBitrate();
diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
index 9a023d7ed..1846ba26b 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
@@ -128,7 +128,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
return false;
}
- internal static bool IsExtractionAllowedForFile(ReadOnlySpan<char> filePath, string[] allowedExtensions)
+ internal static bool IsExtractionAllowedForFile(ReadOnlySpan<char> filePath, IReadOnlyList<string> allowedExtensions)
{
var extension = Path.GetExtension(filePath);
if (extension.IsEmpty)
@@ -138,7 +138,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
// Remove the leading dot
var extensionWithoutDot = extension[1..];
- for (var i = 0; i < allowedExtensions.Length; i++)
+ for (var i = 0; i < allowedExtensions.Count; i++)
{
var allowedExtension = allowedExtensions[i].AsSpan().TrimStart('.');
if (extensionWithoutDot.Equals(allowedExtension, StringComparison.OrdinalIgnoreCase))
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index cf6a2cc55..b285b836b 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -97,10 +97,15 @@ public class NetworkManager : INetworkManager, IDisposable
_networkEventLock = new object();
_remoteAddressFilter = new List<IPNetwork>();
+ _ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange);
+
UpdateSettings(_configurationManager.GetNetworkConfiguration());
- NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
- NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
+ if (detectNetworkChange)
+ {
+ NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
+ NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
+ }
_configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
}
diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
index f3ada59db..6171f12e4 100644
--- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
+++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
@@ -1,4 +1,7 @@
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.MediaInfo;
+using Moq;
using Xunit;
namespace Jellyfin.Controller.Tests.Entities;
@@ -14,4 +17,30 @@ public class BaseItemTests
[InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
=> Assert.Equal(expected, BaseItem.ModifySortChunks(input));
+
+ [Theory]
+ [InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
+ [InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
+ public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
+ {
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
+ .Returns((string x) => MediaProtocol.File);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+
+ var video = new Video()
+ {
+ Path = primaryPath
+ };
+
+ var videoAlt = new Video()
+ {
+ Path = altPath,
+ };
+
+ video.LocalAlternateVersions = [videoAlt.Path];
+
+ Assert.Equal(name, video.GetMediaSourceName(video));
+ Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
+ }
}
diff --git a/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs
new file mode 100644
index 000000000..06e3c2721
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs
@@ -0,0 +1,23 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests;
+
+public static class FormattingStreamWriterTests
+{
+ [Fact]
+ public static void Shuffle_Valid_Correct()
+ {
+ Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE", false);
+ using (var ms = new MemoryStream())
+ using (var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture))
+ {
+ txt.Write("{0}", 3.14159);
+ txt.Close();
+ Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray()));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
index 61105b42b..9fc015823 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
@@ -41,7 +41,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<string>
{
- Value = new[] { "a", "b", "c" }
+ Value = ["a", "b", "c"]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
@@ -53,7 +53,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<string>
{
- Value = new[] { "a", "b", "c" }
+ Value = ["a", "b", "c"]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
@@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
- Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
@@ -77,7 +77,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
- Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions);
@@ -89,7 +89,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
- Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions);
@@ -101,7 +101,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
- Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
@@ -113,7 +113,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<string>
{
- Value = new[] { "a", "b", "c" }
+ Value = ["a", "b", "c"]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
@@ -125,7 +125,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
- Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs
new file mode 100644
index 000000000..1ad4bed56
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs
@@ -0,0 +1,83 @@
+using System;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Extensions;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Dlna;
+
+public class ContainerHelperTests
+{
+ private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("mp4")]
+ public void ContainsContainer_EmptyContainerProfile_ReturnsTrue(string? containers)
+ {
+ Assert.True(_emptyContainerProfile.ContainsContainer(containers));
+ }
+
+ [Theory]
+ [InlineData("mp3,mpeg", "mp3")]
+ [InlineData("mp3,mpeg,avi", "mp3,avi")]
+ [InlineData("-mp3,mpeg", "avi")]
+ [InlineData("-mp3,mpeg,avi", "mp4,jpg")]
+ public void ContainsContainer_InList_ReturnsTrue(string container, string? extension)
+ {
+ Assert.True(ContainerHelper.ContainsContainer(container, extension));
+ }
+
+ [Theory]
+ [InlineData("mp3,mpeg", "avi")]
+ [InlineData("mp3,mpeg,avi", "mp4,jpg")]
+ [InlineData("mp3,mpeg", null)]
+ [InlineData("mp3,mpeg", "")]
+ [InlineData("-mp3,mpeg", "mp3")]
+ [InlineData("-mp3,mpeg,avi", "mpeg,avi")]
+ [InlineData(",mp3,", ",avi,")] // Empty values should be discarded
+ [InlineData("-,mp3,", ",mp3,")] // Empty values should be discarded
+ public void ContainsContainer_NotInList_ReturnsFalse(string container, string? extension)
+ {
+ Assert.False(ContainerHelper.ContainsContainer(container, extension));
+
+ if (extension is not null)
+ {
+ Assert.False(ContainerHelper.ContainsContainer(container, extension.AsSpan()));
+ }
+ }
+
+ [Theory]
+ [InlineData("mp3,mpeg", "mp3")]
+ [InlineData("mp3,mpeg,avi", "mp3,avi")]
+ [InlineData("-mp3,mpeg", "avi")]
+ [InlineData("-mp3,mpeg,avi", "mp4,jpg")]
+ public void ContainsContainer_InList_ReturnsTrue_SpanVersion(string container, string? extension)
+ {
+ Assert.True(ContainerHelper.ContainsContainer(container, extension.AsSpan()));
+ }
+
+ [Theory]
+ [InlineData(new string[] { "mp3", "mpeg" }, false, "mpeg")]
+ [InlineData(new string[] { "mp3", "mpeg", "avi" }, false, "avi")]
+ [InlineData(new string[] { "mp3", "", "avi" }, false, "mp3")]
+ [InlineData(new string[] { "mp3", "mpeg" }, true, "avi")]
+ [InlineData(new string[] { "mp3", "mpeg", "avi" }, true, "mkv")]
+ [InlineData(new string[] { "mp3", "", "avi" }, true, "")]
+ public void ContainsContainer_ThreeArgs_InList_ReturnsTrue(string[] containers, bool isNegativeList, string inputContainer)
+ {
+ Assert.True(ContainerHelper.ContainsContainer(containers, isNegativeList, inputContainer));
+ }
+
+ [Theory]
+ [InlineData(new string[] { "mp3", "mpeg" }, false, "avi")]
+ [InlineData(new string[] { "mp3", "mpeg", "avi" }, false, "mkv")]
+ [InlineData(new string[] { "mp3", "", "avi" }, false, "")]
+ [InlineData(new string[] { "mp3", "mpeg" }, true, "mpeg")]
+ [InlineData(new string[] { "mp3", "mpeg", "avi" }, true, "mp3")]
+ [InlineData(new string[] { "mp3", "", "avi" }, true, "avi")]
+ public void ContainsContainer_ThreeArgs_InList_ReturnsFalse(string[] containers, bool isNegativeList, string inputContainer)
+ {
+ Assert.False(ContainerHelper.ContainsContainer(containers, isNegativeList, inputContainer));
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs
deleted file mode 100644
index cca056c28..000000000
--- a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using MediaBrowser.Model.Dlna;
-using Xunit;
-
-namespace Jellyfin.Model.Tests.Dlna
-{
- public class ContainerProfileTests
- {
- private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
-
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- [InlineData("mp4")]
- public void ContainsContainer_EmptyContainerProfile_True(string? containers)
- {
- Assert.True(_emptyContainerProfile.ContainsContainer(containers));
- }
- }
-}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 31ddd427c..bd2143f25 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -22,37 +22,58 @@ namespace Jellyfin.Model.Tests
[Theory]
// Chrome
[InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
- [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450
- [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-hi10p-aac-5000k", PlayMethod.DirectPlay)]
+ [InlineData("Chrome", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("Chrome", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Chrome", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Chrome", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Chrome", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
// Firefox
[InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
- [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450
- [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
// Safari
[InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("SafariNext", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("SafariNext", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("SafariNext", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")]
+ [InlineData("SafariNext", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")]
+ [InlineData("SafariNext", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")]
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -62,21 +83,21 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
// Yatse
[InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
+ [InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay
- [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// JellyfinMediaPlayer
[InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -86,18 +107,18 @@ namespace Jellyfin.Model.Tests
[InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- // Chrome-NoHLS
+ // Non-HLS Progressive transcoding
[InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
- [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "http")] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "Remux", "http")] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450
[InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")]
- [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450
- [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "DirectStream", "http")] // webm requested, aac not supported
+ [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450
+ [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "http")] // #6450
// TranscodeMedia
[InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
@@ -147,7 +168,7 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
- [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -155,7 +176,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
- [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
@@ -163,13 +184,18 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
- [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
- [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
+ // WebOS 23
+ [InlineData("WebOS-23", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")]
+ [InlineData("WebOS-23", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("WebOS-23", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("WebOS-23", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")]
public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{
var options = await GetMediaOptions(deviceName, mediaSource);
@@ -179,24 +205,24 @@ namespace Jellyfin.Model.Tests
[Theory]
// Chrome
[InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 <BUG: this is direct played>
- [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
- [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450
- [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 <BUG: this is direct played>
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")]
+ [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "HLS.mp4")] // #6450
// Firefox
[InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")]
- [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450
- [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
[InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
// Safari
[InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -204,9 +230,10 @@ namespace Jellyfin.Model.Tests
[InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
- [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -215,19 +242,19 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
// Yatse
[InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
- [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
- [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
- [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// JellyfinMediaPlayer
[InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -245,7 +272,7 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
- [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -253,7 +280,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
- [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
@@ -261,10 +288,10 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
- [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
- [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
@@ -281,34 +308,37 @@ namespace Jellyfin.Model.Tests
[Theory]
// Chrome
- [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Chrome", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
- [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")]
// Firefox
- [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Firefox", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "HLS.mp4")]
// Yatse
- [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
- [InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
// no streams
- [InlineData("Chrome", "no-streams", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450
+ [InlineData("Chrome", "no-streams", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")] // #6450
// AndroidTV
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
// Tizen 3 Stereo
- [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
// Tizen 4 4K 5.1
- [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
- [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
+ // TranscodeMedia
+ [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-mp3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.ts")]
public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{
var options = await GetMediaOptions(deviceName, mediaSource);
@@ -328,7 +358,7 @@ namespace Jellyfin.Model.Tests
{
if (string.IsNullOrEmpty(transcodeProtocol))
{
- transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "HLS.ts";
+ transcodeProtocol = "HLS.ts";
}
var builder = GetStreamBuilder();
@@ -359,25 +389,30 @@ namespace Jellyfin.Model.Tests
if (playMethod == PlayMethod.DirectPlay)
{
// Check expected container
- var containers = ContainerProfile.SplitValue(mediaSource.Container);
+ var containers = mediaSource.Container.Split(',');
+ Assert.Contains(uri.Extension, containers);
// TODO: Test transcode too
- // Assert.Contains(uri.Extension, containers);
// Check expected video codec (1)
- Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
- Assert.Single(streamInfo.TargetVideoCodec);
+ if (targetVideoStream?.Codec is not null)
+ {
+ Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
+ Assert.Single(streamInfo.TargetVideoCodec);
+ }
- // Check expected audio codecs (1)
- Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec);
- Assert.Single(streamInfo.TargetAudioCodec);
- // Assert.Single(val.AudioCodecs);
+ if (targetAudioStream?.Codec is not null)
+ {
+ // Check expected audio codecs (1)
+ Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec);
+ Assert.Single(streamInfo.TargetAudioCodec);
+ }
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{
Assert.Equal(streamInfo.Container, uri.Extension);
}
}
- else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
+ else if (playMethod == PlayMethod.Transcode)
{
Assert.NotNull(streamInfo.Container);
Assert.NotEmpty(streamInfo.VideoCodecs);
@@ -409,7 +444,7 @@ namespace Jellyfin.Model.Tests
// Full transcode
if (transcodeMode.Equals("Transcode", StringComparison.Ordinal))
{
- if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
+ if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError | TranscodeReason.VideoRangeTypeNotSupported)) == 0)
{
Assert.All(
videoStreams,
@@ -547,6 +582,7 @@ namespace Jellyfin.Model.Tests
Profile = dp,
AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true,
+ EnableDirectStream = false // This is disabled in server
};
}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
index 81bb97ac8..e2f75b569 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
@@ -16,324 +16,200 @@
"DirectPlayProfiles": [
{
"Container": "webm",
- "AudioCodec": "vorbis,opus",
- "VideoCodec": "vp8,vp9,av1",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "vp8,vp9,av1",
+ "AudioCodec": "vorbis,opus"
},
{
"Container": "mp4,m4v",
- "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
- "VideoCodec": "h264,vp8,vp9,av1",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "h264,hevc,vp9,av1",
+ "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis"
},
{
"Container": "mov",
- "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
- "VideoCodec": "h264",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis"
},
{
"Container": "opus",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webm",
"AudioCodec": "opus",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
+ },
+ {
+ "Container": "ts",
+ "AudioCodec": "mp3",
+ "Type": "Audio"
},
{
"Container": "mp3",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4a",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4b",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "flac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "m4a",
- "AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "m4b",
- "AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webm",
"AudioCodec": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "wav",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "ogg",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
+ },
+ {
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "av1,hevc,h264,vp9",
+ "AudioCodec": "aac,mp2,opus,flac"
+ },
+ {
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,mp2"
}
],
"TranscodingProfiles": [
{
- "Container": "ts",
+ "Container": "mp4",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "hls",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 2,
- "SegmentLength": 0,
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
"BreakOnNonKeyFrames": true,
- "$type": "TranscodingProfile"
+ "EnableAudioVbrEncoding": true
},
{
"Container": "aac",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "mp3",
"Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "opus",
"Type": "Audio",
"AudioCodec": "opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "wav",
"Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "opus",
"Type": "Audio",
"AudioCodec": "opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "mp3",
"Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "aac",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
"Container": "wav",
"Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
- },
- {
- "Container": "ts",
- "Type": "Video",
- "VideoCodec": "h264",
- "AudioCodec": "aac,mp3",
- "Protocol": "hls",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
- "Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 2,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": true,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
},
{
- "Container": "webm",
+ "Container": "mp4",
"Type": "Video",
- "VideoCodec": "vp8,vp9,av1,vpx",
- "AudioCodec": "vorbis,opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "AudioCodec": "aac,mp2,opus,flac",
+ "VideoCodec": "av1,hevc,h264,vp9",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true
},
{
- "Container": "mp4",
+ "Container": "ts",
"Type": "Video",
+ "AudioCodec": "aac,mp3,mp2",
"VideoCodec": "h264",
- "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
- "Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true
}
],
+ "ContainerProfiles": [],
"CodecProfiles": [
{
"Type": "VideoAudio",
+ "Codec": "aac",
"Conditions": [
{
"Condition": "Equals",
"Property": "IsSecondaryAudio",
"Value": "false",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
}
- ],
- "Codec": "aac",
- "$type": "CodecProfile"
+ ]
},
{
"Type": "VideoAudio",
@@ -342,107 +218,144 @@
"Condition": "Equals",
"Property": "IsSecondaryAudio",
"Value": "false",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
}
- ],
- "$type": "CodecProfile"
+ ]
},
{
"Type": "Video",
+ "Codec": "h264",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "high|main|baseline|constrained baseline|high 10",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR",
+ "IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "52",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "NotEquals",
"Property": "IsInterlaced",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
}
- ],
- "Codec": "h264",
- "$type": "CodecProfile"
+ ]
},
{
"Type": "Video",
+ "Codec": "hevc",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
- "Value": "main",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "Value": "main|main 10",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
- "Value": "120",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "Value": "183",
+ "IsRequired": false
},
{
"Condition": "NotEquals",
"Property": "IsInterlaced",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
}
- ],
- "Codec": "hevc",
- "$type": "CodecProfile"
- }
- ],
- "ResponseProfiles": [
+ ]
+ },
{
- "Container": "m4v",
"Type": "Video",
- "MimeType": "video/mp4",
- "$type": "ResponseProfile"
+ "Codec": "vp9",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "av1",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "19",
+ "IsRequired": false
+ }
+ ]
}
],
"SubtitleProfiles": [
{
"Format": "vtt",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ass",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ssa",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
}
],
- "$type": "DeviceProfile"
+ "ResponseProfiles": [
+ {
+ "Type": "Video",
+ "Container": "m4v",
+ "MimeType": "video/mp4"
+ }
+ ]
}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
index 9874793d3..21ae7e5cb 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
@@ -15,426 +15,357 @@
"IgnoreTranscodeByteRangeRequests": false,
"DirectPlayProfiles": [
{
- "Container": "webm",
"AudioCodec": "vorbis,opus",
- "VideoCodec": "vp8,vp9,av1",
+ "Container": "webm",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "vp8,vp9,av1"
},
{
+ "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis",
"Container": "mp4,m4v",
- "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
- "VideoCodec": "h264,vp8,vp9,av1",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "h264,vp9,av1"
},
{
"Container": "opus",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
- "Container": "webm",
"AudioCodec": "opus",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Container": "webm",
+ "Type": "Audio"
+ },
+ {
+ "AudioCodec": "mp3",
+ "Container": "ts",
+ "Type": "Audio"
},
{
"Container": "mp3",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
- "Container": "m4a",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Container": "m4a",
+ "Type": "Audio"
},
{
- "Container": "m4b",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Container": "m4b",
+ "Type": "Audio"
},
{
"Container": "flac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "m4a",
- "AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
- },
- {
- "Container": "m4b",
- "AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
- "Container": "webm",
"AudioCodec": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Container": "webm",
+ "Type": "Audio"
},
{
"Container": "wav",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "ogg",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
+ },
+ {
+ "AudioCodec": "aac,mp2,opus,flac",
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "av1,h264,vp9"
+ },
+ {
+ "AudioCodec": "aac,mp3,mp2",
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "h264"
}
],
"TranscodingProfiles": [
{
- "Container": "ts",
- "Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "hls",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
- "Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 2,
- "SegmentLength": 0,
"BreakOnNonKeyFrames": true,
- "$type": "TranscodingProfile"
+ "Container": "mp4",
+ "Context": "Streaming",
+ "EnableAudioVbrEncoding": true,
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "Protocol": "hls",
+ "Type": "Audio"
},
{
- "Container": "aac",
- "Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "aac",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "mp3",
- "Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "mp3",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "opus",
- "Type": "Audio",
"AudioCodec": "opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "opus",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "wav",
- "Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "wav",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "opus",
- "Type": "Audio",
"AudioCodec": "opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "opus",
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "mp3",
- "Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "mp3",
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "aac",
- "Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "aac",
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "wav",
- "Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "wav",
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "Protocol": "http",
+ "Type": "Audio"
},
{
- "Container": "ts",
- "Type": "Video",
- "VideoCodec": "h264",
- "AudioCodec": "aac,mp3",
- "Protocol": "hls",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
- "Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 2,
- "SegmentLength": 0,
+ "AudioCodec": "aac,mp2,opus,flac",
"BreakOnNonKeyFrames": true,
- "$type": "TranscodingProfile"
- },
- {
- "Container": "webm",
- "Type": "Video",
- "VideoCodec": "vp8,vp9,av1,vpx",
- "AudioCodec": "vorbis,opus",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
+ "Container": "mp4",
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "Protocol": "hls",
+ "Type": "Video",
+ "VideoCodec": "av1,h264,vp9"
},
{
- "Container": "mp4",
+ "AudioCodec": "aac,mp3,mp2",
+ "BreakOnNonKeyFrames": true,
+ "Container": "ts",
+ "Context": "Streaming",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "Protocol": "hls",
"Type": "Video",
- "VideoCodec": "h264",
- "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
- "Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "VideoCodec": "h264"
}
],
"CodecProfiles": [
{
- "Type": "VideoAudio",
+ "Codec": "aac",
"Conditions": [
{
"Condition": "Equals",
+ "IsRequired": false,
"Property": "IsSecondaryAudio",
- "Value": "false",
+ "Value": "false"
+ }
+ ],
+ "Type": "VideoAudio"
+ },
+ {
+ "Conditions": [
+ {
+ "Condition": "LessThanEqual",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "AudioChannels",
+ "Value": "2"
}
],
- "Codec": "aac",
- "$type": "CodecProfile"
+ "Type": "Audio"
},
{
- "Type": "VideoAudio",
"Conditions": [
{
+ "Condition": "LessThanEqual",
+ "IsRequired": false,
+ "Property": "AudioChannels",
+ "Value": "2"
+ },
+ {
"Condition": "Equals",
- "Property": "IsSecondaryAudio",
- "Value": "false",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "IsSecondaryAudio",
+ "Value": "false"
}
],
- "$type": "CodecProfile"
+ "Type": "VideoAudio"
},
{
- "Type": "Video",
+ "Codec": "h264",
"Conditions": [
{
"Condition": "NotEquals",
- "Property": "IsAnamorphic",
- "Value": "true",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "IsAnamorphic",
+ "Value": "true"
},
{
"Condition": "EqualsAny",
+ "IsRequired": false,
"Property": "VideoProfile",
- "Value": "high|main|baseline|constrained baseline",
+ "Value": "high|main|baseline|constrained baseline"
+ },
+ {
+ "Condition": "EqualsAny",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "VideoRangeType",
+ "Value": "SDR"
},
{
"Condition": "LessThanEqual",
- "Property": "VideoLevel",
- "Value": "52",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "VideoLevel",
+ "Value": "52"
},
{
"Condition": "NotEquals",
- "Property": "IsInterlaced",
- "Value": "true",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "IsInterlaced",
+ "Value": "true"
}
],
- "Codec": "h264",
- "$type": "CodecProfile"
+ "Type": "Video"
},
{
- "Type": "Video",
+ "Codec": "hevc",
"Conditions": [
{
"Condition": "NotEquals",
- "Property": "IsAnamorphic",
- "Value": "true",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "IsAnamorphic",
+ "Value": "true"
},
{
"Condition": "EqualsAny",
+ "IsRequired": false,
"Property": "VideoProfile",
- "Value": "main",
+ "Value": "main"
+ },
+ {
+ "Condition": "EqualsAny",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "VideoRangeType",
+ "Value": "SDR"
},
{
"Condition": "LessThanEqual",
- "Property": "VideoLevel",
- "Value": "120",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "VideoLevel",
+ "Value": "120"
},
{
"Condition": "NotEquals",
+ "IsRequired": false,
"Property": "IsInterlaced",
- "Value": "true",
+ "Value": "true"
+ }
+ ],
+ "Type": "Video"
+ },
+ {
+ "Codec": "vp9",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
"IsRequired": false,
- "$type": "ProfileCondition"
+ "Property": "VideoRangeType",
+ "Value": "SDR"
}
],
- "Codec": "hevc",
- "$type": "CodecProfile"
+ "Type": "Video"
+ },
+ {
+ "Codec": "av1",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "IsRequired": false,
+ "Property": "IsAnamorphic",
+ "Value": "true"
+ },
+ {
+ "Condition": "EqualsAny",
+ "IsRequired": false,
+ "Property": "VideoProfile",
+ "Value": "main"
+ },
+ {
+ "Condition": "EqualsAny",
+ "IsRequired": false,
+ "Property": "VideoRangeType",
+ "Value": "SDR"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "IsRequired": false,
+ "Property": "VideoLevel",
+ "Value": "19"
+ }
+ ],
+ "Type": "Video"
}
],
"ResponseProfiles": [
{
"Container": "m4v",
- "Type": "Video",
"MimeType": "video/mp4",
- "$type": "ResponseProfile"
+ "Type": "Video"
}
],
"SubtitleProfiles": [
{
"Format": "vtt",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ass",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ssa",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
}
],
"$type": "DeviceProfile"
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
index 3b5a0c254..f61d0e36b 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
@@ -16,211 +16,160 @@
"DirectPlayProfiles": [
{
"Container": "webm",
- "AudioCodec": "vorbis",
- "VideoCodec": "vp8,vp9",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "vp8",
+ "AudioCodec": "vorbis,opus"
},
{
"Container": "mp4,m4v",
- "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
- "VideoCodec": "h264,vp8,vp9",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "h264,hevc,vp9",
+ "AudioCodec": "aac,ac3,eac3,opus,flac,alac"
},
{
"Container": "mov",
- "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
- "VideoCodec": "h264",
"Type": "Video",
- "$type": "DirectPlayProfile"
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,ac3,eac3,opus,flac,alac"
+ },
+ {
+ "Container": "ts",
+ "AudioCodec": "mp3",
+ "Type": "Audio"
},
{
"Container": "mp3",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4a",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4b",
"AudioCodec": "aac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
- "Container": "flac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Container": "mp4",
+ "AudioCodec": "flac",
+ "Type": "Audio"
},
{
"Container": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4a",
"AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "m4b",
"AudioCodec": "alac",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "webm",
"AudioCodec": "webma",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
},
{
"Container": "wav",
- "Type": "Audio",
- "$type": "DirectPlayProfile"
+ "Type": "Audio"
+ },
+ {
+ "Container": "mp4",
+ "AudioCodec": "opus",
+ "Type": "Audio"
+ },
+ {
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "hevc,h264,vp9",
+ "AudioCodec": "aac,ac3,eac3,opus,flac,alac"
+ },
+ {
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,ac3,eac3"
}
],
"TranscodingProfiles": [
{
- "Container": "aac",
+ "Container": "mp4",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "hls",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
+ "Protocol": "hls",
"MaxAudioChannels": "6",
- "MinSegments": 2,
- "SegmentLength": 0,
+ "MinSegments": "2",
"BreakOnNonKeyFrames": true,
- "$type": "TranscodingProfile"
+ "EnableAudioVbrEncoding": true
},
{
"Container": "aac",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "mp3",
"Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "wav",
"Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Streaming",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "mp3",
"Type": "Audio",
"AudioCodec": "mp3",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "aac",
"Type": "Audio",
"AudioCodec": "aac",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "wav",
"Type": "Audio",
"AudioCodec": "wav",
- "Protocol": "http",
- "EstimateContentLength": false,
- "EnableMpegtsM2TsMode": false,
- "TranscodeSeekInfo": "Auto",
- "CopyTimestamps": false,
"Context": "Static",
- "EnableSubtitlesInManifest": false,
- "MaxAudioChannels": "6",
- "MinSegments": 0,
- "SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
- "$type": "TranscodingProfile"
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
},
{
"Container": "mp4",
"Type": "Video",
- "AudioCodec": "aac,ac3,eac3,flac,alac",
- "VideoCodec": "hevc,h264",
+ "AudioCodec": "aac,ac3,eac3,opus,flac,alac",
+ "VideoCodec": "hevc,h264,vp9",
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
@@ -237,121 +186,170 @@
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true
- },
- {
- "Container": "webm",
- "Type": "Video",
- "AudioCodec": "vorbis",
- "VideoCodec": "vp8,vpx",
- "Context": "Streaming",
- "Protocol": "http",
- "MaxAudioChannels": "2"
- },
- {
- "Container": "mp4",
- "Type": "Video",
- "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
- "VideoCodec": "h264",
- "Context": "Static",
- "Protocol": "http"
}
],
+ "ContainerProfiles": [],
"CodecProfiles": [
{
"Type": "Video",
+ "Container": "hls",
+ "SubContainer": "mp4",
+ "Codec": "h264",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline|high 10",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "h264",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "high|main|baseline|constrained baseline",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR",
+ "IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "52",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "NotEquals",
"Property": "IsInterlaced",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
}
- ],
- "Codec": "h264",
- "$type": "CodecProfile"
+ ]
},
{
"Type": "Video",
+ "Codec": "hevc",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "main|main 10",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG|DOVI|DOVIWithHDR10|DOVIWithHLG|DOVIWithSDR",
+ "IsRequired": false
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "183",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
},
{
"Condition": "NotEquals",
"Property": "IsInterlaced",
"Value": "true",
- "IsRequired": false,
- "$type": "ProfileCondition"
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoCodecTag",
+ "Value": "hvc1|dvh1",
+ "IsRequired": true
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoFramerate",
+ "Value": "60",
+ "IsRequired": true
}
- ],
- "Codec": "hevc",
- "$type": "CodecProfile"
- }
- ],
- "ResponseProfiles": [
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "vp9",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ }
+ ]
+ },
{
- "Container": "m4v",
"Type": "Video",
- "MimeType": "video/mp4",
- "$type": "ResponseProfile"
+ "Codec": "av1",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "15",
+ "IsRequired": false
+ }
+ ]
}
],
"SubtitleProfiles": [
{
"Format": "vtt",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ass",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
},
{
"Format": "ssa",
- "Method": "External",
- "$type": "SubtitleProfile"
+ "Method": "External"
}
],
- "$type": "DeviceProfile"
+ "ResponseProfiles": [
+ {
+ "Type": "Video",
+ "Container": "m4v",
+ "MimeType": "video/mp4"
+ }
+ ]
}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json
new file mode 100644
index 000000000..094b0723b
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json
@@ -0,0 +1,355 @@
+{
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 384000,
+ "DirectPlayProfiles": [
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "VideoCodec": "vp8,vp9,av1",
+ "AudioCodec": "vorbis,opus"
+ },
+ {
+ "Container": "mp4,m4v",
+ "Type": "Video",
+ "VideoCodec": "h264,hevc,mpeg2video,vc1,vp9,av1",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "mkv",
+ "Type": "Video",
+ "VideoCodec": "h264,hevc,mpeg2video,vc1,vp9,av1",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "m2ts",
+ "Type": "Video",
+ "VideoCodec": "h264,vc1,mpeg2video",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "wmv",
+ "Type": "Video",
+ "VideoCodec": "",
+ "AudioCodec": ""
+ },
+ {
+ "Container": "ts,mpegts",
+ "Type": "Video",
+ "VideoCodec": "h264,hevc,vc1,mpeg2video",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "asf",
+ "Type": "Video",
+ "VideoCodec": "",
+ "AudioCodec": ""
+ },
+ {
+ "Container": "avi",
+ "Type": "Video",
+ "VideoCodec": "",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "mpg",
+ "Type": "Video",
+ "VideoCodec": "",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "mpeg",
+ "Type": "Video",
+ "VideoCodec": "",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "mov",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "opus",
+ "Type": "Audio"
+ },
+ {
+ "Container": "ts",
+ "AudioCodec": "mp3",
+ "Type": "Audio"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "aac",
+ "Type": "Audio"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "aac",
+ "Type": "Audio"
+ },
+ {
+ "Container": "mp4",
+ "AudioCodec": "flac",
+ "Type": "Audio"
+ },
+ {
+ "Container": "webma",
+ "Type": "Audio"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "webma",
+ "Type": "Audio"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio"
+ },
+ {
+ "Container": "hls",
+ "Type": "Video",
+ "VideoCodec": "h264,hevc",
+ "AudioCodec": "aac,ac3,eac3,mp2"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "6",
+ "MinSegments": "1",
+ "BreakOnNonKeyFrames": false,
+ "EnableAudioVbrEncoding": true
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Context": "Static",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Context": "Static",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Context": "Static",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Context": "Static",
+ "Protocol": "http",
+ "MaxAudioChannels": "6"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "AudioCodec": "aac,ac3,eac3,mp2",
+ "VideoCodec": "h264,hevc",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "6",
+ "MinSegments": "1",
+ "BreakOnNonKeyFrames": false
+ }
+ ],
+ "ContainerProfiles": [],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Codec": "flac",
+ "Conditions": [
+ {
+ "Condition": "LessThanEqual",
+ "Property": "AudioChannels",
+ "Value": "2",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "h264",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR",
+ "IsRequired": false
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "52",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Container": "-mp4,ts",
+ "Codec": "hevc",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "hevc",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG|DOVI|DOVIWithHDR10|DOVIWithHLG|DOVIWithSDR",
+ "IsRequired": false
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "183",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "vp9",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ }
+ ]
+ },
+ {
+ "Type": "Video",
+ "Codec": "av1",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|HDR10|HLG",
+ "IsRequired": false
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "15",
+ "IsRequired": false
+ }
+ ]
+ }
+ ],
+ "SubtitleProfiles": [],
+ "ResponseProfiles": [
+ {
+ "Type": "Video",
+ "Container": "m4v",
+ "MimeType": "video/mp4"
+ }
+ ]
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json
new file mode 100644
index 000000000..2fdd33276
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json
@@ -0,0 +1,95 @@
+{
+ "Id": "e313fd4bfdfcab326b1fea833cffd779",
+ "Path": "/Media/MyVideo-dovi-p5.mkv",
+ "Type": "Default",
+ "Container": "mkv",
+ "Size": 199246498,
+ "Name": "MyVideo-dovi-p5",
+ "IsRemote": false,
+ "ETag": "3c932ee1cd94e3fecebcc3fac15053e9",
+ "RunTimeTicks": 562000000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": false,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "dvhe",
+ "Language": "und",
+ "DvVersionMajor": 1,
+ "DvVersionMinor": 0,
+ "DvProfile": 5,
+ "DvLevel": 9,
+ "RpuPresentFlag": 1,
+ "ElPresentFlag": 0,
+ "BlPresentFlag": 1,
+ "DvBlSignalCompatibilityId": 0,
+ "TimeBase": "1/60000",
+ "VideoRange": "HDR",
+ "VideoRangeType": "DOVI",
+ "VideoDoViTitle": "Dolby Vision Profile 5",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "4K HEVC Dolby Vision Profile 5",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitRate": 27713921,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 2160,
+ "Width": 3840,
+ "AverageFrameRate": 60,
+ "RealFrameRate": 60,
+ "ReferenceFrameRate": 60,
+ "Profile": "Main 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 153,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "eac3",
+ "CodecTag": "ec-3",
+ "Language": "und",
+ "TimeBase": "1/48000",
+ "Title": "sound handler",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "DolbyAtmos",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "sound handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "5.1",
+ "BitRate": 640000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "Dolby Digital Plus + Dolby Atmos",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Bitrate": 28362490,
+ "RequiredHttpHeaders": {},
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": -1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json
new file mode 100644
index 000000000..c4197fe31
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json
@@ -0,0 +1,97 @@
+{
+ "Protocol": "File",
+ "Id": "ac2a9824755fbeffd891b8ff2634901a",
+ "Path": "/Media/MyVideo-dovi-p8.mkv",
+ "Type": "Default",
+ "Container": "mkv",
+ "Size": 344509829,
+ "Name": "MyVideo-dovi-p8",
+ "ETag": "8ac40cacc99e4748bc9218045b38d184",
+ "RunTimeTicks": 1781120000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": false,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "und",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "DvVersionMajor": 1,
+ "DvVersionMinor": 0,
+ "DvProfile": 8,
+ "DvLevel": 5,
+ "RpuPresentFlag": 1,
+ "ElPresentFlag": 0,
+ "BlPresentFlag": 1,
+ "DvBlSignalCompatibilityId": 1,
+ "TimeBase": "1/60000",
+ "VideoRange": "HDR",
+ "VideoRangeType": "DOVIWithHDR10",
+ "VideoDoViTitle": "Dolby Vision Profile 8.1 (HDR10)",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "1080p HEVC Dolby Vision Profile 8.1 (HDR10)",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitRate": 15091058,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 1080,
+ "Width": 1920,
+ "AverageFrameRate": 59.94006,
+ "RealFrameRate": 59.94006,
+ "ReferenceFrameRate": 59.94006,
+ "Profile": "Main 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 153,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "eac3",
+ "CodecTag": "ec-3",
+ "Language": "und",
+ "TimeBase": "1/48000",
+ "Title": "Bento4 Sound Handler",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "DolbyAtmos",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "5.1",
+ "BitRate": 640000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "Dolby Digital Plus + Dolby Atmos",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Formats": [],
+ "Bitrate": 15473851,
+ "DefaultAudioStreamIndex": 1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json
new file mode 100644
index 000000000..4f6d5bf00
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json
@@ -0,0 +1,71 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json
new file mode 100644
index 000000000..b2dda6c5d
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json
@@ -0,0 +1,82 @@
+{
+ "Protocol": "File",
+ "Id": "a6e78000340509437325708e41b9e3bb",
+ "Path": "/Media/hi10p.mkv",
+ "Type": "Default",
+ "Container": "mkv",
+ "Size": 58211635,
+ "Name": "MyVideo-hi10p-brokenfps",
+ "IsRemote": false,
+ "ETag": "60c03cb8a315fb6538439d3bb7e6944b",
+ "RunTimeTicks": 920115000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "TimeBase": "1/1000",
+ "VideoRange": "SDR",
+ "VideoRangeType": "SDR",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "4",
+ "IsInterlaced": false,
+ "IsAVC": true,
+ "BitRate": 5075104,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 1000,
+ "RealFrameRate": 23.976025,
+ "ReferenceFrameRate": 23.976025,
+ "Profile": "High 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 51,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "aac",
+ "TimeBase": "1/1000",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "AAC - Stereo - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "stereo",
+ "BitRate": 192000,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "LC",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Formats": [],
+ "Bitrate": 5061248,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": -1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json
new file mode 100644
index 000000000..96e3caffc
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json
@@ -0,0 +1,94 @@
+{
+ "Id": "a5365160a83cb0c518cc1c9ead31dbc7",
+ "Path": "/Media/MyVideo-dovi-p5.mp4",
+ "Type": "Default",
+ "Container": "mp4",
+ "Size": 345485021,
+ "Name": "MyVideo-dovi-p5",
+ "IsRemote": false,
+ "ETag": "a1aa7e722b9af5125b7387d0f58d463e",
+ "RunTimeTicks": 1781120000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "dvh1",
+ "Language": "und",
+ "DvVersionMajor": 1,
+ "DvVersionMinor": 0,
+ "DvProfile": 5,
+ "DvLevel": 5,
+ "RpuPresentFlag": 1,
+ "ElPresentFlag": 0,
+ "BlPresentFlag": 1,
+ "DvBlSignalCompatibilityId": 0,
+ "TimeBase": "1/60000",
+ "VideoRange": "HDR",
+ "VideoRangeType": "DOVI",
+ "VideoDoViTitle": "Dolby Vision Profile 5",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "1080p HEVC Dolby Vision Profile 5",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitRate": 15135631,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 1080,
+ "Width": 1920,
+ "AverageFrameRate": 59.94006,
+ "RealFrameRate": 59.94006,
+ "ReferenceFrameRate": 59.94006,
+ "Profile": "Main 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 153,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "eac3",
+ "CodecTag": "ec-3",
+ "Language": "und",
+ "TimeBase": "1/48000",
+ "Title": "Bento4 Sound Handler",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "DolbyAtmos",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "5.1",
+ "BitRate": 640000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "Dolby Digital Plus + Dolby Atmos",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Bitrate": 15517652,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": -1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json
new file mode 100644
index 000000000..6f77a8805
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json
@@ -0,0 +1,97 @@
+{
+ "Protocol": "File",
+ "Id": "ac2a9824755fbeffd891b8ff2634901a",
+ "Path": "/Media/MyVideo-dovi-p8.mp4",
+ "Type": "Default",
+ "Container": "mp4",
+ "Size": 344509829,
+ "Name": "MyVideo-dovi-p8",
+ "ETag": "8ac40cacc99e4748bc9218045b38d184",
+ "RunTimeTicks": 1781120000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": false,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "und",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "DvVersionMajor": 1,
+ "DvVersionMinor": 0,
+ "DvProfile": 8,
+ "DvLevel": 5,
+ "RpuPresentFlag": 1,
+ "ElPresentFlag": 0,
+ "BlPresentFlag": 1,
+ "DvBlSignalCompatibilityId": 1,
+ "TimeBase": "1/60000",
+ "VideoRange": "HDR",
+ "VideoRangeType": "DOVIWithHDR10",
+ "VideoDoViTitle": "Dolby Vision Profile 8.1 (HDR10)",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "1080p HEVC Dolby Vision Profile 8.1 (HDR10)",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitRate": 15091058,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 1080,
+ "Width": 1920,
+ "AverageFrameRate": 59.94006,
+ "RealFrameRate": 59.94006,
+ "ReferenceFrameRate": 59.94006,
+ "Profile": "Main 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 153,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "eac3",
+ "CodecTag": "ec-3",
+ "Language": "und",
+ "TimeBase": "1/48000",
+ "Title": "Bento4 Sound Handler",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "DolbyAtmos",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "5.1",
+ "BitRate": 640000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "Dolby Digital Plus + Dolby Atmos",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Formats": [],
+ "Bitrate": 15473851,
+ "DefaultAudioStreamIndex": 1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json
new file mode 100644
index 000000000..2e05e70d6
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json
@@ -0,0 +1,100 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": false,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203
+ },
+ {
+ "Codec": "mp3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - MP3 - Stereo",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": false,
+ "Index": 3,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 4,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 4
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json
new file mode 100644
index 000000000..1296bece5
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json
@@ -0,0 +1,86 @@
+{
+ "Protocol": "File",
+ "Id": "a6e78000340509437325708e41b9e3bb",
+ "Path": "/Media/hi10p.mp4",
+ "Type": "Default",
+ "Container": "mov",
+ "Size": 58211635,
+ "Name": "MyVideo-hi10p",
+ "IsRemote": false,
+ "ETag": "8ad487e37ce9578122bbd8c42be2a392",
+ "RunTimeTicks": 920115000,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "VideoType": "VideoFile",
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "und",
+ "TimeBase": "1/16000",
+ "VideoRange": "SDR",
+ "VideoRangeType": "SDR",
+ "AudioSpatialFormat": "None",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "4",
+ "IsInterlaced": false,
+ "IsAVC": true,
+ "BitRate": 4820299,
+ "BitDepth": 10,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 24.007952,
+ "RealFrameRate": 23.976025,
+ "ReferenceFrameRate": 24.007952,
+ "Profile": "High 10",
+ "Type": "Video",
+ "AspectRatio": "16:9",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuv420p10le",
+ "Level": 51,
+ "IsAnamorphic": false
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "und",
+ "TimeBase": "1/48000",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "AAC - Stereo - Default",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "stereo",
+ "BitRate": 257358,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Profile": "LC",
+ "Type": "Audio",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ }
+ ],
+ "MediaAttachments": [],
+ "Formats": [],
+ "Bitrate": 5061248,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": -1,
+ "HasSegments": false
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 183ec8984..6b1398695 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result.Where(v => v.ExtraType is null));
- Assert.Single(result.Where(v => v.ExtraType is not null));
+ Assert.Single(result, v => v.ExtraType is null);
+ Assert.Single(result, v => v.ExtraType is not null);
}
[Fact]
@@ -44,8 +44,8 @@ namespace Jellyfin.Naming.Tests.Video
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result.Where(v => v.ExtraType is null));
- Assert.Single(result.Where(v => v.ExtraType is not null));
+ Assert.Single(result, v => v.ExtraType is null);
+ Assert.Single(result, v => v.ExtraType is not null);
Assert.Equal(2, result[0].AlternateVersions.Count);
}
@@ -357,6 +357,45 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void TestMultiVersion13()
+ {
+ var files = new[]
+ {
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
+ Assert.Equal(11, result[0].AlternateVersions.Count);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv", result[0].AlternateVersions[1].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[2].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv", result[0].AlternateVersions[3].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv", result[0].AlternateVersions[4].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv", result[0].AlternateVersions[5].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv", result[0].AlternateVersions[6].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[7].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv", result[0].AlternateVersions[8].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[9].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[10].Path);
+ }
+
+ [Fact]
public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
{
var files = new[]
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index 5dd3eb8ab..0c7d2487c 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -580,6 +580,7 @@ namespace Jellyfin.Providers.Tests.Manager
CallBase = true
};
item.Setup(m => m.IsSaveLocalMetadataEnabled()).Returns(false);
+ item.Setup(m => m.GetInternalMetadataPath()).Returns(string.Empty);
var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
for (int i = 0; i < count; i++)
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index cced2b1e2..c227883b5 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -574,7 +574,8 @@ namespace Jellyfin.Providers.Tests.Manager
libraryManager.Object,
baseItemManager!,
Mock.Of<ILyricManager>(),
- Mock.Of<IMemoryCache>());
+ Mock.Of<IMemoryCache>(),
+ Mock.Of<IMediaSegmentManager>());
return providerManager;
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
deleted file mode 100644
index c267d3dd3..000000000
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.Net;
-using System.Threading.Tasks;
-using Xunit;
-
-namespace Jellyfin.Server.Integration.Tests.Controllers;
-
-public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory>
-{
- private readonly JellyfinApplicationFactory _factory;
- private static string? _accessToken;
-
- public SessionControllerTests(JellyfinApplicationFactory factory)
- {
- _factory = factory;
- }
-
- [Fact]
- public async Task GetSessions_NonExistentUserId_NotFound()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- using var response = await client.GetAsync($"Sessions?controllableByUserId={Guid.NewGuid()}");
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 3721d1f7a..12d6e1934 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -157,7 +157,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_parser.Fetch(result, "Test Data/Sonarr-Thumb.nfo", CancellationToken.None);
- Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Primary));
+ Assert.Single(result.RemoteImages, x => x.Type == ImageType.Primary);
Assert.Equal("https://artworks.thetvdb.com/banners/episodes/359095/7081317.jpg", result.RemoteImages.First(x => x.Type == ImageType.Primary).Url);
}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 20a8f6152..b9833c225 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -220,7 +220,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_parser.Fetch(result, "Test Data/Fanart.nfo", CancellationToken.None);
- Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Backdrop));
+ Assert.Single(result.RemoteImages, x => x.Type == ImageType.Backdrop);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg", result.RemoteImages.First(x => x.Type == ImageType.Backdrop).Url);
}