aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml93
-rw-r--r--.ci/azure-pipelines-main.yml71
-rw-r--r--.ci/azure-pipelines-package.yml274
-rw-r--r--.ci/azure-pipelines-test.yml98
-rw-r--r--.ci/azure-pipelines.yml64
-rw-r--r--Emby.Photos/PhotoProvider.cs241
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs4
-rw-r--r--Jellyfin.Data/Enums/MediaStreamProtocol.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs194
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs2
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs10
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs10
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs28
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs8
16 files changed, 322 insertions, 791 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
deleted file mode 100644
index 547a514f8..000000000
--- a/.ci/azure-pipelines-abi.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-parameters:
-- name: Packages
- type: object
- default: {}
-- name: LinuxImage
- type: string
- default: "ubuntu-latest"
-- name: DotNetSdkVersion
- type: string
- default: 8.0.x
-
-jobs:
- - job: CompatibilityCheck
- displayName: Compatibility Check
- dependsOn: Build
- condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
-
- pool:
- vmImage: "${{ parameters.LinuxImage }}"
-
- strategy:
- matrix:
- ${{ each Package in parameters.Packages }}:
- ${{ Package.key }}:
- NugetPackageName: ${{ Package.value.NugetPackageName }}
- AssemblyFileName: ${{ Package.value.AssemblyFileName }}
- maxParallel: 2
-
- steps:
- - checkout: none
-
- - task: UseDotNet@2
- displayName: "Update DotNet"
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: DotNetCoreCLI@2
- displayName: 'Install ABI CompatibilityChecker Tool'
- inputs:
- command: custom
- custom: tool
- arguments: 'update compatibilitychecker -g'
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download New Assembly Build Artifact'
- inputs:
- source: 'current'
- artifact: "$(NugetPackageName)"
- path: "$(System.ArtifactsDirectory)/new-artifacts"
- runVersion: "latest"
-
- - task: CopyFiles@2
- displayName: 'Copy New Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/new-release
- cleanTargetFolder: true
- overWrite: true
- flattenFolders: true
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download Reference Assembly Build Artifact'
- enabled: false
- inputs:
- source: "specific"
- artifact: "$(NugetPackageName)"
- path: "$(System.ArtifactsDirectory)/current-artifacts"
- project: "$(System.TeamProjectId)"
- pipeline: "$(System.DefinitionId)"
- runVersion: "latestFromBranch"
- runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
-
- - task: CopyFiles@2
- displayName: 'Copy Reference Assembly Build Artifact'
- enabled: false
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/current-release
- cleanTargetFolder: true
- overWrite: true
- flattenFolders: true
-
- - task: DotNetCoreCLI@2
- displayName: 'Execute ABI Compatibility Check Tool'
- enabled: false
- inputs:
- command: custom
- custom: compat
- arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
- workingDirectory: $(System.ArtifactsDirectory)
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
deleted file mode 100644
index 0702aeb6b..000000000
--- a/.ci/azure-pipelines-main.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-parameters:
- LinuxImage: 'ubuntu-latest'
- RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 8.0.x
-
-jobs:
- - job: Build
- displayName: Build
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- Debug:
- BuildConfiguration: Debug
- pool:
- vmImage: '${{ parameters.LinuxImage }}'
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: DotNetCoreCLI@2
- displayName: 'Publish Server'
- inputs:
- command: publish
- publishWebProjects: false
- projects: '${{ parameters.RestoreBuildProjects }}'
- arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- zipAfterPublish: false
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Naming'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
- artifactName: 'Jellyfin.Naming'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Controller'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
- artifactName: 'Jellyfin.Controller'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Model'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
- artifactName: 'Jellyfin.Model'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Common'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
- artifactName: 'Jellyfin.Common'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Extensions'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
- artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
deleted file mode 100644
index b0684c0d4..000000000
--- a/.ci/azure-pipelines-package.yml
+++ /dev/null
@@ -1,274 +0,0 @@
-jobs:
-- job: BuildPackage
- displayName: 'Build Packages'
-
- strategy:
- matrix:
- CentOS.amd64:
- BuildConfiguration: centos.amd64
- Fedora.amd64:
- BuildConfiguration: fedora.amd64
- Debian.amd64:
- BuildConfiguration: debian.amd64
- Debian.arm64:
- BuildConfiguration: debian.arm64
- Debian.armhf:
- BuildConfiguration: debian.armhf
- Ubuntu.amd64:
- BuildConfiguration: ubuntu.amd64
- Ubuntu.arm64:
- BuildConfiguration: ubuntu.arm64
- Ubuntu.armhf:
- BuildConfiguration: ubuntu.armhf
- Linux.amd64:
- BuildConfiguration: linux.amd64
- Linux.amd64-musl:
- BuildConfiguration: linux.amd64-musl
- Linux.arm64:
- BuildConfiguration: linux.arm64
- Linux.musl-linux-arm64:
- BuildConfiguration: linux.musl-linux-arm64
- Linux.armhf:
- BuildConfiguration: linux.armhf
- Windows.amd64:
- BuildConfiguration: windows.amd64
- MacOS.amd64:
- BuildConfiguration: macos.amd64
- MacOS.arm64:
- BuildConfiguration: macos.arm64
- Portable:
- BuildConfiguration: portable
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
- displayName: 'Build Dockerfile'
-
- - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
- displayName: 'Run Dockerfile (unstable)'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-
- - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
- displayName: 'Run Dockerfile (stable)'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Release'
- inputs:
- targetPath: '$(Build.SourcesDirectory)/deployment/dist'
- artifactName: 'jellyfin-server-$(BuildConfiguration)'
-
- - task: SSH@0
- displayName: 'Create target directory on repository server'
- inputs:
- sshEndpoint: repository
- runOptions: 'inline'
- inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Upload artifacts to repository server'
- inputs:
- sshEndpoint: repository
- sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
- contents: '**'
- targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
-
-- job: OpenAPISpec
- dependsOn: Test
- condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
- displayName: 'Push OpenAPI Spec to repository'
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download OpenAPI Spec'
- inputs:
- source: 'current'
- artifact: "OpenAPI Spec"
- path: "$(System.ArtifactsDirectory)/openapispec"
- runVersion: "latest"
-
- - task: SSH@0
- displayName: 'Create target directory on repository server'
- inputs:
- sshEndpoint: repository
- runOptions: 'inline'
- inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Upload artifacts to repository server'
- inputs:
- sshEndpoint: repository
- sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
- contents: 'openapi.json'
- targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
-
-- job: BuildDocker
- displayName: 'Build Docker'
-
- strategy:
- matrix:
- amd64:
- BuildConfiguration: amd64
- arm64:
- BuildConfiguration: arm64
- armhf:
- BuildConfiguration: armhf
-
- pool:
- vmImage: 'ubuntu-latest'
-
- variables:
- - name: JellyfinVersion
- value: 0.0.0
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: Docker@2
- displayName: 'Push Unstable Image'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- repository: 'jellyfin/jellyfin-server'
- command: buildAndPush
- buildContext: '.'
- Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
- containerRegistry: Docker Hub
- tags: |
- unstable-$(Build.BuildNumber)-$(BuildConfiguration)
- unstable-$(BuildConfiguration)
-
- - task: Docker@2
- displayName: 'Push Stable Image'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- repository: 'jellyfin/jellyfin-server'
- command: buildAndPush
- buildContext: '.'
- Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
- containerRegistry: Docker Hub
- tags: |
- stable-$(Build.BuildNumber)-$(BuildConfiguration)
- $(JellyfinVersion)-$(BuildConfiguration)
-
-- job: CollectArtifacts
- timeoutInMinutes: 20
- displayName: 'Collect Artifacts'
- condition: succeededOrFailed()
- continueOnError: true
- dependsOn:
- - BuildPackage
- - BuildDocker
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - task: SSH@0
- displayName: 'Update Unstable Repository'
- continueOnError: true
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- sshEndpoint: repository
- runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
-
- - task: SSH@0
- displayName: 'Update Stable Repository'
- continueOnError: true
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- sshEndpoint: repository
- runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
-
-- job: PublishNuget
- displayName: 'Publish NuGet packages'
-
- pool:
- vmImage: 'ubuntu-latest'
-
- variables:
- - name: JellyfinVersion
- value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
-
- steps:
- - task: UseDotNet@2
- displayName: 'Use .NET 8.0 sdk'
- inputs:
- packageType: 'sdk'
- version: '8.0.x'
-
- - task: DotNetCoreCLI@2
- displayName: 'Build Stable Nuget packages'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- command: 'custom'
- projects: |
- Jellyfin.Data/Jellyfin.Data.csproj
- MediaBrowser.Common/MediaBrowser.Common.csproj
- MediaBrowser.Controller/MediaBrowser.Controller.csproj
- MediaBrowser.Model/MediaBrowser.Model.csproj
- Emby.Naming/Emby.Naming.csproj
- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
- custom: 'pack'
- arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
-
- - task: DotNetCoreCLI@2
- displayName: 'Build Unstable Nuget packages'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- command: 'custom'
- projects: |
- Jellyfin.Data/Jellyfin.Data.csproj
- MediaBrowser.Common/MediaBrowser.Common.csproj
- MediaBrowser.Controller/MediaBrowser.Controller.csproj
- MediaBrowser.Model/MediaBrowser.Model.csproj
- Emby.Naming/Emby.Naming.csproj
- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
- custom: 'pack'
- arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
-
- - task: PublishBuildArtifacts@1
- displayName: 'Publish Nuget packages'
- inputs:
- pathToPublish: $(Build.ArtifactStagingDirectory)
- artifactName: Jellyfin Nuget Packages
-
- - task: NuGetCommand@2
- displayName: 'Push Nuget packages to stable feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
- nuGetFeedType: 'external'
- publishFeedCredentials: 'NugetOrg'
- allowPackageConflicts: true # This ignores an error if the version already exists
-
- - task: NuGetAuthenticate@1
- displayName: 'Authenticate to unstable Nuget feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-
- - task: NuGetCommand@2
- displayName: 'Push Nuget packages to unstable feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
- nuGetFeedType: 'internal'
- publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
- allowPackageConflicts: true # This ignores an error if the version already exists
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
deleted file mode 100644
index 3549c691c..000000000
--- a/.ci/azure-pipelines-test.yml
+++ /dev/null
@@ -1,98 +0,0 @@
-parameters:
-- name: ImageNames
- type: object
- default:
- Linux: "ubuntu-latest"
- Windows: "windows-latest"
- macOS: "macos-latest"
-- name: TestProjects
- type: string
- default: "tests/**/*Tests.csproj"
-- name: DotNetSdkVersion
- type: string
- default: 8.0.x
-
-jobs:
- - job: Test
- displayName: Test
- strategy:
- matrix:
- ${{ each imageName in parameters.ImageNames }}:
- ${{ imageName.key }}:
- ImageName: ${{ imageName.value }}
- pool:
- vmImage: "$(ImageName)"
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: false
-
- # This is required for the SonarCloud analyzer
- - task: UseDotNet@2
- displayName: "Install .NET SDK 5.x"
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- inputs:
- packageType: sdk
- version: '5.x'
-
- - task: UseDotNet@2
- displayName: "Update DotNet"
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: SonarCloudPrepare@1
- displayName: 'Prepare analysis on SonarCloud'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
- inputs:
- SonarCloud: 'Sonarcloud for Jellyfin'
- organization: 'jellyfin'
- projectKey: 'jellyfin_jellyfin'
-
- - task: DotNetCoreCLI@2
- displayName: 'Run CLI Tests'
- inputs:
- command: "test"
- projects: ${{ parameters.TestProjects }}
- arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
- publishTestResults: true
- testRunTitle: $(Agent.JobName)
- workingDirectory: "$(Build.SourcesDirectory)"
-
- - task: SonarCloudAnalyze@1
- displayName: 'Run Code Analysis'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
-
- - task: SonarCloudPublish@1
- displayName: 'Publish Quality Gate Result'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
-
- - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
- displayName: 'Run ReportGenerator'
- inputs:
- reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
- targetdir: "$(Agent.TempDirectory)/merged/"
- reporttypes: "Cobertura"
-
- ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
- - task: PublishCodeCoverageResults@1
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
- displayName: 'Publish Code Coverage'
- inputs:
- codeCoverageTool: "cobertura"
- #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
- summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
- pathToSources: $(Build.SourcesDirectory)
- failIfCoverageEmpty: true
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish OpenAPI Artifact'
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json"
- artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
deleted file mode 100644
index 19c9caacb..000000000
--- a/.ci/azure-pipelines.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: $(Date:yyyyMMdd)$(Rev:.r)
-
-variables:
-- name: TestProjects
- value: 'tests/**/*Tests.csproj'
-- name: RestoreBuildProjects
- value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-
-pr:
- autoCancel: true
-
-trigger:
- batch: true
- branches:
- include:
- - '*'
- tags:
- include:
- - 'v*'
-
-jobs:
-- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
- - template: azure-pipelines-main.yml
- parameters:
- LinuxImage: 'ubuntu-latest'
- RestoreBuildProjects: $(RestoreBuildProjects)
-
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- - template: azure-pipelines-test.yml
- parameters:
- ImageNames:
- Linux: 'ubuntu-latest'
- Windows: 'windows-latest'
- macOS: 'macos-latest'
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-test.yml
- parameters:
- ImageNames:
- Linux: 'ubuntu-latest'
-
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- - template: azure-pipelines-abi.yml
- parameters:
- Packages:
- Naming:
- NugetPackageName: Jellyfin.Naming
- AssemblyFileName: Emby.Naming.dll
- Controller:
- NugetPackageName: Jellyfin.Controller
- AssemblyFileName: MediaBrowser.Controller.dll
- Model:
- NugetPackageName: Jellyfin.Model
- AssemblyFileName: MediaBrowser.Model.dll
- Common:
- NugetPackageName: Jellyfin.Common
- AssemblyFileName: MediaBrowser.Common.dll
- Extensions:
- NugetPackageName: Jellyfin.Extensions
- AssemblyFileName: Jellyfin.Extensions.dll
- LinuxImage: 'ubuntu-latest'
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-package.yml
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 27329a7f2..e2f1ca813 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -16,167 +16,160 @@ using TagLib.IFD;
using TagLib.IFD.Entries;
using TagLib.IFD.Tags;
-namespace Emby.Photos
+namespace Emby.Photos;
+
+/// <summary>
+/// Metadata provider for photos.
+/// </summary>
+public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
{
+ private readonly ILogger<PhotoProvider> _logger;
+ private readonly IImageProcessor _imageProcessor;
+
+ // These are causing taglib to hang
+ private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
+
/// <summary>
- /// Metadata provider for photos.
+ /// Initializes a new instance of the <see cref="PhotoProvider" /> class.
/// </summary>
- public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
+ /// <param name="logger">The logger.</param>
+ /// <param name="imageProcessor">The image processor.</param>
+ public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
{
- private readonly ILogger<PhotoProvider> _logger;
- private readonly IImageProcessor _imageProcessor;
-
- // These are causing taglib to hang
- private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PhotoProvider" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="imageProcessor">The image processor.</param>
- public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
- {
- _logger = logger;
- _imageProcessor = imageProcessor;
- }
+ _logger = logger;
+ _imageProcessor = imageProcessor;
+ }
- /// <inheritdoc />
- public string Name => "Embedded Information";
+ /// <inheritdoc />
+ public string Name => "Embedded Information";
- /// <inheritdoc />
- public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
{
- if (item.IsFileProtocol)
- {
- var file = directoryService.GetFile(item.Path);
- return file is not null && file.LastWriteTimeUtc != item.DateModified;
- }
-
- return false;
+ var file = directoryService.GetFile(item.Path);
+ return file is not null && file.LastWriteTimeUtc != item.DateModified;
}
- /// <inheritdoc />
- public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- item.SetImagePath(ImageType.Primary, item.Path);
+ return false;
+ }
- // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ item.SetImagePath(ImageType.Primary, item.Path);
+
+ // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
+ {
+ try
{
- try
+ using var file = TagLib.File.Create(item.Path);
+ if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
{
- using (var file = TagLib.File.Create(item.Path))
+ var structure = tag.Structure;
+ if (structure?.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
- if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
+ var exifStructure = exif.Structure;
+ if (exifStructure is not null)
{
- var structure = tag.Structure;
- if (structure is not null
- && structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
+ if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) is RationalIFDEntry apertureEntry)
{
- var exifStructure = exif.Structure;
- if (exifStructure is not null)
- {
- var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
- if (entry is not null)
- {
- item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
- }
-
- entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
- if (entry is not null)
- {
- item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
- }
- }
+ item.Aperture = (double)apertureEntry.Value.Numerator / apertureEntry.Value.Denominator;
+ }
+
+ if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) is RationalIFDEntry shutterSpeedEntry)
+ {
+ item.ShutterSpeed = (double)shutterSpeedEntry.Value.Numerator / shutterSpeedEntry.Value.Denominator;
}
}
+ }
+ }
- if (file is TagLib.Image.File image)
- {
- item.CameraMake = image.ImageTag.Make;
- item.CameraModel = image.ImageTag.Model;
+ if (file is TagLib.Image.File image)
+ {
+ item.CameraMake = image.ImageTag.Make;
+ item.CameraModel = image.ImageTag.Model;
- item.Width = image.Properties.PhotoWidth;
- item.Height = image.Properties.PhotoHeight;
+ item.Width = image.Properties.PhotoWidth;
+ item.Height = image.Properties.PhotoHeight;
- var rating = image.ImageTag.Rating;
- item.CommunityRating = rating.HasValue ? rating : null;
+ item.CommunityRating = image.ImageTag.Rating;
- item.Overview = image.ImageTag.Comment;
+ item.Overview = image.ImageTag.Comment;
- if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
- && !item.LockedFields.Contains(MetadataField.Name))
- {
- item.Name = image.ImageTag.Title;
- }
+ if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
+ && !item.LockedFields.Contains(MetadataField.Name))
+ {
+ item.Name = image.ImageTag.Title;
+ }
- var dateTaken = image.ImageTag.DateTime;
- if (dateTaken.HasValue)
- {
- item.DateCreated = dateTaken.Value;
- item.PremiereDate = dateTaken.Value;
- item.ProductionYear = dateTaken.Value.Year;
- }
+ var dateTaken = image.ImageTag.DateTime;
+ if (dateTaken.HasValue)
+ {
+ item.DateCreated = dateTaken.Value;
+ item.PremiereDate = dateTaken.Value;
+ item.ProductionYear = dateTaken.Value.Year;
+ }
- item.Genres = image.ImageTag.Genres;
- item.Tags = image.ImageTag.Keywords;
- item.Software = image.ImageTag.Software;
+ item.Genres = image.ImageTag.Genres;
+ item.Tags = image.ImageTag.Keywords;
+ item.Software = image.ImageTag.Software;
- if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
- {
- item.Orientation = null;
- }
- else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
- {
- item.Orientation = orientation;
- }
+ if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
+ {
+ item.Orientation = null;
+ }
+ else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
+ {
+ item.Orientation = orientation;
+ }
- item.ExposureTime = image.ImageTag.ExposureTime;
- item.FocalLength = image.ImageTag.FocalLength;
+ item.ExposureTime = image.ImageTag.ExposureTime;
+ item.FocalLength = image.ImageTag.FocalLength;
- item.Latitude = image.ImageTag.Latitude;
- item.Longitude = image.ImageTag.Longitude;
- item.Altitude = image.ImageTag.Altitude;
+ item.Latitude = image.ImageTag.Latitude;
+ item.Longitude = image.ImageTag.Longitude;
+ item.Altitude = image.ImageTag.Altitude;
- if (image.ImageTag.ISOSpeedRatings.HasValue)
- {
- item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
- }
- else
- {
- item.IsoSpeedRating = null;
- }
- }
+ if (image.ImageTag.ISOSpeedRatings.HasValue)
+ {
+ item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
+ }
+ else
+ {
+ item.IsoSpeedRating = null;
}
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
}
}
-
- if (item.Width <= 0 || item.Height <= 0)
+ catch (Exception ex)
{
- var img = item.GetImageInfo(ImageType.Primary, 0);
+ _logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
+ }
+ }
- try
- {
- var size = _imageProcessor.GetImageDimensions(item, img);
+ if (item.Width <= 0 || item.Height <= 0)
+ {
+ var img = item.GetImageInfo(ImageType.Primary, 0);
- if (size.Width > 0 && size.Height > 0)
- {
- item.Width = size.Width;
- item.Height = size.Height;
- }
- }
- catch (ArgumentException)
+ try
+ {
+ var size = _imageProcessor.GetImageDimensions(item, img);
+
+ if (size.Width > 0 && size.Height > 0)
{
- // format not supported
+ item.Width = size.Width;
+ item.Height = size.Height;
}
}
-
- const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
- return Task.FromResult(Result);
+ catch (ArgumentException)
+ {
+ // format not supported
+ }
}
+
+ const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
+ return Task.FromResult(Result);
}
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 4c3ef2c7f..634fca2eb 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -157,7 +157,7 @@ public class UniversalAudioController : BaseJellyfinApiController
}
var isStatic = mediaSource.SupportsDirectStream;
- if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.Hls)
+ if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
{
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
// ffmpeg option -> file extension
@@ -268,7 +268,7 @@ public class UniversalAudioController : BaseJellyfinApiController
Context = EncodingContext.Streaming,
Container = transcodingContainer ?? "mp3",
AudioCodec = audioCodec ?? "mp3",
- Protocol = transcodingProtocol ?? MediaStreamProtocol.Http,
+ Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
}
diff --git a/Jellyfin.Data/Enums/MediaStreamProtocol.cs b/Jellyfin.Data/Enums/MediaStreamProtocol.cs
index 965edd6c1..844dc95c1 100644
--- a/Jellyfin.Data/Enums/MediaStreamProtocol.cs
+++ b/Jellyfin.Data/Enums/MediaStreamProtocol.cs
@@ -1,20 +1,22 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
using System.ComponentModel;
namespace Jellyfin.Data.Enums;
/// <summary>
/// Media streaming protocol.
+/// Lowercase for backwards compatibility.
/// </summary>
-[DefaultValue(Http)]
+[DefaultValue(http)]
public enum MediaStreamProtocol
{
/// <summary>
/// HTTP.
/// </summary>
- Http = 0,
+ http = 0,
/// <summary>
/// HTTP Live Streaming.
/// </summary>
- Hls = 1
+ hls = 1
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b6738e7cc..946f7266c 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -253,6 +253,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
}
+ private bool IsVideoToolboxFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("videotoolbox")
+ && _mediaEncoder.SupportsFilter("yadif_videotoolbox")
+ && _mediaEncoder.SupportsFilter("overlay_videotoolbox")
+ && _mediaEncoder.SupportsFilter("scale_vt");
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
@@ -272,7 +280,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
- return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
+ var isVideoToolBoxDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
return state.VideoStream.VideoRange == VideoRange.HDR
@@ -308,6 +317,21 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
}
+ private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream is null
+ || !options.EnableVideoToolboxTonemapping
+ || GetVideoColorBitDepth(state) != 10)
+ {
+ return false;
+ }
+
+ // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
+ // All other HDR formats working.
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus;
+ }
+
/// <summary>
/// Gets the name of the output video codec.
/// </summary>
@@ -4954,22 +4978,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null, null);
}
- var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
+ var isMacOS = OperatingSystem.IsMacOS();
+ var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
+ var isVtOclSupported = isVtFullSupported && IsOpenclFullSupported();
- if (!options.EnableHardwareEncoding)
+ // legacy videotoolbox pipeline (disable hw filters)
+ if (!isVtEncoder
+ || !isVtOclSupported
+ || !_mediaEncoder.SupportsFilter("alphasrc"))
{
- return swFilterChain;
+ return GetSwVidFilterChain(state, options, vidEncoder);
}
- if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0)
- {
- // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg
- return swFilterChain;
- }
+ // preferred videotoolbox + vt/ocl filters pipeline
+ return GetAppleVidFiltersPreferred(state, options, vidDecoder, vidEncoder);
+ }
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFiltersPreferred(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
var reqW = state.BaseRequest.Width;
@@ -4977,33 +5009,114 @@ namespace MediaBrowser.Controller.MediaEncoding
var reqMaxW = state.BaseRequest.MaxWidth;
var reqMaxH = state.BaseRequest.MaxHeight;
var threeDFormat = state.MediaSource.Video3DFormat;
- var newfilters = new List<string>();
- var noOverlay = swFilterChain.OverlayFilters.Count == 0;
- var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox");
- // fallback to software filters if we are using filters not supported by hardware yet.
- var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint);
- if (!useHardwareFilters)
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", 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 doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
+ var doOclTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
+
+ var scaleFormat = string.Empty;
+ if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase))
{
- return swFilterChain;
+ // Use P010 for OpenCL tone mapping, otherwise force an 8bit output.
+ scaleFormat = doOclTonemap ? "p010le" : "nv12";
}
- // ffmpeg cannot use videotoolbox to scale
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
- newfilters.Add(swScaleFilter);
+ var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent
- // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here
- // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion
- newfilters.Add("hwupload");
+ var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ 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));
+
+ if (!isVtEncoder)
+ {
+ // should not happen.
+ return (null, null, null);
+ }
+
+ /* Make main filters for video stream */
+ var mainFilters = new List<string>();
+ // Color override is only required for OpenCL where hardware surface is in use
+ if (doOclTonemap)
+ {
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doOclTonemap));
+ }
+
+ // INPUT videotoolbox/memory surface(vram/uma)
+ // this will pass-through automatically if in/out format matches.
+ mainFilters.Add("format=nv12|p010le|videotoolbox_vld");
+ mainFilters.Add("hwupload=derive_device=videotoolbox");
+
+ // hw deint
if (doDeintH2645)
{
var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
- newfilters.Add(deintFilter);
+ mainFilters.Add(deintFilter);
}
- return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
+ if (doVtTonemap)
+ {
+ const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709";
+
+ // scale_vt can handle scaling & tonemapping in one shot, just like vpp_qsv.
+ hwScaleFilter = string.IsNullOrEmpty(hwScaleFilter)
+ ? "scale_vt=" + VtTonemapArgs
+ : hwScaleFilter + ":" + VtTonemapArgs;
+ }
+
+ // hw scale & vt tonemap
+ mainFilters.Add(hwScaleFilter);
+
+ // ocl tonemap
+ if (doOclTonemap)
+ {
+ // map from videotoolbox to opencl via videotoolbox-opencl interop.
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
+
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ mainFilters.Add(tonemapFilter);
+
+ // OUTPUT videotoolbox(nv12) surface(vram/uma)
+ // reverse-mapping via videotoolbox-opencl interop.
+ mainFilters.Add("hwmap=derive_device=videotoolbox:mode=write:reverse=1");
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List<string>();
+ var overlayFilters = new List<string>();
+
+ if (hasSubs)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
+
+ subFilters.Add("hwupload=derive_device=videotoolbox");
+ overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
}
/// <summary>
@@ -5995,22 +6108,37 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ // Hardware surface only make sense when interop with OpenCL
+ // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
+ // For example: https://trac.ffmpeg.org/ticket/10884
+ var useOclToneMapping = !IsVideoToolboxTonemapAvailable(state, options)
+ && options.EnableTonemapping
+ && state.VideoStream is not null
+ && GetVideoColorBitDepth(state) == 10
+ && state.VideoStream.VideoRange == VideoRange.HDR
+ && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG
+ || (state.VideoStream.VideoRangeType == VideoRangeType.DOVI
+ && string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)));
+
+ var useHwSurface = useOclToneMapping && IsVideoToolboxFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
+
if (is8bitSwFormatsVt)
{
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "h264", bitDepth, false);
+ return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
}
if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "mpeg2video", bitDepth, false);
+ return GetHwaccelType(state, options, "mpeg2video", bitDepth, useHwSurface);
}
if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "mpeg4", bitDepth, false);
+ return GetHwaccelType(state, options, "mpeg4", bitDepth, useHwSurface);
}
}
@@ -6019,12 +6147,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, false);
+ return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, false);
+ return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index fdca28390..6549125d3 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -128,6 +128,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
"overlay_vulkan",
// videotoolbox
"yadif_videotoolbox",
+ "scale_vt",
+ "overlay_videotoolbox",
// rkrga
"scale_rkrga",
"vpp_rkrga",
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 84c735f9c..ab6f0d867 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -28,6 +28,7 @@ public class EncodingOptions
VaapiDevice = "/dev/dri/renderD128";
EnableTonemapping = false;
EnableVppTonemapping = false;
+ EnableVideoToolboxTonemapping = false;
TonemappingAlgorithm = "bt2390";
TonemappingMode = "auto";
TonemappingRange = "auto";
@@ -147,6 +148,11 @@ public class EncodingOptions
public bool EnableVppTonemapping { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether videotoolbox tonemapping is enabled.
+ /// </summary>
+ public bool EnableVideoToolboxTonemapping { get; set; }
+
+ /// <summary>
/// Gets or sets the tone-mapping algorithm.
/// </summary>
public string TonemappingAlgorithm { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 7d9449b74..011453e52 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -557,7 +557,7 @@ namespace MediaBrowser.Model.Dlna
private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
- var protocol = MediaStreamProtocol.Http;
+ var protocol = MediaStreamProtocol.http;
item.TranscodingContainer = container;
item.TranscodingSubProtocol = protocol;
@@ -648,7 +648,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == PlayMethod.DirectPlay)
{
- playlistItem.SubProtocol = MediaStreamProtocol.Http;
+ playlistItem.SubProtocol = MediaStreamProtocol.http;
var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
if (audioStreamIndex.HasValue)
@@ -803,7 +803,7 @@ namespace MediaBrowser.Model.Dlna
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
// Enforce HLS video codec restrictions
- if (playlistItem.SubProtocol == MediaStreamProtocol.Hls)
+ if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
}
@@ -840,7 +840,7 @@ namespace MediaBrowser.Model.Dlna
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
// Enforce HLS audio codec restrictions
- if (playlistItem.SubProtocol == MediaStreamProtocol.Hls)
+ if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
@@ -1360,7 +1360,7 @@ namespace MediaBrowser.Model.Dlna
string? outputContainer,
MediaStreamProtocol? transcodingSubProtocol)
{
- if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.Hls))
+ if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls))
{
// Look for supported embedded subs of the same format
foreach (var profile in subtitleProfiles)
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index cd6d34be2..75e5b6d18 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -670,7 +670,7 @@ namespace MediaBrowser.Model.Dlna
if (MediaType == DlnaProfileType.Audio)
{
- if (SubProtocol == MediaStreamProtocol.Hls)
+ if (SubProtocol == MediaStreamProtocol.hls)
{
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
@@ -678,7 +678,7 @@ namespace MediaBrowser.Model.Dlna
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
- if (SubProtocol == MediaStreamProtocol.Hls)
+ if (SubProtocol == MediaStreamProtocol.hls)
{
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
@@ -716,7 +716,7 @@ namespace MediaBrowser.Model.Dlna
long startPositionTicks = item.StartPositionTicks;
- if (item.SubProtocol == MediaStreamProtocol.Hls)
+ if (item.SubProtocol == MediaStreamProtocol.hls)
{
list.Add(new NameValuePair("StartTimeTicks", string.Empty));
}
@@ -778,7 +778,7 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
- if (item.SubProtocol == MediaStreamProtocol.Hls)
+ if (item.SubProtocol == MediaStreamProtocol.hls)
{
list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
@@ -829,7 +829,7 @@ namespace MediaBrowser.Model.Dlna
var list = new List<SubtitleStreamInfo>();
// HLS will preserve timestamps so we can just grab the full subtitle stream
- long startPositionTicks = SubProtocol == MediaStreamProtocol.Hls
+ long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
? 0
: (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 8f4f3e2f8..891448c66 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Dlna
public string AudioCodec { get; set; } = string.Empty;
[XmlAttribute("protocol")]
- public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.Http;
+ public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
[DefaultValue(false)]
[XmlAttribute("estimateContentLength")]
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
index 4fd9fd290..5d86d6bae 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
@@ -15,9 +15,9 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Direct(string input, MediaStreamProtocol output)
{
var value = JsonSerializer.Deserialize<MediaStreamProtocol>(input, _jsonOptions);
@@ -30,10 +30,10 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
- [InlineData(null, MediaStreamProtocol.Http)]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData(null, MediaStreamProtocol.http)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum(string? input, MediaStreamProtocol output)
{
input ??= "null";
@@ -51,9 +51,9 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, null)]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Nullable(string? input, MediaStreamProtocol? output)
{
input ??= "null";
@@ -69,8 +69,8 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
- [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)]
- [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)]
+ [InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
+ [InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
public void Enum_RoundTrip(MediaStreamProtocol input, MediaStreamProtocol output)
{
var inputObj = new TestClass { EnumValue = input };
@@ -87,8 +87,8 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
- [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)]
- [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)]
+ [InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
+ [InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
[InlineData(null, null)]
public void Enum_RoundTrip_Nullable(MediaStreamProtocol? input, MediaStreamProtocol? output)
{
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 183997fdb..6d88dbb8e 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -389,21 +389,21 @@ namespace Jellyfin.Model.Tests
// Assert.Equal("webm", val.Container);
Assert.Equal(streamInfo.Container, uri.Extension);
Assert.Equal("stream", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.http, streamInfo.SubProtocol);
}
else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
{
Assert.Equal("mp4", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.hls, streamInfo.SubProtocol);
}
else
{
Assert.Equal("ts", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.hls, streamInfo.SubProtocol);
}
// Full transcode
@@ -489,7 +489,7 @@ namespace Jellyfin.Model.Tests
}
else if (playMethod is null)
{
- Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.http, streamInfo.SubProtocol);
Assert.Equal("stream", uri.Filename);
Assert.False(streamInfo.EstimateContentLength);