Integrate branch 'master' into feature/language_filters
|
@ -259,7 +259,7 @@ jobs:
|
|||
publishFeedCredentials: 'NugetOrg'
|
||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||
|
||||
- task: NuGetAuthenticate@0
|
||||
- task: NuGetAuthenticate@1
|
||||
displayName: 'Authenticate to unstable Nuget feed'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.0",
|
||||
"version": "8.0.1",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "Development Jellyfin Server",
|
||||
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
|
||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
|
||||
// reads the extensions list and installs them
|
||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||
"version": "none",
|
||||
"dotnetRuntimeVersions": "8.0",
|
||||
"aspNetCoreRuntimeVersions": "8.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": ["libfontconfig1"]
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
|
||||
},
|
||||
"hostRequirements": {
|
||||
"memory": "8gb",
|
||||
"cpus": 4
|
||||
}
|
||||
}
|
|
@ -22,16 +22,16 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
||||
uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
||||
uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
|
||||
uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
|
|
|
@ -19,13 +19,13 @@ jobs:
|
|||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
|
@ -53,13 +53,13 @@ jobs:
|
|||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||
git checkout --progress --force $ANCESTOR_REF
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
|
@ -78,12 +78,12 @@ jobs:
|
|||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
|
|
@ -19,9 +19,9 @@ jobs:
|
|||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3
|
||||
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
@ -42,9 +42,3 @@ jobs:
|
|||
|
||||
# TODO - which action / tool to use to publish code coverage results?
|
||||
# - name: Publish code coverage results
|
||||
|
||||
- name: Publish OpenAPI Artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
with:
|
||||
name: "OpenAPI Spec"
|
||||
path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json"
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Remove from 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
@ -24,7 +24,7 @@ jobs:
|
|||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Release Next' project
|
||||
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
@ -47,7 +47,7 @@ jobs:
|
|||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||
|
||||
- name: Move issue to needs triage
|
||||
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
@ -56,7 +56,7 @@ jobs:
|
|||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add issue to triage project
|
||||
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp",
|
||||
"editorconfig.editorconfig"
|
||||
"editorconfig.editorconfig",
|
||||
"GitHub.vscode-github-actions",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"ms-dotnettools.csdevkit"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
- [Maxr1998](https://github.com/Maxr1998)
|
||||
- [mcarlton00](https://github.com/mcarlton00)
|
||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||
- [mohd-akram](https://github.com/mohd-akram)
|
||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||
- [n8225](https://github.com/n8225)
|
||||
- [Nalsai](https://github.com/Nalsai)
|
||||
|
@ -171,10 +172,11 @@
|
|||
- [tallbl0nde](https://github.com/tallbl0nde)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [scampower3](https://github.com/scampower3)
|
||||
- [Chris-Codes-It] (https://github.com/Chris-Codes-It)
|
||||
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
- [Gauvino](https://github.com/Gauvino)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -4,87 +4,87 @@
|
|||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.18" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.27" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.1.2" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.1.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.7" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.7" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.10" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.0.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.1.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.6.1" />
|
||||
<PackageVersion Include="xunit" Version="2.6.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
68
Dockerfile
|
@ -8,72 +8,68 @@ FROM node:20-alpine as web-builder
|
|||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM debian:stable-slim as app
|
||||
FROM debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
ARG GMMLIB_VERSION=22.0.2
|
||||
ARG IGC_VERSION=1.0.10395
|
||||
ARG NEO_VERSION=22.08.22549
|
||||
ARG LEVEL_ZERO_VERSION=1.3.22549
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
ARG GMMLIB_VERSION=22.3.11.ci17757293
|
||||
ARG IGC_VERSION=1.0.15136.22
|
||||
ARG NEO_VERSION=23.39.27427.23
|
||||
ARG LEVEL_ZERO_VERSION=1.3.27427.23
|
||||
|
||||
# Install dependencies:
|
||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
||||
# curl: healthcheck
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget curl \
|
||||
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
mesa-va-drivers \
|
||||
jellyfin-ffmpeg5 \
|
||||
openssl \
|
||||
locales \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y mesa-va-drivers jellyfin-ffmpeg6 openssl locales \
|
||||
# Intel VAAPI Tone mapping dependencies:
|
||||
# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
|
||||
# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
|
||||
&& mkdir intel-compute-runtime \
|
||||
&& cd intel-compute-runtime \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
||||
&& curl -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
|
||||
-LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/libigdgmm12_${GMMLIB_VERSION}_amd64.deb \
|
||||
&& dpkg -i *.deb \
|
||||
&& cd .. \
|
||||
&& rm -rf intel-compute-runtime \
|
||||
&& apt-get remove gnupg wget -y \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /cache /config /media \
|
||||
&& chmod 777 /cache /config /media \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# because of changes in docker and systemd we need to not build in parallel at the moment
|
||||
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
|
@ -83,11 +79,9 @@ COPY --from=builder /jellyfin /jellyfin
|
|||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "./jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
|
|
|
@ -4,64 +4,58 @@
|
|||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM arm32v7/debian:stable-slim as app
|
||||
FROM arm32v7/debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
|
||||
# curl: setup & healthcheck
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
|
||||
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
|
||||
curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
|
||||
echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
|
||||
echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
jellyfin-ffmpeg \
|
||||
libssl-dev \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
vainfo \
|
||||
libva2 \
|
||||
locales \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
jellyfin-ffmpeg6 libssl-dev libfontconfig1 \
|
||||
libfreetype6 vainfo libva2 locales \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /cache /config /media \
|
||||
&& chmod 777 /cache /config /media \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
@ -72,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
|
|||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "/jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
|
|
|
@ -4,58 +4,58 @@
|
|||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=8.0
|
||||
|
||||
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& apk del curl \
|
||||
&& cd jellyfin-web-* \
|
||||
&& npm ci --no-audit --unsafe-perm \
|
||||
&& npm run build:production \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM arm64v8/debian:stable-slim as app
|
||||
FROM arm64v8/debian:bookworm-slim as app
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES="all"
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config
|
||||
ENV JELLYFIN_CACHE_DIR=/cache
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
|
||||
# curl: healcheck
|
||||
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
ffmpeg \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
libomxil-bellagio0 \
|
||||
libomxil-bellagio-bin \
|
||||
locales \
|
||||
curl \
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
|
||||
&& curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
|
||||
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
jellyfin-ffmpeg6 locales libssl-dev libfontconfig1 \
|
||||
libfreetype6 libomxil-bellagio0 libomxil-bellagio-bin \
|
||||
&& apt-get remove gnupg -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /cache /config /media \
|
||||
&& chmod 777 /cache /config /media \
|
||||
&& mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
|
||||
|
||||
# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
@ -66,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
|
|||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/bin/ffmpeg"]
|
||||
VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
|
||||
ENTRYPOINT [ "/jellyfin/jellyfin", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// DLNA Query parameter type, used when querying DLNA devices via SOAP.
|
||||
/// </summary>
|
||||
public class Argument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets name of the DLNA argument.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direction of the parameter.
|
||||
/// </summary>
|
||||
public string Direction { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the related DLNA state variable for this argument.
|
||||
/// </summary>
|
||||
public string RelatedStateVariable { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceIcon" />.
|
||||
/// </summary>
|
||||
public class DeviceIcon
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Url.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MimeType.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Width.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Height.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Depth.
|
||||
/// </summary>
|
||||
public string Depth { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceService" />.
|
||||
/// </summary>
|
||||
public class DeviceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Type.
|
||||
/// </summary>
|
||||
public string ServiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Id.
|
||||
/// </summary>
|
||||
public string ServiceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Scpd Url.
|
||||
/// </summary>
|
||||
public string ScpdUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Control Url.
|
||||
/// </summary>
|
||||
public string ControlUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EventSubUrl.
|
||||
/// </summary>
|
||||
public string EventSubUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => ServiceId;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceAction" />.
|
||||
/// </summary>
|
||||
public class ServiceAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
|
||||
/// </summary>
|
||||
public ServiceAction()
|
||||
{
|
||||
ArgumentList = new List<Argument>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the action.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ArgumentList.
|
||||
/// </summary>
|
||||
public List<Argument> ArgumentList { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="StateVariable" />.
|
||||
/// </summary>
|
||||
public class StateVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the state variable.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data type of the state variable.
|
||||
/// </summary>
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether it sends events.
|
||||
/// </summary>
|
||||
public bool SendsEvents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed values range.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
|
||||
/// </summary>
|
||||
public class DlnaOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
|
||||
/// </summary>
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = false;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
AliveMessageIntervalSeconds = 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
|
||||
/// </summary>
|
||||
public bool EnablePlayTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
|
||||
/// </summary>
|
||||
public bool EnableServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
|
||||
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public bool EnableDebugLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
|
||||
/// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public bool EnablePlayToTracing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ssdp client discovery interval time (in seconds).
|
||||
/// This is the time after which the server will send a ssdp search request.
|
||||
/// </summary>
|
||||
public int ClientDiscoveryIntervalSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
|
||||
/// </summary>
|
||||
public int AliveMessageIntervalSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
|
||||
/// </summary>
|
||||
public int BlastAliveMessageIntervalSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return AliveMessageIntervalSeconds;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
AliveMessageIntervalSeconds = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default user account that the dlna server uses.
|
||||
/// </summary>
|
||||
public string? DefaultUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||
/// </summary>
|
||||
public bool AutoCreatePlayToProfiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to blast alive messages.
|
||||
/// </summary>
|
||||
public bool BlastAliveMessages { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// gets or sets a value indicating whether to send only matched host.
|
||||
/// </summary>
|
||||
public bool SendOnlyMatchedHost { get; set; } = true;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public static class ConfigurationExtension
|
||||
{
|
||||
public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager)
|
||||
{
|
||||
return manager.GetConfiguration<DlnaOptions>("dlna");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ConnectionManagerService" />.
|
||||
/// </summary>
|
||||
public class ConnectionManagerService : BaseService, IConnectionManager
|
||||
{
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
||||
public ConnectionManagerService(
|
||||
IDlnaManager dlna,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<ConnectionManagerService> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return ConnectionManagerXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ConnectionManagerXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ConnectionManager:1 service template.
|
||||
/// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
|
||||
/// </summary>
|
||||
/// <returns>An XML description of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SourceProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SinkProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "CurrentConnectionIDs",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
"InsufficientBandwidth",
|
||||
"UnreliableChannel",
|
||||
"Unknown"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionManager",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Direction",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"Output",
|
||||
"Input"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_AVTransportID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RcsID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
private readonly DeviceProfile _profile;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
|
||||
: base(config, logger)
|
||||
{
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleGetProtocolInfo(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the response to the GetProtocolInfo request.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
|
||||
{
|
||||
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
|
||||
xmlWriter.WriteElementString("Sink", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
GetCurrentConnectionInfo(),
|
||||
GetProtocolInfo(),
|
||||
GetCurrentConnectionIDs(),
|
||||
ConnectionComplete(),
|
||||
PrepareForConnection()
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "PrepareForConnection".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction PrepareForConnection()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "PrepareForConnection"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RemoteProtocolInfo",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionManager",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Direction",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Direction"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AVTransportID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RcsID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RcsID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetCurrentConnectionInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetCurrentConnectionInfo"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RcsID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RcsID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AVTransportID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ProtocolInfo",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionManager",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Direction",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Direction"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Status",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionStatus"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetProtocolInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetProtocolInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetProtocolInfo"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Source",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SourceProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Sink",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SinkProtocolInfo"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionIDs".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetCurrentConnectionIDs()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetCurrentConnectionIDs"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionIDs",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "CurrentConnectionIDs"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "ConnectionComplete".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction ConnectionComplete()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "ConnectionComplete"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryService" />.
|
||||
/// </summary>
|
||||
public class ContentDirectoryService : BaseService, IContentDirectory
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IUserViewManager _userViewManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
public ContentDirectoryService(
|
||||
IDlnaManager dlna,
|
||||
IUserDataManager userDataManager,
|
||||
IImageProcessor imageProcessor,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
ILogger<ContentDirectoryService> logger,
|
||||
IHttpClientFactory httpClient,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IUserViewManager userViewManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ITVSeriesManager tvSeriesManager)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_userDataManager = userDataManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_userManager = userManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_userViewManager = userViewManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_tvSeriesManager = tvSeriesManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system id. (A unique id which changes on when our definition changes.)
|
||||
/// </summary>
|
||||
private static int SystemUpdateId
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return now.Year + now.DayOfYear + now.Hour;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return ContentDirectoryXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
|
||||
|
||||
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var user = GetUser(profile);
|
||||
|
||||
return new ControlHandler(
|
||||
Logger,
|
||||
_libraryManager,
|
||||
profile,
|
||||
serverAddress,
|
||||
null,
|
||||
_imageProcessor,
|
||||
_userDataManager,
|
||||
user,
|
||||
SystemUpdateId,
|
||||
_config,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_userViewManager,
|
||||
_mediaEncoder,
|
||||
_tvSeriesManager)
|
||||
.ProcessControlRequestAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user stored in the device profile.
|
||||
/// </summary>
|
||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||
/// <returns>The <see cref="User"/>.</returns>
|
||||
private User? GetUser(DeviceProfile profile)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profile.UserId))
|
||||
{
|
||||
var user = _userManager.GetUserById(Guid.Parse(profile.UserId));
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
var userId = _config.GetDlnaConfiguration().DefaultUserId;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = _userManager.GetUserById(Guid.Parse(userId));
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in _userManager.Users)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return _userManager.Users.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ContentDirectoryXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ContentDirectory:1 service template.
|
||||
/// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
|
||||
/// </summary>
|
||||
/// <returns>An XML description of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Filter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SortCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Index",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Count",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_UpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SearchCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SortCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SystemUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SearchCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ObjectID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseFlag",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"BrowseMetadata",
|
||||
"BrowseDirectChildren"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseLetter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_CategoryType",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_PosSec",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Featurelist",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServerItem" />.
|
||||
/// </summary>
|
||||
internal class ServerItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
/// <param name="stubType">The stub type.</param>
|
||||
public ServerItem(BaseItem item, StubType? stubType)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
StubType = stubType;
|
||||
}
|
||||
else if (item is IItemByName and not Folder)
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying base item.
|
||||
/// </summary>
|
||||
public BaseItem Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DLNA item type.
|
||||
/// </summary>
|
||||
public StubType? StubType { get; }
|
||||
}
|
||||
}
|
|
@ -1,415 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
GetSearchCapabilitiesAction(),
|
||||
GetSortCapabilitiesAction(),
|
||||
GetGetSystemUpdateIDAction(),
|
||||
GetBrowseAction(),
|
||||
GetSearchAction(),
|
||||
GetX_GetFeatureListAction(),
|
||||
GetXSetBookmarkAction(),
|
||||
GetBrowseByLetterAction()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSystemUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetSystemUpdateIDAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSystemUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Id",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SystemUpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSearchCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSearchCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSearchCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SearchCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SearchCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSortCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSortCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSortCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SortCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_GetFeatureList".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetX_GetFeatureListAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_GetFeatureList"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "FeatureList",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Featurelist"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Search".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSearchAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "Search"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ContainerID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SearchCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SearchCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Browse".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetBrowseAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "Browse"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "BrowseFlag",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_BrowseByLetter".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetBrowseByLetterAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_BrowseByLetter"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "BrowseFlag",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingLetter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseLetter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_SetBookmark".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetXSetBookmarkAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_SetBookmark"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "CategoryType",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_CategoryType"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PosSecond",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_PosSec"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the DLNA item types.
|
||||
/// </summary>
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
Latest = 2,
|
||||
Playlists = 3,
|
||||
Albums = 4,
|
||||
AlbumArtists = 5,
|
||||
Artists = 6,
|
||||
Songs = 7,
|
||||
Genres = 8,
|
||||
FavoriteSongs = 9,
|
||||
FavoriteArtists = 10,
|
||||
FavoriteAlbums = 11,
|
||||
ContinueWatching = 12,
|
||||
Movies = 13,
|
||||
Collections = 14,
|
||||
Favorites = 15,
|
||||
NextUp = 16,
|
||||
Series = 17,
|
||||
FavoriteSeries = 18,
|
||||
FavoriteEpisodes = 19
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class ControlRequest
|
||||
{
|
||||
public ControlRequest(IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; }
|
||||
|
||||
public Stream InputXml { get; set; }
|
||||
|
||||
public string TargetServerUuId { get; set; }
|
||||
|
||||
public string RequestedUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class ControlResponse
|
||||
{
|
||||
public ControlResponse(string xml, bool isSuccessful)
|
||||
{
|
||||
Headers = new Dictionary<string, string>();
|
||||
Xml = xml;
|
||||
IsSuccessful = isSuccessful;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers { get; }
|
||||
|
||||
public string Xml { get; set; }
|
||||
|
||||
public bool IsSuccessful { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Xml;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class Filter
|
||||
{
|
||||
private readonly string[] _fields;
|
||||
private readonly bool _all;
|
||||
|
||||
public Filter()
|
||||
: this("*")
|
||||
{
|
||||
}
|
||||
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
{
|
||||
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CA1305
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class StringWriterWithEncoding : StringWriter
|
||||
{
|
||||
private readonly Encoding? _encoding;
|
||||
|
||||
public StringWriterWithEncoding()
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(IFormatProvider formatProvider)
|
||||
: base(formatProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb)
|
||||
: base(sb)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider)
|
||||
: base(sb, formatProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(IFormatProvider formatProvider, Encoding encoding)
|
||||
: base(formatProvider)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, Encoding encoding)
|
||||
: base(sb)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider, Encoding encoding)
|
||||
: base(sb, formatProvider)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => _encoding ?? base.Encoding;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof(DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,491 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Profiles;
|
||||
using Emby.Dlna.Server;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaManager : IDlnaManager
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<DlnaManager> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
public DlnaManager(
|
||||
IXmlSerializer xmlSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
|
||||
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
|
||||
public async Task InitProfilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExtractSystemProfilesAsync().ConfigureAwait(false);
|
||||
Directory.CreateDirectory(UserProfilesPath);
|
||||
LoadProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error extracting DLNA profiles.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadProfiles()
|
||||
{
|
||||
var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
|
||||
.OrderBy(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
|
||||
.OrderBy(i => i.Name));
|
||||
}
|
||||
|
||||
public IEnumerable<DeviceProfile> GetProfiles()
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile GetDefaultProfile()
|
||||
{
|
||||
return new DefaultProfile();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(deviceInfo);
|
||||
|
||||
var profile = GetProfiles()
|
||||
.FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
/// - If the profile field has no value, the field matches regardless of its contents.
|
||||
/// - the profile field can be an exact match, or a reg exp.
|
||||
/// </summary>
|
||||
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||
/// <returns><b>True</b> if they match.</returns>
|
||||
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
{
|
||||
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||
}
|
||||
|
||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
// In profile identification: An empty pattern matches anything.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
// The profile contains a value, and the device doesn't.
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(headers);
|
||||
|
||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
|
||||
if (profile is null)
|
||||
{
|
||||
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
|
||||
{
|
||||
return profileInfo.Headers.Any(i => IsMatch(headers, i));
|
||||
}
|
||||
|
||||
private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
|
||||
{
|
||||
// Handle invalid user setup
|
||||
if (string.IsNullOrEmpty(header.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (headers.TryGetValue(header.Name, out StringValues value))
|
||||
{
|
||||
if (StringValues.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (header.Match)
|
||||
{
|
||||
case HeaderMatchType.Equals:
|
||||
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
||||
case HeaderMatchType.Substring:
|
||||
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
||||
return isMatch;
|
||||
case HeaderMatchType.Regex:
|
||||
// Can't be null, we checked above the switch statement
|
||||
return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase);
|
||||
default:
|
||||
throw new ArgumentException("Unrecognized HeaderMatchType");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i is not null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Array.Empty<DeviceProfile>();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
|
||||
{
|
||||
return profileTuple.Item2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error parsing profile file: {Path}", path);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(string id)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||
|
||||
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseProfileFile(info.Path, info.Info.Type);
|
||||
}
|
||||
|
||||
private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||
{
|
||||
return GetProfileInfosInternal().Select(i => i.Info);
|
||||
}
|
||||
|
||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||
{
|
||||
return new InternalProfileInfo(
|
||||
new DeviceProfileInfo
|
||||
{
|
||||
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||
Type = type
|
||||
},
|
||||
file.FullName);
|
||||
}
|
||||
|
||||
private async Task ExtractSystemProfilesAsync()
|
||||
{
|
||||
var namespaceName = GetType().Namespace + ".Profiles.Xml.";
|
||||
|
||||
var systemProfilesPath = SystemProfilesPath;
|
||||
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = Path.Join(
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The stream should exist as we just got its name from GetManifestResourceNames
|
||||
using (var stream = _assembly.GetManifestResourceStream(name)!)
|
||||
{
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.CreateNew;
|
||||
fileOptions.PreallocationSize = stream.Length;
|
||||
var fileStream = new FileStream(path, fileOptions);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteProfile(string id)
|
||||
{
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info.Info.Type == DeviceProfileType.System)
|
||||
{
|
||||
throw new ArgumentException("System profiles cannot be deleted.");
|
||||
}
|
||||
|
||||
_fileSystem.DeleteFile(info.Path);
|
||||
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles.Remove(info.Path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Id);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
if (current.Info.Type == DeviceProfileType.System)
|
||||
{
|
||||
throw new ArgumentException("System profiles can't be edited");
|
||||
}
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Join(UserProfilesPath, newFilename);
|
||||
|
||||
if (!string.Equals(path, current.Path, StringComparison.Ordinal))
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles.Remove(current.Path);
|
||||
}
|
||||
}
|
||||
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
}
|
||||
|
||||
SerializeToXml(profile, path);
|
||||
}
|
||||
|
||||
internal void SerializeToXml(DeviceProfile profile, string path)
|
||||
{
|
||||
_xmlSerializer.SerializeToFile(profile, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the object using serialization, to ensure it's not a subclass.
|
||||
/// If it's a subclass it may not serialize properly to xml (different root element tag name).
|
||||
/// </summary>
|
||||
/// <param name="profile">The device profile.</param>
|
||||
/// <returns>The re-serialized device profile.</returns>
|
||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||
{
|
||||
if (profile.GetType() == typeof(DeviceProfile))
|
||||
{
|
||||
return profile;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||
|
||||
// Output can't be null if the input isn't null
|
||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ?? GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageStream? GetIcon(string filename)
|
||||
{
|
||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||
? ImageFormat.Png
|
||||
: ImageFormat.Jpg;
|
||||
|
||||
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
if (stream is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImageStream(stream)
|
||||
{
|
||||
Format = format
|
||||
};
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
||||
{
|
||||
Info = info;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
internal DeviceProfileInfo Info { get; }
|
||||
|
||||
internal string Path { get; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\RSSDP\RSSDP.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="IDisposableAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||
<EmbeddedResource Include="Images\logo120.png" />
|
||||
<EmbeddedResource Include="Images\logo240.jpg" />
|
||||
<EmbeddedResource Include="Images\logo240.png" />
|
||||
<EmbeddedResource Include="Images\logo48.jpg" />
|
||||
<EmbeddedResource Include="Images\logo48.png" />
|
||||
<EmbeddedResource Include="Images\people48.jpg" />
|
||||
<EmbeddedResource Include="Images\people48.png" />
|
||||
<EmbeddedResource Include="Images\people480.jpg" />
|
||||
<EmbeddedResource Include="Images\people480.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Profiles\Xml\Default.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Denon AVR.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\foobar2000.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\LG Smart TV.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Linksys DMA2100.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Marantz.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Panasonic Viera.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Samsung Smart TV.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2013.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282010%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282011%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282012%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,22 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class EventSubscriptionResponse
|
||||
{
|
||||
public EventSubscriptionResponse(string content, string contentType)
|
||||
{
|
||||
Content = content;
|
||||
ContentType = contentType;
|
||||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public string Content { get; set; }
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Eventing
|
||||
{
|
||||
public class DlnaEventManager : IDlnaEventManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
||||
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
var subscription = GetSubscription(subscriptionId, false);
|
||||
if (subscription is not null)
|
||||
{
|
||||
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
int timeoutSeconds = subscription.TimeoutSeconds;
|
||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Renewing event subscription for {0} with timeout of {1} to {2}",
|
||||
subscription.NotificationType,
|
||||
timeoutSeconds,
|
||||
subscription.CallbackUrl);
|
||||
|
||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||
}
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
notificationType,
|
||||
timeout,
|
||||
callbackUrl);
|
||||
|
||||
_subscriptions.TryAdd(id, new EventSubscription
|
||||
{
|
||||
Id = id,
|
||||
CallbackUrl = callbackUrl,
|
||||
SubscriptionTime = DateTime.UtcNow,
|
||||
TimeoutSeconds = timeout,
|
||||
NotificationType = notificationType
|
||||
});
|
||||
|
||||
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
|
||||
}
|
||||
|
||||
private int? ParseTimeout(string header)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
// Starts with SECOND-
|
||||
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
||||
{
|
||||
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
||||
|
||||
_subscriptions.TryRemove(subscriptionId, out _);
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
}
|
||||
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
|
||||
response.Headers["SID"] = subscriptionId;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public EventSubscription GetSubscription(string id)
|
||||
{
|
||||
return GetSubscription(id, false);
|
||||
}
|
||||
|
||||
private EventSubscription GetSubscription(string id, bool throwOnMissing)
|
||||
{
|
||||
if (!_subscriptions.TryGetValue(id, out EventSubscription e) && throwOnMissing)
|
||||
{
|
||||
throw new ResourceNotFoundException("Event with Id " + id + " not found.");
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables)
|
||||
{
|
||||
var subs = _subscriptions.Values
|
||||
.Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task TriggerEvent(EventSubscription subscription, IDictionary<string, string> stateVariables)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\"?>");
|
||||
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
|
||||
foreach (var key in stateVariables.Keys)
|
||||
{
|
||||
builder.Append("<e:property>")
|
||||
.Append('<')
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append(stateVariables[key])
|
||||
.Append("</")
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append("</e:property>");
|
||||
}
|
||||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
||||
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
|
||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
}
|
||||
finally
|
||||
{
|
||||
subscription.IncrementTriggerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.Eventing
|
||||
{
|
||||
public class EventSubscription
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string CallbackUrl { get; set; }
|
||||
|
||||
public string NotificationType { get; set; }
|
||||
|
||||
public DateTime SubscriptionTime { get; set; }
|
||||
|
||||
public int TimeoutSeconds { get; set; }
|
||||
|
||||
public long TriggerCount { get; set; }
|
||||
|
||||
public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
|
||||
|
||||
public void IncrementTriggerCount()
|
||||
{
|
||||
if (TriggerCount == long.MaxValue)
|
||||
{
|
||||
TriggerCount = 0;
|
||||
}
|
||||
|
||||
TriggerCount++;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Emby.Dlna.ConnectionManager;
|
||||
using Emby.Dlna.ContentDirectory;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.MediaReceiverRegistrar;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace Emby.Dlna.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding DLNA services.
|
||||
/// </summary>
|
||||
public static class DlnaServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
|
||||
/// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
|
||||
public static void AddDlnaServices(
|
||||
this IServiceCollection services,
|
||||
IServerApplicationHost applicationHost)
|
||||
{
|
||||
services.AddHttpClient(NamedClient.Dlna, c =>
|
||||
{
|
||||
c.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}/{1} UPnP/1.0 {2}/{3}",
|
||||
Environment.OSVersion.Platform,
|
||||
Environment.OSVersion,
|
||||
applicationHost.Name,
|
||||
applicationHost.ApplicationVersionString));
|
||||
|
||||
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
|
||||
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
|
||||
});
|
||||
|
||||
services.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
services.AddSingleton<IContentDirectory, ContentDirectoryService>();
|
||||
services.AddSingleton<IConnectionManager, ConnectionManagerService>();
|
||||
services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
|
||||
|
||||
services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
|
||||
provider.GetRequiredService<ISocketFactory>(),
|
||||
provider.GetRequiredService<INetworkManager>(),
|
||||
provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
|
||||
{
|
||||
IsShared = true
|
||||
});
|
||||
|
||||
services.AddHostedService<DlnaHost>();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IConnectionManager : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IContentDirectory : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IDlnaEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Cancels the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Renews the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a string.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a string.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IUpnpService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the content directory XML.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetServiceXml();
|
||||
|
||||
/// <summary>
|
||||
/// Processes the control request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>ControlResponse.</returns>
|
||||
Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 740 B |
Before Width: | Height: | Size: 278 B |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1,387 +0,0 @@
|
|||
#pragma warning disable CA1031 // Do not catch general exception types.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace Emby.Dlna.Main;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IHostedService"/> that manages a DLNA server.
|
||||
/// </summary>
|
||||
public sealed class DlnaHost : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger<DlnaHost> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly ISsdpCommunicationsServer _communicationsServer;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new();
|
||||
|
||||
private SsdpDevicePublisher? _publisher;
|
||||
private PlayToManager? _manager;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
/// <param name="dlnaManager">The <see cref="IDlnaManager"/>.</param>
|
||||
/// <param name="imageProcessor">The <see cref="IImageProcessor"/>.</param>
|
||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
|
||||
/// <param name="localizationManager">The <see cref="ILocalizationManager"/>.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
|
||||
/// <param name="deviceDiscovery">The <see cref="IDeviceDiscovery"/>.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/>.</param>
|
||||
/// <param name="networkManager">The <see cref="INetworkManager"/>.</param>
|
||||
public DlnaHost(
|
||||
IServerConfigurationManager config,
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerApplicationHost appHost,
|
||||
ISessionManager sessionManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IDlnaManager dlnaManager,
|
||||
IImageProcessor imageProcessor,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISsdpCommunicationsServer communicationsServer,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_sessionManager = sessionManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localizationManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_communicationsServer = communicationsServer;
|
||||
_networkManager = networkManager;
|
||||
_logger = loggerFactory.CreateLogger<DlnaHost>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var netConfig = _config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
if (_appHost.ListenWithHttps && netConfig.RequireHttps)
|
||||
{
|
||||
if (_config.GetDlnaConfiguration().EnableServer)
|
||||
{
|
||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||
}
|
||||
|
||||
// No use starting as dlna won't work, as we're running purely on HTTPS.
|
||||
return;
|
||||
}
|
||||
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
ReloadComponents();
|
||||
|
||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Stop();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
Stop();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e)
|
||||
{
|
||||
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReloadComponents();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
StartDeviceDiscovery();
|
||||
|
||||
if (options.EnableServer)
|
||||
{
|
||||
StartDevicePublisher(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisposeDevicePublisher();
|
||||
}
|
||||
|
||||
if (options.EnablePlayTo)
|
||||
{
|
||||
StartPlayToManager();
|
||||
}
|
||||
else
|
||||
{
|
||||
DisposePlayToManager();
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateUuid(string text)
|
||||
{
|
||||
if (!Guid.TryParse(text, out var guid))
|
||||
{
|
||||
guid = text.GetMD5();
|
||||
}
|
||||
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static void SetProperties(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var serviceParts = fullDeviceType
|
||||
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Split(':');
|
||||
|
||||
device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting device discovery");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartDevicePublisher(Configuration.DlnaOptions options)
|
||||
{
|
||||
if (_publisher is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString(),
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = msg => _logger.LogDebug("{Msg}", msg),
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
RegisterServerEndpoints();
|
||||
|
||||
if (options.BlastAliveMessages)
|
||||
{
|
||||
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error registering endpoint");
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterServerEndpoints()
|
||||
{
|
||||
var udn = CreateUuid(_appHost.SystemId);
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
// Only get bind addresses in LAN
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
||||
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
.ToList();
|
||||
|
||||
if (validInterfaces.Count == 0)
|
||||
{
|
||||
// No interfaces returned, fall back to loopback
|
||||
validInterfaces = _networkManager.GetLoopbacks().ToList();
|
||||
}
|
||||
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
||||
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
Address = intf.Address,
|
||||
PrefixLength = NetworkUtils.MaskToCidr(intf.Subnet.Prefix),
|
||||
FriendlyName = "Jellyfin",
|
||||
Manufacturer = "Jellyfin",
|
||||
ModelName = "Jellyfin Server",
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperties(device, fullService);
|
||||
_publisher!.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new[]
|
||||
{
|
||||
"urn:schemas-upnp-org:service:ContentDirectory:1",
|
||||
"urn:schemas-upnp-org:service:ConnectionManager:1",
|
||||
// "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
|
||||
};
|
||||
|
||||
foreach (var subDevice in embeddedDevices)
|
||||
{
|
||||
var embeddedDevice = new SsdpEmbeddedDevice
|
||||
{
|
||||
FriendlyName = device.FriendlyName,
|
||||
Manufacturer = device.Manufacturer,
|
||||
ModelName = device.ModelName,
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperties(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_manager is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_manager = new PlayToManager(
|
||||
_logger,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_userManager,
|
||||
_dlnaManager,
|
||||
_appHost,
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
|
||||
_manager.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting PlayTo manager");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposePlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_manager is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Disposing PlayToManager");
|
||||
_manager.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error disposing PlayTo manager");
|
||||
}
|
||||
|
||||
_manager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDevicePublisher()
|
||||
{
|
||||
if (_publisher is not null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
DisposeDevicePublisher();
|
||||
DisposePlayToManager();
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
public ControlHandler(IServerConfigurationManager config, ILogger logger)
|
||||
: base(config, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleIsAuthorized(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleIsValidated(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that the handle is authorized in the xml stream.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private static void HandleIsAuthorized(XmlWriter xmlWriter)
|
||||
=> xmlWriter.WriteElementString("Result", "1");
|
||||
|
||||
/// <summary>
|
||||
/// Records that the handle is validated in the xml stream.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private static void HandleIsValidated(XmlWriter xmlWriter)
|
||||
=> xmlWriter.WriteElementString("Result", "1");
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="MediaReceiverRegistrarService" />.
|
||||
/// </summary>
|
||||
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
public MediaReceiverRegistrarService(
|
||||
ILogger<MediaReceiverRegistrarService> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerConfigurationManager config)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return MediaReceiverRegistrarXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
return new ControlHandler(
|
||||
_config,
|
||||
Logger)
|
||||
.ProcessControlRequestAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
|
||||
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
|
||||
/// </summary>
|
||||
public static class MediaReceiverRegistrarXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
|
||||
/// </summary>
|
||||
/// <returns>An XML representation of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The a list of all the state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
GetIsValidated(),
|
||||
GetIsAuthorized(),
|
||||
GetRegisterDevice(),
|
||||
GetGetAuthorizationDeniedUpdateID(),
|
||||
GetGetAuthorizationGrantedUpdateID(),
|
||||
GetGetValidationRevokedUpdateID(),
|
||||
GetGetValidationSucceededUpdateID()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsValidated".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetIsValidated()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "IsValidated"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "DeviceID",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsAuthorized".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetIsAuthorized()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "IsAuthorized"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "DeviceID",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "RegisterDevice".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetRegisterDevice()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "RegisterDevice"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RegistrationReqMsg",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RegistrationRespMsg",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationSucceededUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationSucceededUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationSucceededUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationDeniedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationRevokedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationRevokedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationRevokedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationGrantedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class DeviceInfo
|
||||
{
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
public DeviceInfo()
|
||||
{
|
||||
Name = "Generic Device";
|
||||
}
|
||||
|
||||
public string UUID { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string ModelDescription { get; set; }
|
||||
|
||||
public string ModelUrl { get; set; }
|
||||
|
||||
public string Manufacturer { get; set; }
|
||||
|
||||
public string SerialNumber { get; set; }
|
||||
|
||||
public string ManufacturerUrl { get; set; }
|
||||
|
||||
public string PresentationUrl { get; set; }
|
||||
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
set => _baseUrl = value;
|
||||
}
|
||||
|
||||
public DeviceIcon Icon { get; set; }
|
||||
|
||||
public List<DeviceService> Services => _services;
|
||||
|
||||
public DeviceIdentification ToDeviceIdentification()
|
||||
{
|
||||
return new DeviceIdentification
|
||||
{
|
||||
Manufacturer = Manufacturer,
|
||||
ModelName = ModelName,
|
||||
ModelNumber = ModelNumber,
|
||||
FriendlyName = Name,
|
||||
ManufacturerUrl = ManufacturerUrl,
|
||||
ModelUrl = ModelUrl,
|
||||
ModelDescription = ModelDescription,
|
||||
SerialNumber = SerialNumber
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Http client for Dlna PlayTo function.
|
||||
/// </summary>
|
||||
public partial class DlnaHttpClient
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(&(?![a-z]*;))")]
|
||||
private static partial Regex EscapeAmpersandRegex();
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
{
|
||||
// If it's already a complete url, don't stick anything onto the front of it
|
||||
if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return serviceUrl;
|
||||
}
|
||||
|
||||
if (!serviceUrl.StartsWith('/'))
|
||||
{
|
||||
serviceUrl = "/" + serviceUrl;
|
||||
}
|
||||
|
||||
return baseUrl + serviceUrl;
|
||||
}
|
||||
|
||||
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// try correcting the Xml response with common errors
|
||||
stream.Position = 0;
|
||||
using StreamReader sr = new StreamReader(stream);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<XDocument?> SendCommandAsync(
|
||||
string baseUrl,
|
||||
DeviceService service,
|
||||
string command,
|
||||
string postData,
|
||||
string? header = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
|
||||
{
|
||||
Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
|
||||
};
|
||||
|
||||
request.Headers.TryAddWithoutValidation(
|
||||
"SOAPACTION",
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"\"{0}#{1}\"",
|
||||
service.ServiceType,
|
||||
command));
|
||||
request.Headers.Pragma.ParseAdd("no-cache");
|
||||
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
|
||||
}
|
||||
|
||||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
|
||||
{
|
||||
OldMediaInfo = oldMediaInfo;
|
||||
NewMediaInfo = newMediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject OldMediaInfo { get; set; }
|
||||
|
||||
public UBaseObject NewMediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,980 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Photo = MediaBrowser.Controller.Entities.Photo;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlayToController : ISessionController, IDisposable
|
||||
{
|
||||
private readonly SessionInfo _session;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly string _serverAddress;
|
||||
private readonly string? _accessToken;
|
||||
|
||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||
private Device _device;
|
||||
private int _currentPlaylistIndex;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public PlayToController(
|
||||
SessionInfo session,
|
||||
ISessionManager sessionManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger logger,
|
||||
IDlnaManager dlnaManager,
|
||||
IUserManager userManager,
|
||||
IImageProcessor imageProcessor,
|
||||
string serverAddress,
|
||||
string? accessToken,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
Device device)
|
||||
{
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dlnaManager = dlnaManager;
|
||||
_userManager = userManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_serverAddress = serverAddress;
|
||||
_accessToken = accessToken;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
|
||||
_device = device;
|
||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||
_device.PlaybackStart += OnDevicePlaybackStart;
|
||||
_device.PlaybackProgress += OnDevicePlaybackProgress;
|
||||
_device.PlaybackStopped += OnDevicePlaybackStopped;
|
||||
_device.MediaChanged += OnDeviceMediaChanged;
|
||||
|
||||
_device.Start();
|
||||
|
||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
/*
|
||||
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
*/
|
||||
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
|
||||
{
|
||||
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
|
||||
var nextItemIndex = currentPlayListItemIndex + 1;
|
||||
var nextItem = _playlist[nextItemIndex];
|
||||
|
||||
// Send the SetNextAvTransport message.
|
||||
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceUnavailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Could throw if the session is already gone
|
||||
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string? usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string? nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
if (streamInfo.Item is not null)
|
||||
{
|
||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
||||
|
||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
if (streamInfo.Item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newItemProgress = GetProgressInfo(streamInfo);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
|
||||
if (currentItemIndex >= 0)
|
||||
{
|
||||
_currentPlaylistIndex = currentItemIndex;
|
||||
}
|
||||
|
||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (streamInfo.Item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
||||
|
||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var duration = mediaSource is null
|
||||
? _device.Duration?.Ticks
|
||||
: mediaSource.RunTimeTicks;
|
||||
|
||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
||||
|
||||
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
|
||||
{
|
||||
double percent = positionTicks.Value;
|
||||
percent /= duration.Value;
|
||||
|
||||
playedToCompletion = Math.Abs(1 - percent) <= .1;
|
||||
}
|
||||
|
||||
if (playedToCompletion)
|
||||
{
|
||||
await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_playlist.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting playback stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
|
||||
{
|
||||
ItemId = streamInfo.ItemId,
|
||||
SessionId = _session.Id,
|
||||
PositionTicks = positionTicks,
|
||||
MediaSourceId = streamInfo.MediaSourceId
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var progress = GetProgressInfo(info);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mediaUrl = e.MediaInfo.Url;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var progress = GetProgressInfo(info);
|
||||
|
||||
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private long? GetProgressPositionTicks(StreamParams info)
|
||||
{
|
||||
var ticks = _device.Position.Ticks;
|
||||
|
||||
if (!EnableClientSideSeek(info))
|
||||
{
|
||||
ticks += info.StartPositionTicks;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
private PlaybackStartInfo GetProgressInfo(StreamParams info)
|
||||
{
|
||||
return new PlaybackStartInfo
|
||||
{
|
||||
ItemId = info.ItemId,
|
||||
SessionId = _session.Id,
|
||||
PositionTicks = GetProgressPositionTicks(info),
|
||||
IsMuted = _device.IsMuted,
|
||||
IsPaused = _device.IsPaused,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
AudioStreamIndex = info.AudioStreamIndex,
|
||||
SubtitleStreamIndex = info.SubtitleStreamIndex,
|
||||
VolumeLevel = _device.Volume,
|
||||
|
||||
CanSeek = true,
|
||||
|
||||
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
|
||||
};
|
||||
}
|
||||
|
||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = command.ControllingUserId.Equals(default)
|
||||
? null :
|
||||
_userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
var items = new List<BaseItem>();
|
||||
foreach (var id in command.ItemIds)
|
||||
{
|
||||
AddItemFromId(id, items);
|
||||
}
|
||||
|
||||
var startIndex = command.StartIndex ?? 0;
|
||||
int len = items.Count - startIndex;
|
||||
if (startIndex > 0)
|
||||
{
|
||||
items = items.GetRange(startIndex, len);
|
||||
}
|
||||
|
||||
var playlist = new PlaylistItem[len];
|
||||
|
||||
// Not nullable enabled - so this is required.
|
||||
playlist[0] = CreatePlaylistItem(
|
||||
items[0],
|
||||
user,
|
||||
command.StartPositionTicks ?? 0,
|
||||
command.MediaSourceId ?? string.Empty,
|
||||
command.AudioStreamIndex,
|
||||
command.SubtitleStreamIndex);
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
|
||||
}
|
||||
|
||||
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
||||
|
||||
if (command.PlayCommand == PlayCommand.PlayLast)
|
||||
{
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (command.PlayCommand == PlayCommand.PlayNext)
|
||||
{
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (!command.ControllingUserId.Equals(default))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(
|
||||
_session.Client,
|
||||
_session.ApplicationVersion,
|
||||
_session.DeviceId,
|
||||
_session.DeviceName,
|
||||
_session.RemoteEndPoint,
|
||||
user);
|
||||
}
|
||||
|
||||
return PlayItems(playlist, cancellationToken);
|
||||
}
|
||||
|
||||
private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.Command)
|
||||
{
|
||||
case PlaystateCommand.Stop:
|
||||
_playlist.Clear();
|
||||
return _device.SetStop(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Pause:
|
||||
return _device.SetPause(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Unpause:
|
||||
return _device.SetPlay(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.PlayPause:
|
||||
return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Seek:
|
||||
return Seek(command.SeekPositionTicks ?? 0);
|
||||
|
||||
case PlaystateCommand.NextTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
|
||||
|
||||
case PlaystateCommand.PreviousTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Seek(long newPosition)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null && !EnableClientSideSeek(info))
|
||||
{
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamParams info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamInfo info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
private void AddItemFromId(Guid id, List<BaseItem> list)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistItem CreatePlaylistItem(
|
||||
BaseItem item,
|
||||
User? user,
|
||||
long startPostionTicks,
|
||||
string? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex)
|
||||
{
|
||||
var deviceInfo = _device.Properties;
|
||||
|
||||
var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
var mediaSources = item is IHasMediaSources
|
||||
? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
|
||||
: Array.Empty<MediaSourceInfo>();
|
||||
|
||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
||||
|
||||
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
|
||||
|
||||
var itemXml = new DidlBuilder(
|
||||
profile,
|
||||
user,
|
||||
_imageProcessor,
|
||||
_serverAddress,
|
||||
_accessToken,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_logger,
|
||||
_mediaEncoder,
|
||||
_libraryManager)
|
||||
.GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
|
||||
|
||||
playlistItem.Didl = itemXml;
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
private string? GetDlnaHeaders(PlaylistItem item)
|
||||
{
|
||||
var profile = item.Profile;
|
||||
var streamInfo = item.StreamInfo;
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
return ContentFeatureBuilder.BuildAudioHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
streamInfo.TargetAudioSampleRate,
|
||||
streamInfo.TargetAudioChannels,
|
||||
streamInfo.TargetAudioBitDepth,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TranscodeSeekInfo);
|
||||
}
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoBitrate,
|
||||
streamInfo.TargetTimestamp,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
streamInfo.TranscodeSeekInfo,
|
||||
streamInfo.IsTargetAnamorphic,
|
||||
streamInfo.IsTargetInterlaced,
|
||||
streamInfo.TargetRefFrames,
|
||||
streamInfo.TargetVideoStreamCount,
|
||||
streamInfo.TargetAudioStreamCount,
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
if (item.MediaType == MediaType.Video)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
SubtitleStreamIndex = subtitleStreamIndex
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (item.MediaType == MediaType.Audio)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (item.MediaType == MediaType.Photo)
|
||||
{
|
||||
return PlaylistItemFactory.Create((Photo)item, profile);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unrecognized item type.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><c>true</c> on success.</returns>
|
||||
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_playlist.Clear();
|
||||
_playlist.AddRange(items);
|
||||
_logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
|
||||
|
||||
await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (index < 0 || index >= _playlist.Count)
|
||||
{
|
||||
_playlist.Clear();
|
||||
await _device.SetStop(cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPlaylistIndex = index;
|
||||
var currentitem = _playlist[index];
|
||||
|
||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var streamInfo = currentitem.StreamInfo;
|
||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_device.PlaybackStart -= OnDevicePlaybackStart;
|
||||
_device.PlaybackProgress -= OnDevicePlaybackProgress;
|
||||
_device.PlaybackStopped -= OnDevicePlaybackStopped;
|
||||
_device.MediaChanged -= OnDeviceMediaChanged;
|
||||
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
_device.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.Name)
|
||||
{
|
||||
case GeneralCommandType.VolumeDown:
|
||||
return _device.VolumeDown(cancellationToken);
|
||||
case GeneralCommandType.VolumeUp:
|
||||
return _device.VolumeUp(cancellationToken);
|
||||
case GeneralCommandType.Mute:
|
||||
return _device.Mute(cancellationToken);
|
||||
case GeneralCommandType.Unmute:
|
||||
return _device.Unmute(cancellationToken);
|
||||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute(cancellationToken);
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string? index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string? vol))
|
||||
{
|
||||
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||
{
|
||||
return _device.SetVolume(volume, cancellationToken);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported volume value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("Volume argument cannot be null");
|
||||
default:
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetAudioStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetSubtitleStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
|
||||
{
|
||||
const int MaxWait = 15000000;
|
||||
const int Interval = 500;
|
||||
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
|
||||
{
|
||||
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||
currentWait += Interval;
|
||||
}
|
||||
|
||||
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
return name switch
|
||||
{
|
||||
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
|
||||
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
|
||||
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
|
||||
_ => Task.CompletedTask // Not supported or needed right now
|
||||
};
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo? _mediaSource;
|
||||
private IMediaSourceManager? _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
public bool IsDirectStream { get; set; }
|
||||
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public string? DeviceProfileId { get; set; }
|
||||
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
public string? MediaSourceId { get; set; }
|
||||
|
||||
public string? LiveStreamId { get; set; }
|
||||
|
||||
public BaseItem? Item { get; set; }
|
||||
|
||||
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_mediaSource is not null)
|
||||
{
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
if (Item is not IHasMediaSources)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_mediaSourceManager is not null)
|
||||
{
|
||||
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
private static Guid GetItemId(string url)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
var parts = url.Split('/');
|
||||
|
||||
for (var i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(parts[i + 1], out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
var request = new StreamParams
|
||||
{
|
||||
ItemId = GetItemId(url)
|
||||
};
|
||||
|
||||
if (request.ItemId.Equals(default))
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
var index = url.IndexOf('?', StringComparison.Ordinal);
|
||||
if (index == -1)
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
var query = url.Substring(index + 1);
|
||||
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
|
||||
|
||||
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
|
||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||
|
||||
request.Item = libraryManager.GetItemById(request.ItemId);
|
||||
|
||||
request._mediaSourceManager = mediaSourceManager;
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,258 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public sealed class PlayToManager : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = e.Argument;
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string? usn))
|
||||
{
|
||||
usn = string.Empty;
|
||||
}
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string? nt))
|
||||
{
|
||||
nt = string.Empty;
|
||||
}
|
||||
|
||||
// It has to report that it's a media renderer
|
||||
if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
|
||||
&& !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellationToken = _disposeCancellationTokenSource.Token;
|
||||
|
||||
await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await AddDevice(info, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating PlayTo device.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetUuid(string usn)
|
||||
{
|
||||
const string UuidStr = "uuid:";
|
||||
const string UuidColonStr = "::";
|
||||
|
||||
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
{
|
||||
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
|
||||
|
||||
index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
tmp = tmp[..index];
|
||||
}
|
||||
|
||||
index = tmp.IndexOf('{');
|
||||
if (index != -1)
|
||||
{
|
||||
int endIndex = tmp.IndexOf('}');
|
||||
if (endIndex != -1)
|
||||
{
|
||||
tmp = tmp[(index + 1)..endIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return tmp.ToString();
|
||||
}
|
||||
|
||||
private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = info.Location;
|
||||
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
||||
|
||||
if (info.Headers.TryGetValue("USN", out string? uuid))
|
||||
{
|
||||
uuid = GetUuid(uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var sessionInfo = await _sessionManager
|
||||
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
|
||||
|
||||
if (controller is null)
|
||||
{
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (device is null)
|
||||
{
|
||||
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string deviceName = device.Properties.Name;
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder,
|
||||
device);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
_sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities
|
||||
{
|
||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||
|
||||
SupportedCommands = new[]
|
||||
{
|
||||
GeneralCommandType.VolumeDown,
|
||||
GeneralCommandType.VolumeUp,
|
||||
GeneralCommandType.Mute,
|
||||
GeneralCommandType.Unmute,
|
||||
GeneralCommandType.ToggleMute,
|
||||
GeneralCommandType.SetVolume,
|
||||
GeneralCommandType.SetAudioStreamIndex,
|
||||
GeneralCommandType.SetSubtitleStreamIndex,
|
||||
GeneralCommandType.PlayMediaSource
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
});
|
||||
|
||||
_logger.LogInformation("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||
|
||||
try
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error while disposing PlayToManager");
|
||||
}
|
||||
|
||||
_sessionLock.Dispose();
|
||||
_disposeCancellationTokenSource.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaylistItem
|
||||
{
|
||||
public string StreamUrl { get; set; }
|
||||
|
||||
public string Didl { get; set; }
|
||||
|
||||
public StreamInfo StreamInfo { get; set; }
|
||||
|
||||
public DeviceProfile Profile { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public static class PlaylistItemFactory
|
||||
{
|
||||
public static PlaylistItem Create(Photo item, DeviceProfile profile)
|
||||
{
|
||||
var playlistItem = new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamInfo
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaType = DlnaProfileType.Photo,
|
||||
DeviceProfile = profile
|
||||
},
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
|
||||
var directPlay = profile.DirectPlayProfiles
|
||||
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item));
|
||||
|
||||
if (directPlay is not null)
|
||||
{
|
||||
playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream;
|
||||
playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
var transcodingProfile = profile.TranscodingProfiles
|
||||
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo);
|
||||
|
||||
if (transcodingProfile is not null)
|
||||
{
|
||||
playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
|
||||
playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
|
||||
}
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
private static bool IsSupported(DirectPlayProfile profile, Photo item)
|
||||
{
|
||||
var mediaPath = item.Path;
|
||||
|
||||
if (profile.Container.Length > 0)
|
||||
{
|
||||
// Check container type
|
||||
var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.');
|
||||
|
||||
if (!profile.SupportsContainer(mediaContainer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Ssdp;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class TransportCommands
|
||||
{
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
|
||||
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||
|
||||
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||
|
||||
public static TransportCommands Create(XDocument document)
|
||||
{
|
||||
var command = new TransportCommands();
|
||||
|
||||
var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
|
||||
|
||||
foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
|
||||
{
|
||||
command.ServiceActions.Add(ServiceActionFromXml(container));
|
||||
}
|
||||
|
||||
var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
|
||||
|
||||
if (stateValues is not null)
|
||||
{
|
||||
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
|
||||
{
|
||||
command.StateVariables.Add(FromXml(container));
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static ServiceAction ServiceActionFromXml(XElement container)
|
||||
{
|
||||
var serviceAction = new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
};
|
||||
|
||||
var argumentList = serviceAction.ArgumentList;
|
||||
|
||||
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
|
||||
{
|
||||
argumentList.Add(ArgumentFromXml(arg));
|
||||
}
|
||||
|
||||
return serviceAction;
|
||||
}
|
||||
|
||||
private static Argument ArgumentFromXml(XElement container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static StateVariable FromXml(XElement container)
|
||||
{
|
||||
var allowedValues = Array.Empty<string>();
|
||||
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
|
||||
.FirstOrDefault();
|
||||
|
||||
if (element is not null)
|
||||
{
|
||||
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
|
||||
|
||||
allowedValues = values.Select(child => child.Value).ToArray();
|
||||
}
|
||||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||
AllowedValues = allowedValues
|
||||
};
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, null);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else if (dictionary.TryGetValue(arg.Name, out var argValue))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, argValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
|
||||
{
|
||||
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (state is not null)
|
||||
{
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Core of the AVTransport service. It defines the conceptually top-
|
||||
/// level state of the transport, for example, whether it is playing, recording, etc.
|
||||
/// </summary>
|
||||
public enum TransportState
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Ssdp;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class UpnpContainer : UBaseObject
|
||||
{
|
||||
public static UBaseObject Create(XElement container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
return new UBaseObject
|
||||
{
|
||||
Id = container.GetAttributeValue(UPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(UPnpNamespaces.Title),
|
||||
IconUrl = container.GetValue(UPnpNamespaces.Artwork),
|
||||
UpnpClass = container.GetValue(UPnpNamespaces.Class)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class UBaseObject
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string SecondText { get; set; }
|
||||
|
||||
public string IconUrl { get; set; }
|
||||
|
||||
public string MetaData { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public IReadOnlyList<string> ProtocolInfo { get; set; }
|
||||
|
||||
public string UpnpClass { get; set; }
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
var classType = UpnpClass ?? string.Empty;
|
||||
|
||||
if (classType.Contains("Audio", StringComparison.Ordinal))
|
||||
{
|
||||
return "Audio";
|
||||
}
|
||||
|
||||
if (classType.Contains("Video", StringComparison.Ordinal))
|
||||
{
|
||||
return "Video";
|
||||
}
|
||||
|
||||
if (classType.Contains("image", StringComparison.Ordinal))
|
||||
{
|
||||
return "Photo";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(UBaseObject obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public static class UPnpNamespaces
|
||||
{
|
||||
public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
|
||||
public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
|
||||
|
||||
public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
|
||||
|
||||
public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
|
||||
|
||||
public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
|
||||
|
||||
public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
|
||||
|
||||
public static XName Containers { get; } = Ns + "container";
|
||||
|
||||
public static XName Items { get; } = Ns + "item";
|
||||
|
||||
public static XName Title { get; } = Dc + "title";
|
||||
|
||||
public static XName Creator { get; } = Dc + "creator";
|
||||
|
||||
public static XName Artist { get; } = UPnp + "artist";
|
||||
|
||||
public static XName Id { get; } = "id";
|
||||
|
||||
public static XName ParentId { get; } = "parentID";
|
||||
|
||||
public static XName Class { get; } = UPnp + "class";
|
||||
|
||||
public static XName Artwork { get; } = UPnp + "albumArtURI";
|
||||
|
||||
public static XName Description { get; } = Dc + "description";
|
||||
|
||||
public static XName LongDescription { get; } = UPnp + "longDescription";
|
||||
|
||||
public static XName Album { get; } = UPnp + "album";
|
||||
|
||||
public static XName Author { get; } = UPnp + "author";
|
||||
|
||||
public static XName Director { get; } = UPnp + "director";
|
||||
|
||||
public static XName PlayCount { get; } = UPnp + "playbackCount";
|
||||
|
||||
public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
|
||||
|
||||
public static XName Res { get; } = Ns + "res";
|
||||
|
||||
public static XName Duration { get; } = "duration";
|
||||
|
||||
public static XName ProtocolInfo { get; } = "protocolInfo";
|
||||
|
||||
public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
|
||||
|
||||
public static XName StateVariable { get; } = Svc + "stateVariable";
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
{
|
||||
[System.Xml.Serialization.XmlRoot("Profile")]
|
||||
public class DefaultProfile : DeviceProfile
|
||||
{
|
||||
public DefaultProfile()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
Name = "Generic Device";
|
||||
|
||||
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
||||
|
||||
Manufacturer = "Jellyfin";
|
||||
ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
|
||||
ModelName = "Jellyfin Server";
|
||||
ModelNumber = "01";
|
||||
ModelUrl = "https://github.com/jellyfin/jellyfin";
|
||||
ManufacturerUrl = "https://github.com/jellyfin/jellyfin";
|
||||
|
||||
AlbumArtPn = "JPEG_SM";
|
||||
|
||||
MaxAlbumArtHeight = 480;
|
||||
MaxAlbumArtWidth = 480;
|
||||
|
||||
MaxIconWidth = 48;
|
||||
MaxIconHeight = 48;
|
||||
|
||||
MaxStreamingBitrate = 140000000;
|
||||
MaxStaticBitrate = 140000000;
|
||||
MusicStreamingTranscodingBitrate = 192000;
|
||||
|
||||
EnableAlbumArtInDidl = false;
|
||||
|
||||
TranscodingProfiles = new[]
|
||||
{
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "mp3",
|
||||
AudioCodec = "mp3",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "ts",
|
||||
Type = DlnaProfileType.Video,
|
||||
AudioCodec = "aac",
|
||||
VideoCodec = "h264"
|
||||
},
|
||||
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "jpeg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
// play all
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
// play all
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Audio
|
||||
}
|
||||
};
|
||||
|
||||
SubtitleProfiles = new[]
|
||||
{
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sup",
|
||||
Method = SubtitleDeliveryMethod.External
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ass",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ssa",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "smi",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "dvdsub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgs",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgssub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sup",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "subrip",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "vtt",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "m4v",
|
||||
Type = DlnaProfileType.Video,
|
||||
MimeType = "video/mp4"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Generic Device</Name>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="" type="Video" />
|
||||
<DirectPlayProfile container="" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,68 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Denon AVR</Name>
|
||||
<Identification>
|
||||
<FriendlyName>Denon:\[AVR:.*</FriendlyName>
|
||||
<Manufacturer>Denon</Manufacturer>
|
||||
<Headers />
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Audio" container="flac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioSampleRate" value="96000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>DirecTV HD-DVR</Name>
|
||||
<Identification>
|
||||
<FriendlyName>^DIRECTV.*$</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="DIRECTV" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>true</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>true</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="jpeg,jpg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,96 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Dish Hopper-Joey</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Echostar Technologies LLC</Manufacturer>
|
||||
<ManufacturerUrl>http://www.echostar.com</ManufacturerUrl>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Zip_" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp3,alac,flac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3,he-aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="IsSecondaryAudio" value="false" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="mkv,ts,mpegts" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>LG Smart TV</Name>
|
||||
<Identification>
|
||||
<FriendlyName>LG.*</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="LG" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts,avi,mkv,m2ts" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg4">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3,eac3,aac,mp3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" type="Video" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Linksys DMA2100</Name>
|
||||
<Identification>
|
||||
<ModelName>DMA2100us</ModelName>
|
||||
<Headers />
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
|
||||
<DirectPlayProfile container="avi,mp4,mkv,ts,mpegts,m4v" type="Video" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,62 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Marantz</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Marantz</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Marantz" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="aac,mp3,wav,wma,flac" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,62 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>MediaMonkey</Name>
|
||||
<Identification>
|
||||
<FriendlyName>MediaMonkey</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="MediaMonkey" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,87 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Panasonic Viera</Name>
|
||||
<Identification>
|
||||
<FriendlyName>VIERA</FriendlyName>
|
||||
<Manufacturer>Panasonic</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Panasonic MIL DLNA" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,pcm_dvd" videoCodec="mpeg2video,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="aac,ac3,dca,mp3,mp2,pcm,dts" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,mp3,mp2" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,mp3,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mov" audioCodec="aac,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="flv" audioCodec="aac" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Popcorn Hour</Name>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp4,mov,m4v" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,eac3,mp3,mp2,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="asf,wmv" audioCodec="wmav2,wmapro" videoCodec="wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="mp3,ac3,eac3,mp2,pcm" videoCodec="mpeg4,msmpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="aac,mp3,ac3,eac3,mp2,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,flac,ogg,wma,wav" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg,gif,bmp,png" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="EqualsAny" property="VideoProfile" value="baseline|constrained baseline" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="mp3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="320000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,128 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Samsung Smart TV</Name>
|
||||
<Identification>
|
||||
<ModelUrl>samsung.com</ModelUrl>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="SEC_" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="asf" audioCodec="mp3,ac3,wmav2,wmapro,wmavoice" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="mp3,ac3,dca,dts" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="mp3,ac3,dca,aac,dts" videoCodec="h264,mpeg4,mjpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="mp3,aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="3gp" audioCodec="aac,he-aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mpg,mpeg" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||
<DirectPlayProfile container="vro,vob" audioCodec="ac3,mp2,mp3" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3,eac3" videoCodec="mpeg2video,h264,vc1" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmavoice" videoCodec="wmv2,wmv3" type="Video" />
|
||||
<DirectPlayProfile container="mp3,flac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="30720000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg4">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="37500000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="wmv2,wmv3,vc1">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="25600000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="wmav2,dca,aac,mp3,dts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="avi" type="Video" mimeType="video/x-msvideo">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mkv" type="Video" mimeType="video/x-mkv">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="flac" type="Audio" mimeType="audio/x-flac">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" didlMode="CaptionInfoEx" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,60 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sharp Smart TV</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Sharp</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Sharp" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>true</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>true</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="m4v,mkv,avi,mov,mp4" audioCodec="aac,mp3,ac3,dts,dca" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="asf,wmv" type="Video" />
|
||||
<DirectPlayProfile container="mpg,mpeg" audioCodec="mp3,aac" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="flv" audioCodec="mp3,aac" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp3,wav" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3,dts,dca" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,133 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2010)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,139 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2011)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|