diff options
| -rw-r--r-- | .ci/azure-pipelines-abi.yml | 93 | ||||
| -rw-r--r-- | .ci/azure-pipelines-main.yml | 71 | ||||
| -rw-r--r-- | .ci/azure-pipelines-package.yml | 274 | ||||
| -rw-r--r-- | .ci/azure-pipelines-test.yml | 98 | ||||
| -rw-r--r-- | .ci/azure-pipelines.yml | 64 | ||||
| -rw-r--r-- | Emby.Photos/PhotoProvider.cs | 241 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UniversalAudioController.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Data/Enums/MediaStreamProtocol.cs | 8 | ||||
| -rw-r--r-- | MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 194 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Model/Configuration/EncodingOptions.cs | 6 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamBuilder.cs | 10 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/StreamInfo.cs | 10 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dlna/TranscodingProfile.cs | 2 | ||||
| -rw-r--r-- | tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs | 28 | ||||
| -rw-r--r-- | tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs | 8 |
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); |
