diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index cfb5a6ec25..b522412088 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 513139ea57..f730408f29 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 c6e655d080..85f5d00ca8 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 d01c506dbe..82804852a0 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 af8106c0a6..eaa054ca99 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 4b469f0d73..d79d437d90 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 d890765952..ea4c31f613 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 575f2d7562..ac8bb2516d 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 cdf8df17fc..a9deb1c4a2 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 02937b193a..1bc7c12d0d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -55,7 +55,7 @@
-
+
@@ -80,12 +80,12 @@
-
+
-
+
\ No newline at end of file
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 51f29cf088..12bc22a6ac 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 e860105133..91791a1c82 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 c7a8421c66..a2aeaf0fcd 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
///
protected override TempStoreMode TempStore => TempStoreMode.Memory;
+ ///
+ /// Opens the connection to the database.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ 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();
+ }
+
+ ///
+ /// Saves the items.
+ ///
+ /// The items.
+ /// The cancellation token.
+ ///
+ /// or is null.
+ ///
+ public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(items);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ CheckDisposed();
+
+ var itemsLen = items.Count;
+ var tuples = new ValueTuple, BaseItem, string, List>[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 AncestorIds, BaseItem TopParent, string UserDataKey, List 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 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();
+ }
+
+ // 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();
+ }
+
+ // 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 value)
+ {
+ const char Delimiter = '*';
+
+ var nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan path = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan dateModified = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan 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 widthSpan = value[..nextSegment];
+
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan 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 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;
+ }
+
+ ///
+ /// Internal retrieve from items or users table.
+ ///
+ /// The id.
+ /// BaseItem.
+ /// is null.
+ /// is .
+ 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;
}
+ ///
public int GetCount(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -844,6 +1778,7 @@ namespace Emby.Server.Implementations.Data
}
}
+ ///
public List GetItemList(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -997,6 +1932,7 @@ namespace Emby.Server.Implementations.Data
items.Add(newItem);
}
+ ///
public QueryResult GetItems(InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -1241,6 +2177,7 @@ namespace Emby.Server.Implementations.Data
};
}
+ ///
public List 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);
}
+ ///
public void UpdateInheritedValues()
{
const string Statements = """
@@ -2801,6 +3753,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit();
}
+ ///
public void DeleteItem(Guid id)
{
if (id.IsEmpty())
@@ -2843,6 +3796,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
+ ///
public List GetPeopleNames(InternalPeopleQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -2881,6 +3835,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return list;
}
+ ///
public List GetPeople(InternalPeopleQuery query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -3040,46 +3995,55 @@ AND Type = @InternalPersonType)");
}
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
}
+ ///
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
{
return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
}
+ ///
public List GetStudioNames()
{
return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty());
}
+ ///
public List GetAllArtistNames()
{
return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty());
}
+ ///
public List GetMusicGenreNames()
{
return GetItemValueNames(
@@ -3094,6 +4058,7 @@ AND Type = @InternalPersonType)");
Array.Empty());
}
+ ///
public List GetGenreNames()
{
return GetItemValueNames(
@@ -3571,6 +4536,7 @@ AND Type = @InternalPersonType)");
}
}
+ ///
public void UpdatePeople(Guid itemId, List people)
{
if (itemId.IsEmpty())
@@ -3672,6 +4638,7 @@ AND Type = @InternalPersonType)");
return item;
}
+ ///
public List GetMediaStreams(MediaStreamQuery query)
{
CheckDisposed();
@@ -3720,6 +4687,7 @@ AND Type = @InternalPersonType)");
}
}
+ ///
public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -4074,6 +5042,7 @@ AND Type = @InternalPersonType)");
return item;
}
+ ///
public List GetMediaAttachments(MediaAttachmentQuery query)
{
CheckDisposed();
@@ -4109,6 +5078,7 @@ AND Type = @InternalPersonType)");
return list;
}
+ ///
public void SaveMediaAttachments(
Guid id,
IReadOnlyList attachments,
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 31617d1a5f..6af2a553d6 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 28bb29df85..4b68f21d55 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO
}
}
+ ///
+ 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);
+ }
+ }
+
///
/// Returns a object for the specified file or directory path.
///
@@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO
}
}
- ///
- /// Gets the creation time UTC.
- ///
- /// The path.
- /// DateTime.
+ ///
public virtual DateTime GetCreationTimeUtc(string path)
{
return GetCreationTimeUtc(GetFileSystemInfo(path));
@@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO
}
}
- ///
- /// Gets the last write time UTC.
- ///
- /// The path.
- /// DateTime.
+ ///
public virtual DateTime GetLastWriteTimeUtc(string path)
{
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
@@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO
File.SetAttributes(path, attributes);
}
- ///
- /// Swaps the files.
- ///
- /// The file1.
- /// The file2.
+ ///
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 a2301c8aed..bb45dd87e9 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 48d24385e9..28f7ed6598 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 9172af516e..97aa0ca58c 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 2998489b57..6b3b78fa12 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 ad9e555a3c..ba2e2700da 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 bbb162c776..51c9e87d5a 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 65df1e45b1..ca52ffb143 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 d1410ef5e6..9702ab7123 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 b926d9d30e..f2f657b049 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 075bcc9a44..3b2bb70a95 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 b0ddec1046..ff14c13678 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 dced61c5ed..8a88cf28e9 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 1dba78adde..3caf8b5478 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 76a98aa54b..3ba3e6679e 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 c8e0364248..af57b1693e 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 31d6aaedb9..f205e8b64c 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 961d1a0df7..6b0cfb3594 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 c8ed7d0fbc..10f4aee251 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 ffb4345c89..336d286fc4 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 7ef9079188..e149f8adfd 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 7476525387..b1b6e96ea1 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 39e7cd546d..7d101195b7 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 a24a837aba..33b0bb7e15 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 d9867f5e05..9f4f58cb69 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 ff9a0d4f42..7e9be76e51 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 2f52aafa36..bf59e15837 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 a9b6fbeef4..66d8bf899b 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 110af11b71..19be1a23e0 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 a4e2302d16..5cf54522bf 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 1dceadc611..a3cf78fcb2 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 97bad45323..3fddc2e780 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 32e2f4bab1..f890ea74dc 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 4bec590fbe..9a0e2115ed 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 f06bbc5912..81d5b83d61 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 896f47923f..eb55e32c50 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 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 RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList 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 0000000000..d6fad7526b
--- /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;
+
+///
+/// Task to obtain media segments.
+///
+public class MediaSegmentExtractionTask : IScheduledTask
+{
+ ///
+ /// The library manager.
+ ///
+ 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];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The library manager.
+ /// The localization manager.
+ /// The segment manager.
+ public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager)
+ {
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _mediaSegmentManager = mediaSegmentManager;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments");
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription");
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ ///
+ public string Key => "TaskExtractMediaSegments";
+
+ ///
+ public async Task ExecuteAsync(IProgress 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);
+ }
+
+ ///
+ public IEnumerable 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 72e164b521..6a8ad2bdc5 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;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
public SessionManager(
ILogger 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
+ };
+ }
+
///
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);
}
+ ///
+ public IReadOnlyList 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();
+ }
+
///
public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken)
{
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 2a2ab4ad16..50050262f0 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
/// An containing the list of devices.
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetDevices([FromQuery] Guid? userId)
+ public ActionResult> 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 GetDeviceInfo([FromQuery, Required] string id)
+ public ActionResult 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 GetDeviceOptions([FromQuery, Required] string id)
+ public ActionResult 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 662e2acbc7..54e0527c90 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
/// Optional. The max height.
/// Optional. Whether to enable subtitles in the manifest.
/// Optional. Whether to enable Audio Encoding.
+ /// Whether to always burn in subtitles when transcoding.
/// Hls live stream retrieved.
/// A containing the hls file.
[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
/// Enable adaptive bitrate streaming.
/// Enable trickplay image playlists being added to master playlist.
/// Whether to enable Audio Encoding.
+ /// Whether to always burn in subtitles when transcoding.
/// Video stream returned.
/// A containing the playlist file.
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -457,7 +461,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] Dictionary 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
/// Optional. The .
/// Optional. The streaming options.
/// Optional. Whether to enable Audio Encoding.
+ /// Whether to always burn in subtitles when transcoding.
/// Video stream returned.
/// A containing the audio file.
[HttpGet("Videos/{itemId}/main.m3u8")]
@@ -797,7 +805,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary 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
/// Optional. The .
/// Optional. The streaming options.
/// Optional. Whether to enable Audio Encoding.
+ /// Whether to always burn in subtitles when transcoding.
/// Video stream returned.
/// A containing the audio file.
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1146,7 +1158,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary 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 bc52be1842..f22ac0b73a 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
/// The open live stream dto.
/// Whether to enable direct play. Default: true.
/// Whether to enable direct stream. Default: true.
+ /// Always burn-in subtitle when transcoding.
/// Media source opened.
/// A containing a .
[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 e97704d48d..3dc5167a2e 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>> GetSegmentsAsync(
+ public async Task>> GetItemSegments(
[FromRoute, Required] Guid itemId,
[FromQuery] IEnumerable? includeSegmentTypes = null)
{
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 63d6e1cc30..e6f23b1364 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -149,6 +149,37 @@ public class PlaylistsController : BaseJellyfinApiController
return NoContent();
}
+ ///
+ /// Get a playlist.
+ ///
+ /// The playlist id.
+ /// The playlist.
+ /// Playlist not found.
+ ///
+ /// A objects.
+ ///
+ [HttpGet("{playlistId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult 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()
+ };
+ }
+
///
/// Get a playlist's users.
///
@@ -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(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 60de66ab00..2f9e9f091d 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;
///
/// Initializes a new instance of the class.
///
/// Instance of interface.
/// Instance of interface.
- /// Instance of interface.
public SessionController(
ISessionManager sessionManager,
- IUserManager userManager,
- IDeviceManager deviceManager)
+ IUserManager userManager)
{
_sessionManager = sessionManager;
_userManager = userManager;
- _deviceManager = deviceManager;
}
///
@@ -56,67 +48,22 @@ public class SessionController : BaseJellyfinApiController
/// Filter by device Id.
/// Optional. Filter by sessions that were active in the last n seconds.
/// List of sessions returned.
- /// An with the available sessions.
+ /// An with the available sessions.
[HttpGet("Sessions")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetSessions(
+ public ActionResult> 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 60d49af9e3..2cf66144ce 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 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 fe73534967..41c4886d4f 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 effe7b021b..8348fd937d 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 ba92d811cf..0e620e72a9 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);
@@ -406,6 +408,48 @@ public class DynamicHlsHelper
}
}
+ ///
+ /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
+ /// the active streams output Dolby Vision Videos.
+ ///
+ ///
+ ///
+ /// StringBuilder to append the field to.
+ /// StreamState of the current stream.
+ 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('"');
+ }
+
///
/// Appends a RESOLUTION field containing the resolution of the output stream.
///
@@ -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 9bda27031b..4adda0b695 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -156,6 +156,7 @@ public class MediaInfoHelper
/// Enable transcoding.
/// Allow video stream copy.
/// Allow audio stream copy.
+ /// Always burn-in subtitle when transcoding.
/// Requesting IP address.
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 535ef27c3a..3a5db2f3fb 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 53104988f5..978e99b35c 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -65,6 +65,11 @@ public class OpenLiveStreamDto
///
public bool? EnableDirectStream { get; set; }
+ ///
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ ///
+ public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; }
+
///
/// Gets or sets the device profile.
///
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
index 9e12ddde65..82f603ca1e 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.
///
public bool? AutoOpenLiveStream { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
+ ///
+ public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; }
}
diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
index 392ef5ff4e..aad5787097 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;
+
+///
+/// A dto representing custom options for a device.
+///
+public class DeviceOptionsDto
{
///
- /// A dto representing custom options for a device.
+ /// Gets or sets the id.
///
- public class DeviceOptionsDto
- {
- ///
- /// Gets or sets the id.
- ///
- public int Id { get; set; }
+ public int Id { get; set; }
- ///
- /// Gets or sets the device id.
- ///
- public string? DeviceId { get; set; }
+ ///
+ /// Gets or sets the device id.
+ ///
+ public string? DeviceId { get; set; }
- ///
- /// Gets or sets the custom name.
- ///
- public string? CustomName { get; set; }
- }
+ ///
+ /// Gets or sets the custom name.
+ ///
+ public string? CustomName { get; set; }
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index d7a46e2d54..d3bff2936c 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
}
///
- 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
}
///
- 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;
}
///
- 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();
}
///
- 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);
}
///
@@ -135,8 +145,8 @@ namespace Jellyfin.Server.Implementations.Devices
{
IEnumerable 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
}
///
- public QueryResult GetDevicesForUser(Guid? userId)
+ public QueryResult GetDevicesForUser(Guid? userId)
{
IEnumerable 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(array);
+ return new QueryResult(array);
}
///
@@ -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
+ };
+ }
+
+ ///
+ 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 7916d15c92..d641f521b9 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;
///
public class MediaSegmentManager : IMediaSegmentManager
{
+ private readonly ILogger _logger;
private readonly IDbContextFactory _dbProvider;
+ private readonly IMediaSegmentProvider[] _segmentProviders;
+ private readonly ILibraryManager _libraryManager;
///
/// Initializes a new instance of the class.
///
+ /// Logger.
/// EFCore Database factory.
- public MediaSegmentManager(IDbContextFactory dbProvider)
+ /// List of all media segment providers.
+ /// Library manager.
+ public MediaSegmentManager(
+ ILogger logger,
+ IDbContextFactory dbProvider,
+ IEnumerable segmentProviders,
+ ILibraryManager libraryManager)
{
+ _logger = logger;
_dbProvider = dbProvider;
+
+ _segmentProviders = segmentProviders
+ .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
+ .ToArray();
+ _libraryManager = libraryManager;
+ }
+
+ ///
+ 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);
+ }
+ }
}
///
@@ -103,4 +195,21 @@ public class MediaSegmentManager : IMediaSegmentManager
{
return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio;
}
+
+ ///
+ 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 0000000000..8dba31a67f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs
@@ -0,0 +1,712 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("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 0000000000..55b90a54d7
--- /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
+{
+ ///
+ public partial class MarkSegmentProviderIdNonNullable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "SegmentProviderId",
+ table: "MediaSegments",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ 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 399e2a08ac..f6191dd2cd 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("ItemId")
.HasColumnType("TEXT");
+ b.Property("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
b.Property("StartTicks")
.HasColumnType("INTEGER");
b.Property("Type")
.HasColumnType("INTEGER");
- b.Property("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 bb32b7c20e..f6c48498ca 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager
}
///
- 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);
+ }
+ }
+ }
+
+ ///
+ 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
}
///
- public TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir)
+ public TrickplayInfo CreateTiles(IReadOnlyList 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;
}
@@ -355,6 +454,26 @@ public class TrickplayManager : ITrickplayManager
return trickplayResolutions;
}
+ ///
+ public async Task> GetTrickplayItemsAsync(int limit, int offset)
+ {
+ IReadOnlyList 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;
+ }
+
///
public async Task SaveTrickplayInfo(TrickplayInfo info)
{
@@ -392,9 +511,15 @@ public class TrickplayManager : ITrickplayManager
}
///
- public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ public async Task 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;
}
///
@@ -470,29 +595,33 @@ public class TrickplayManager : ITrickplayManager
return null;
}
- private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ ///
+ public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
- var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+ var path = saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
- return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ 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 HasTrickplayResolutionAsync(Guid itemId, int width)
{
- try
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- 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.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 81fecc9a13..9d4441ac39 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)
};
///
@@ -46,6 +47,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
+ typeof(Routines.MoveTrickplayFiles)
};
///
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
index 139a6ec640..8462d0a8c9 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();
}
-#pragma warning restore
}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs
new file mode 100644
index 0000000000..61f5620dc0
--- /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;
+
+///
+public class MigrateEncodingOptions : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An instance of .
+ /// An instance of the interface.
+ public MigrateEncodingOptions(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB");
+
+ ///
+ public string Name => nameof(MigrateEncodingOptions);
+
+ ///
+ public bool PerformOnNewInstall => false;
+
+ ///
+ 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(oldConfig.HardwareAccelerationType, true, out var parsedHardwareAccelerationType))
+ {
+ hardwareAccelerationType = parsedHardwareAccelerationType;
+ }
+
+ var tonemappingAlgorithm = TonemappingAlgorithm.none;
+ if (Enum.TryParse(oldConfig.TonemappingAlgorithm, true, out var parsedTonemappingAlgorithm))
+ {
+ tonemappingAlgorithm = parsedTonemappingAlgorithm;
+ }
+
+ var tonemappingMode = TonemappingMode.auto;
+ if (Enum.TryParse(oldConfig.TonemappingMode, true, out var parsedTonemappingMode))
+ {
+ tonemappingMode = parsedTonemappingMode;
+ }
+
+ var tonemappingRange = TonemappingRange.auto;
+ if (Enum.TryParse(oldConfig.TonemappingRange, true, out var parsedTonemappingRange))
+ {
+ tonemappingRange = parsedTonemappingRange;
+ }
+
+ var encoderPreset = EncoderPreset.superfast;
+ if (Enum.TryParse(oldConfig.TonemappingRange, true, out var parsedEncoderPreset))
+ {
+ encoderPreset = parsedEncoderPreset;
+ }
+
+ var deinterlaceMethod = DeinterlaceMethod.yadif;
+ if (Enum.TryParse(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 0544fe561a..580282a5f5 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 d92c00991b..49960f4305 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(',');
- }
-
- var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
- var xmlWriterSettings = new XmlWriterSettings { Indent = true };
- using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
- networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
+ return;
}
+
+ // 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 0000000000..c1a9e88949
--- /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;
+
+///
+/// Migration to move trickplay files to the new directory.
+///
+public class MoveTrickplayFiles : IMigrationRoutine
+{
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// The logger.
+ public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger)
+ {
+ _trickplayManager = trickplayManager;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
+
+ ///
+ public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
+
+ ///
+ public string Name => "MoveTrickplayFiles";
+
+ ///
+ public bool PerformOnNewInstall => true;
+
+ ///
+ 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
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
+ ///
+ /// The key for a setting that indicates whether the application should detect network status change.
+ ///
+ public const string DetectNetworkChangeKey = "DetectNetworkChange";
+
///
/// Gets a value indicating whether the application should host static web content from the .
///
@@ -102,6 +112,14 @@ namespace MediaBrowser.Controller.Extensions
public static bool GetFFmpegSkipValidation(this IConfiguration configuration)
=> configuration.GetValue(FfmpegSkipValidationKey);
+ ///
+ /// Gets a value indicating whether the server should trade off for performance during FFmpeg image extraction.
+ ///
+ /// The configuration to read the setting from.
+ /// true if the server should trade off for performance during FFmpeg image extraction, otherwise false.
+ public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
+ => configuration.GetValue(FfmpegImgExtractPerfTradeoffKey);
+
///
/// Gets a value indicating whether playlists should allow duplicate entries from the .
///
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index f77186e25c..20f51ddb71 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 24cd141dcd..c120e08fa2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -37,6 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding
///
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 _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 _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
+ private static readonly Dictionary _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(StringComparer.OrdinalIgnoreCase)
+ var codecMap = new Dictionary()
{
- { "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));
+ }
+
///
/// Gets the name of the output video codec.
///
@@ -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
/// Encoding options.
/// Default present to use for encoding.
/// Video bitrate.
- 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();
+ 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 subFilters;
List overlayFilters;
- if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ (mainFilters, subFilters, overlayFilters) = options.HardwareAccelerationType switch
{
- (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) = 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))
+ var decoder = hardwareAccelerationType switch
{
- return GetQsvHwVidDecoder(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, "nvenc", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(decoder))
{
- 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))
- {
- return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth);
- }
-
- if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
- {
- 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,16 +6621,19 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
}
+ if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
+ }
+ }
+
+ if (is8_10_12bitSwFormatsVt)
+ {
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
-
- if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
- }
}
return null;
@@ -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 72df7151da..caa312987d 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 5bf83a9e31..9bf27b3b2e 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -44,5 +44,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// The cancellation token.
/// System.String.
Task GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
+
+ ///
+ /// Gets the path to a subtitle file.
+ ///
+ /// The subtitle stream.
+ /// The media source.
+ /// The cancellation token.
+ /// System.String.
+ Task GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
index 67384f6f64..010d7edb4f 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;
@@ -13,6 +14,15 @@ namespace MediaBrowser.Controller;
///
public interface IMediaSegmentManager
{
+ ///
+ /// Uses all segment providers enabled for the 's library to get the Media Segments.
+ ///
+ /// The Item to evaluate.
+ /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.
+ /// stop request token.
+ /// A task that indicates the Operation is finished.
+ Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken);
+
///
/// Returns if this item supports media segments.
///
@@ -50,4 +60,11 @@ public interface IMediaSegmentManager
/// True if there are any segments stored for the item, otherwise false.
/// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson.
bool HasSegments(Guid itemId);
+
+ ///
+ /// Gets a list of all registered Segment Providers and their IDs.
+ ///
+ /// The media item that should be tested for providers.
+ /// A list of all providers for the tested item.
+ 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 0000000000..39bb58bef2
--- /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;
+
+///
+/// Provides methods for Obtaining the Media Segments from an Item.
+///
+public interface IMediaSegmentProvider
+{
+ ///
+ /// Gets the provider name.
+ ///
+ string Name { get; }
+
+ ///
+ /// Enumerates all Media Segments from an Media Item.
+ ///
+ /// Arguments to enumerate MediaSegments.
+ /// Abort token.
+ /// A list of all MediaSegments found from this provider.
+ Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken);
+
+ ///
+ /// Should return support state for the given item.
+ ///
+ /// The base item to extract segments from.
+ /// True if item is supported, otherwise false.
+ ValueTask Supports(BaseItem item);
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
index 3504831b87..8330745418 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;
///
/// Sessions message.
///
-public class SessionsMessage : OutboundWebSocketMessage>
+public class SessionsMessage : OutboundWebSocketMessage>
{
///
/// Initializes a new instance of the class.
///
/// Session info.
- public SessionsMessage(IReadOnlyList data)
+ public SessionsMessage(IReadOnlyList data)
: base(data)
{
}
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 9e91a8bcd7..0bab2a6b9c 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
///
public bool ReplaceAllMetadata { get; set; }
+ ///
+ /// Gets or sets a value indicating whether all existing trickplay images should be overwritten
+ /// when paired with MetadataRefreshMode=FullRefresh.
+ ///
+ 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 5a47236f92..462a624553 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;
@@ -292,6 +293,17 @@ namespace MediaBrowser.Controller.Session
/// SessionInfo.
SessionInfo GetSession(string deviceId, string client, string version);
+ ///
+ /// Gets all sessions available to a user.
+ ///
+ /// The session identifier.
+ /// The device id.
+ /// Active within session limit.
+ /// Filter for sessions remote controllable for this user.
+ /// Is the request authenticated with API key.
+ /// IReadOnlyList{SessionInfoDto}.
+ IReadOnlyList GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck, bool isApiKey);
+
///
/// Gets the session by authentication token.
///
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 9e33588187..3ba1bfce42 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;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of interface.
+ /// Instance of interface.
public SessionInfo(ISessionManager sessionManager, ILogger logger)
{
_sessionManager = sessionManager;
_logger = logger;
- AdditionalUsers = Array.Empty();
+ AdditionalUsers = [];
PlayState = new PlayerStateInfo();
- SessionControllers = Array.Empty();
- NowPlayingQueue = Array.Empty();
- NowPlayingQueueFullItems = Array.Empty();
+ SessionControllers = [];
+ NowPlayingQueue = [];
+ NowPlayingQueueFullItems = [];
}
+ ///
+ /// Gets or sets the play state.
+ ///
+ /// The play state.
public PlayerStateInfo PlayState { get; set; }
- public SessionUserInfo[] AdditionalUsers { get; set; }
+ ///
+ /// Gets or sets the additional users.
+ ///
+ /// The additional users.
+ public IReadOnlyList AdditionalUsers { get; set; }
+ ///
+ /// Gets or sets the client capabilities.
+ ///
+ /// The client capabilities.
public ClientCapabilities Capabilities { get; set; }
///
@@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session
{
if (Capabilities is null)
{
- return Array.Empty();
+ return [];
}
return Capabilities.PlayableMediaTypes;
@@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session
/// The now playing item.
public BaseItemDto NowPlayingItem { get; set; }
+ ///
+ /// Gets or sets the now playing queue full items.
+ ///
+ /// The now playing queue full items.
[JsonIgnore]
public BaseItem FullNowPlayingItem { get; set; }
+ ///
+ /// Gets or sets the now viewing item.
+ ///
+ /// The now viewing item.
public BaseItemDto NowViewingItem { get; set; }
///
@@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session
///
/// The session controller.
[JsonIgnore]
- public ISessionController[] SessionControllers { get; set; }
+ public IReadOnlyList SessionControllers { get; set; }
+ ///
+ /// Gets or sets the transcoding info.
+ ///
+ /// The transcoding info.
public TranscodingInfo TranscodingInfo { get; set; }
///
@@ -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
}
}
+ ///
+ /// Gets a value indicating whether the session supports media control.
+ ///
+ /// true if this session supports media control; otherwise, false.
public bool SupportsMediaControl
{
get
@@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session
}
}
+ ///
+ /// Gets a value indicating whether the session supports remote control.
+ ///
+ /// true if this session supports remote control; otherwise, false.
public bool SupportsRemoteControl
{
get
@@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session
}
}
+ ///
+ /// Gets or sets the now playing queue.
+ ///
+ /// The now playing queue.
public IReadOnlyList NowPlayingQueue { get; set; }
+ ///
+ /// Gets or sets the now playing queue full items.
+ ///
+ /// The now playing queue full items.
public IReadOnlyList NowPlayingQueueFullItems { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the session has a custom device name.
+ ///
+ /// true if this session has a custom device name; otherwise, false.
public bool HasCustomDeviceName { get; set; }
+ ///
+ /// Gets or sets the playlist item id.
+ ///
+ /// The splaylist item id.
public string PlaylistItemId { get; set; }
+ ///
+ /// Gets or sets the server id.
+ ///
+ /// The server id.
public string ServerId { get; set; }
+ ///
+ /// Gets or sets the user primary image tag.
+ ///
+ /// The user primary image tag.
public string UserPrimaryImageTag { get; set; }
///
@@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session
///
/// The supported commands.
public IReadOnlyList SupportedCommands
- => Capabilities is null ? Array.Empty() : Capabilities.SupportedCommands;
+ => Capabilities is null ? [] : Capabilities.SupportedCommands;
+ ///
+ /// Ensures a controller of type exists.
+ ///
+ /// Class to register.
+ /// The factory.
+ /// Tuple{ISessionController, bool}.
public Tuple EnsureController(Func 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(newController, true);
}
+ ///
+ /// Adds a controller to the session.
+ ///
+ /// The controller.
public void AddController(ISessionController controller)
{
- SessionControllers = [..SessionControllers, controller];
+ SessionControllers = [.. SessionControllers, controller];
}
+ ///
+ /// Gets a value indicating whether the session contains a user.
+ ///
+ /// The user id to check.
+ /// true if this session contains the user; otherwise, false.
public bool ContainsUser(Guid userId)
{
if (UserId.Equals(userId))
@@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session
return false;
}
+ ///
+ /// Starts automatic progressing.
+ ///
+ /// The playback progress info.
+ /// The supported commands.
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
{
if (_disposed)
@@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session
}
}
+ ///
+ /// Stops automatic progressing.
+ ///
public void StopAutomaticProgress()
{
lock (_progressLock)
@@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session
}
}
+ ///
+ /// Disposes the instance async.
+ ///
+ /// ValueTask.
public async ValueTask DisposeAsync()
{
_disposed = true;
@@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session
StopAutomaticProgress();
var controllers = SessionControllers.ToList();
- SessionControllers = Array.Empty();
+ SessionControllers = [];
foreach (var controller in controllers)
{
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 0c41f30235..800317800d 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -18,9 +18,10 @@ public interface ITrickplayManager
///
/// The video.
/// Whether or not existing data should be replaced.
+ /// The library options.
/// CancellationToken to use for operation.
/// Task.
- Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
+ Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
///
/// Creates trickplay tiles out of individual thumbnails.
@@ -33,7 +34,7 @@ public interface ITrickplayManager
///
/// The output directory will be DELETED and replaced if it already exists.
///
- TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir);
+ TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir);
///
/// Get available trickplay resolutions and corresponding info.
@@ -42,6 +43,14 @@ public interface ITrickplayManager
/// Map of width resolutions to trickplay tiles info.
Task> GetTrickplayResolutions(Guid itemId);
+ ///
+ /// Gets the item ids of all items with trickplay info.
+ ///
+ /// The limit of items to return.
+ /// The offset to start the query at.
+ /// The list of item ids that have trickplay info.
+ Task> GetTrickplayItemsAsync(int limit, int offset);
+
///
/// Saves trickplay info.
///
@@ -62,8 +71,29 @@ public interface ITrickplayManager
/// The item.
/// The width of a single thumbnail.
/// The tile's index.
+ /// Whether or not the tile should be saved next to the media file.
/// The absolute path.
- string GetTrickplayTilePath(BaseItem item, int width, int index);
+ Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia);
+
+ ///
+ /// Gets the path to a trickplay tile image.
+ ///
+ /// The item.
+ /// The amount of images for the tile width.
+ /// The amount of images for the tile height.
+ /// The width of a single thumbnail.
+ /// Whether or not the tile should be saved next to the media file.
+ /// The absolute path.
+ string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false);
+
+ ///
+ /// Migrates trickplay images between local and media directories.
+ ///
+ /// The video.
+ /// The library options.
+ /// CancellationToken to use for operation.
+ /// Task.
+ Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
///
/// Gets the trickplay HLS playlist.
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 7e307286a9..431fc0b178 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 2b6ed8fa09..b49fbf2aba 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 764230febe..826ffd0b7e 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 ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken)
+ private async Task 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);
}
///
@@ -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 9ecbfa9cf5..a731d4785b 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -529,11 +529,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
List subtitleStreams,
CancellationToken cancellationToken)
{
- var inputPath = mediaSource.Path;
+ var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
var outputPaths = new List();
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 GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken)
+ .ConfigureAwait(false);
+ return info.Path;
+ }
+
///
public void Dispose()
{
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 42f355b052..57557d55ca 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(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 4c5213d4e2..2720c0bdf6 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"];
}
///
@@ -120,7 +124,7 @@ public class EncodingOptions
///
/// Gets or sets the hardware acceleration type.
///
- public string HardwareAccelerationType { get; set; }
+ public HardwareAccelerationType HardwareAccelerationType { get; set; }
///
/// Gets or sets the FFmpeg path as set by the user via the UI.
@@ -160,17 +164,17 @@ public class EncodingOptions
///
/// Gets or sets the tone-mapping algorithm.
///
- public string TonemappingAlgorithm { get; set; }
+ public TonemappingAlgorithm TonemappingAlgorithm { get; set; }
///
/// Gets or sets the tone-mapping mode.
///
- public string TonemappingMode { get; set; }
+ public TonemappingMode TonemappingMode { get; set; }
///
/// Gets or sets the tone-mapping range.
///
- public string TonemappingRange { get; set; }
+ public TonemappingRange TonemappingRange { get; set; }
///
/// Gets or sets the tone-mapping desaturation.
@@ -210,7 +214,7 @@ public class EncodingOptions
///
/// Gets or sets the encoder preset.
///
- public string EncoderPreset { get; set; }
+ public EncoderPreset? EncoderPreset { get; set; }
///
/// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
@@ -220,7 +224,7 @@ public class EncodingOptions
///
/// Gets or sets the deinterlace method.
///
- public string DeinterlaceMethod { get; set; }
+ public DeinterlaceMethod DeinterlaceMethod { get; set; }
///
/// Gets or sets a value indicating whether 10bit HEVC decoding is enabled.
@@ -232,6 +236,16 @@ public class EncodingOptions
///
public bool EnableDecodingColorDepth10Vp9 { get; set; }
+ ///
+ /// Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled.
+ ///
+ public bool EnableDecodingColorDepth10HevcRext { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled.
+ ///
+ public bool EnableDecodingColorDepth12HevcRext { get; set; }
+
///
/// Gets or sets a value indicating whether the enhanced NVDEC is enabled.
///
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index b0f5c2a11f..6054ba34e5 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();
DisabledSubtitleFetchers = Array.Empty();
+ DisabledMediaSegmentProviders = Array.Empty