aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs10
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs5
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs183
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs19
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs1017
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs5
-rw-r--r--MediaBrowser.Model/Dlna/VideoOptions.cs2
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs4
-rw-r--r--MediaBrowser.Model/Properties/AssemblyInfo.cs1
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs60
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs9
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs36
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs24
-rw-r--r--src/Jellyfin.Extensions/Json/JsonDefaults.cs1
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj6
-rw-r--r--tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs466
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json332
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json430
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json448
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json90
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json441
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json137
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json137
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json9
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json211
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json211
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json357
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json139
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json189
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json189
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json73
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json72
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json73
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json72
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json73
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json72
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json72
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json72
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json87
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json87
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json89
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json71
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json75
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json89
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json91
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json74
-rw-r--r--tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json102
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs28
51 files changed, 5882 insertions, 678 deletions
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index b422eb78c..75df18204 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var profile = playbackInfoDto?.DeviceProfile;
- _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
+ _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile);
if (profile == null)
{
@@ -225,14 +225,6 @@ namespace Jellyfin.Api.Controllers
}
}
- if (info.MediaSources != null)
- {
- foreach (var mediaSource in info.MediaSources)
- {
- _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
- }
- }
-
return info;
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index bc9527a0b..6fcafd426 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers
DeInterlace = false,
RequireNonAnamorphic = false,
EnableMpegtsM2TsMode = false,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
EnableAdaptiveBitrateStreaming = true
@@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers
CopyTimestamps = true,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Embed,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
Context = EncodingContext.Static
};
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 3fa516043..31b979836 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -191,7 +191,9 @@ namespace Jellyfin.Api.Helpers
DeviceId = auth.DeviceId,
ItemId = item.Id,
Profile = profile,
- MaxAudioChannels = maxAudioChannels
+ MaxAudioChannels = maxAudioChannels,
+ AllowAudioStreamCopy = allowAudioStreamCopy,
+ AllowVideoStreamCopy = allowVideoStreamCopy
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
@@ -208,7 +210,7 @@ namespace Jellyfin.Api.Helpers
mediaSource.SupportsDirectPlay = false;
}
- if (!enableDirectStream)
+ if (!enableDirectStream || !allowVideoStreamCopy)
{
mediaSource.SupportsDirectStream = false;
}
@@ -235,168 +237,79 @@ namespace Jellyfin.Api.Helpers
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
- // Beginning of Playback Determination: Attempt DirectPlay first
- if (mediaSource.SupportsDirectPlay)
- {
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- mediaSource.SupportsDirectPlay = false;
- }
- else
- {
- var supportsDirectStream = mediaSource.SupportsDirectStream;
-
- // Dummy this up to fool StreamBuilder
- mediaSource.SupportsDirectStream = true;
- options.MaxBitrate = maxBitrate;
+ options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
- if (item is Audio)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- options.ForceDirectPlay = true;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- options.ForceDirectPlay = true;
- }
- }
+ if (!options.ForceDirectStream)
+ {
+ // direct-stream http streaming is currently broken
+ options.EnableDirectStream = false;
+ }
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
+ // Beginning of Playback Determination
+ var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ ? streamBuilder.BuildAudioItem(options)
+ : streamBuilder.BuildVideoItem(options);
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectPlay = false;
- }
+ if (streamInfo != null)
+ {
+ streamInfo.PlaySessionId = playSessionId;
+ streamInfo.StartPositionTicks = startTimeTicks;
- // Set this back to what it was
- mediaSource.SupportsDirectStream = supportsDirectStream;
+ mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
+ // Players do not handle this being set according to PlayMethod
+ mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay;
+ mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null;
- if (streamInfo != null)
+ if (item is Audio)
+ {
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
+ mediaSource.SupportsTranscoding = false;
}
}
- }
-
- if (mediaSource.SupportsDirectStream)
- {
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
+ else if (item is Video)
{
- mediaSource.SupportsDirectStream = false;
- }
- else
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
-
- if (item is Audio)
+ if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
+ && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- options.ForceDirectStream = true;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- options.ForceDirectStream = true;
- }
- }
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectStream = false;
- }
-
- if (streamInfo != null)
- {
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
+ mediaSource.SupportsTranscoding = false;
}
}
- }
-
- if (mediaSource.SupportsTranscoding)
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
- }
+ mediaSource.SupportsDirectPlay = false;
+ mediaSource.SupportsDirectStream = false;
+
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
+ mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+ mediaSource.TranscodingContainer = streamInfo.Container;
+ mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
}
else
{
- if (streamInfo != null)
+ if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)
{
- streamInfo.PlaySessionId = playSessionId;
+ streamInfo.PlayMethod = PlayMethod.Transcode;
+ mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
- if (streamInfo.PlayMethod == PlayMethod.Transcode)
+ if (!allowVideoStreamCopy)
{
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
- if (!allowVideoStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
-
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+ mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
-
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
+
+ // Do this after the above so that StartPositionTicks is set
+ SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
foreach (var attachment in mediaSource.MediaAttachments)
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c8762b7c5..49a394868 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -479,7 +479,7 @@ namespace Jellyfin.Api.Helpers
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
HardwareAccelerationType = hardwareAccelerationType,
- TranscodeReasons = state.TranscodeReasons
+ TranscodeReason = state.TranscodeReason
});
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 2e7349f00..261ce915f 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1799,7 +1799,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return false;
}
- return request.EnableAutoStreamCopy;
+ return true;
}
public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs)
@@ -1856,17 +1856,11 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Video bitrate must fall within requested value
- if (request.AudioBitRate.HasValue)
+ if (request.AudioBitRate.HasValue
+ && audioStream.BitDepth.HasValue
+ && audioStream.BitRate.Value > request.AudioBitRate.Value)
{
- if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
- {
- return false;
- }
-
- if (audioStream.BitRate.Value > request.AudioBitRate.Value)
- {
- return false;
- }
+ return false;
}
return request.EnableAutoStreamCopy;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index c4affa567..4f6743590 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? OutputAudioBitrate;
public int? OutputAudioChannels;
- private TranscodeReason[] _transcodeReasons = null;
+ private TranscodeReason? _transcodeReasons = null;
public EncodingJobInfo(TranscodingJobType jobType)
{
@@ -34,25 +35,23 @@ namespace MediaBrowser.Controller.MediaEncoding
SupportedSubtitleCodecs = Array.Empty<string>();
}
- public TranscodeReason[] TranscodeReasons
+ public TranscodeReason TranscodeReason
{
get
{
- if (_transcodeReasons == null)
+ if (!_transcodeReasons.HasValue)
{
if (BaseRequest.TranscodeReasons == null)
{
- return Array.Empty<TranscodeReason>();
+ _transcodeReasons = 0;
+ return 0;
}
- _transcodeReasons = BaseRequest.TranscodeReasons
- .Split(',')
- .Where(i => !string.IsNullOrEmpty(i))
- .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
- .ToArray();
+ _ = Enum.TryParse<TranscodeReason>(BaseRequest.TranscodeReasons, out var reason);
+ _transcodeReasons = reason;
}
- return _transcodeReasons;
+ return _transcodeReasons.Value;
}
}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs
index 4d4d8d78c..33755e746 100644
--- a/MediaBrowser.Model/Dlna/AudioOptions.cs
+++ b/MediaBrowser.Model/Dlna/AudioOptions.cs
@@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Dlna
public bool ForceDirectStream { get; set; }
+ public bool AllowAudioStreamCopy { get; set; }
+
public Guid ItemId { get; set; }
public MediaSourceInfo[] MediaSources { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 094895225..444b0e4b2 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -15,6 +15,12 @@ namespace MediaBrowser.Model.Dlna
{
public class StreamBuilder
{
+ // Aliases
+ internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit;
+ internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal;
+ internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported;
+ internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported;
+
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
@@ -143,7 +149,7 @@ namespace MediaBrowser.Model.Dlna
}).ThenBy(streams.IndexOf);
}
- private static TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition)
+ private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition)
{
switch (condition.Property)
{
@@ -161,7 +167,7 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.Has64BitOffsets:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.Height:
return TranscodeReason.VideoResolutionNotSupported;
@@ -171,7 +177,7 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.IsAvc:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.IsInterlaced:
return TranscodeReason.InterlacedVideoNotSupported;
@@ -181,15 +187,15 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.NumAudioStreams:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.NumVideoStreams:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.PacketLength:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.RefFrames:
return TranscodeReason.RefFramesNotSupported;
@@ -217,17 +223,17 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.VideoTimestamp:
// TODO
- return null;
+ return 0;
case ProfileConditionValue.Width:
return TranscodeReason.VideoResolutionNotSupported;
default:
- return null;
+ return 0;
}
}
- public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type)
+ public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
{
@@ -236,16 +242,12 @@ namespace MediaBrowser.Model.Dlna
var formats = ContainerProfile.SplitValue(inputContainer);
- if (formats.Length == 1)
- {
- return formats[0];
- }
-
if (profile != null)
{
+ var playProfiles = playProfile == null ? profile.DirectPlayProfiles : new[] { playProfile };
foreach (var format in formats)
{
- foreach (var directPlayProfile in profile.DirectPlayProfiles)
+ foreach (var directPlayProfile in playProfiles)
{
if (directPlayProfile.Type == type
&& directPlayProfile.SupportsContainer(format))
@@ -287,69 +289,27 @@ namespace MediaBrowser.Model.Dlna
var audioStream = item.GetDefaultAudioStream(null);
- var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options);
+ var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
- var directPlayMethods = directPlayInfo.PlayMethods;
- var transcodeReasons = directPlayInfo.TranscodeReasons.ToList();
+ var directPlayMethod = directPlayInfo.PlayMethod;
+ var transcodeReasons = directPlayInfo.TranscodeReasons;
int? inputAudioChannels = audioStream?.Channels;
int? inputAudioBitrate = audioStream?.BitDepth;
int? inputAudioSampleRate = audioStream?.SampleRate;
int? inputAudioBitDepth = audioStream?.BitDepth;
- if (directPlayMethods.Any())
+ if (directPlayMethod.HasValue)
{
- string audioCodec = audioStream?.Codec;
-
- // Make sure audio codec profiles are satisfied
- var conditions = new List<ProfileCondition>();
- foreach (var i in options.Profile.CodecProfiles)
- {
- if (i.Type == CodecType.Audio && i.ContainsAnyCodec(audioCodec, item.Container))
- {
- bool applyConditions = true;
- foreach (ProfileCondition applyCondition in i.ApplyConditions)
- {
- if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
- {
- LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
- applyConditions = false;
- break;
- }
- }
-
- if (applyConditions)
- {
- conditions.AddRange(i.Conditions);
- }
- }
- }
-
- bool all = true;
- foreach (ProfileCondition c in conditions)
- {
- if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
- {
- LogConditionFailure(options.Profile, "AudioCodecProfile", c, item);
- var transcodeReason = GetTranscodeReasonForFailedCondition(c);
- if (transcodeReason.HasValue)
- {
- transcodeReasons.Add(transcodeReason.Value);
- }
-
- all = false;
- break;
- }
- }
+ var profile = options.Profile;
+ var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
+ var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
+ transcodeReasons |= audioFailureReasons;
- if (all)
+ if (audioFailureReasons == 0)
{
- if (directPlayMethods.Contains(PlayMethod.DirectStream))
- {
- playlistItem.PlayMethod = PlayMethod.DirectStream;
- }
-
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
+ playlistItem.PlayMethod = directPlayMethod.Value;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
return playlistItem;
}
@@ -374,45 +334,9 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile);
-
- var audioCodecProfiles = new List<CodecProfile>();
- foreach (var i in options.Profile.CodecProfiles)
- {
- if (i.Type == CodecType.Audio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container))
- {
- audioCodecProfiles.Add(i);
- }
-
- if (audioCodecProfiles.Count >= 1)
- {
- break;
- }
- }
-
- var audioTranscodingConditions = new List<ProfileCondition>();
- foreach (var i in audioCodecProfiles)
- {
- bool applyConditions = true;
- foreach (var applyCondition in i.ApplyConditions)
- {
- if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
- {
- LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
- applyConditions = false;
- break;
- }
- }
-
- if (applyConditions)
- {
- foreach (ProfileCondition c in i.Conditions)
- {
- audioTranscodingConditions.Add(c);
- }
- }
- }
+ SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
+ var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
// Honor requested max channels
@@ -434,7 +358,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
}
- playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+ playlistItem.TranscodeReasons = transcodeReasons;
return playlistItem;
}
@@ -448,9 +372,9 @@ namespace MediaBrowser.Model.Dlna
return options.GetMaxBitrate(isAudio);
}
- private (IEnumerable<PlayMethod> PlayMethods, IEnumerable<TranscodeReason> TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
+ private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
{
- DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles
+ var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
if (directPlayProfile == null)
@@ -461,64 +385,56 @@ namespace MediaBrowser.Model.Dlna
item.Path ?? "Unknown path",
audioStream.Codec ?? "Unknown codec");
- return (Enumerable.Empty<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
+ return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
var playMethods = new List<PlayMethod>();
- var transcodeReasons = new List<TranscodeReason>();
+ TranscodeReason transcodeReasons = 0;
- // While options takes the network and other factors into account. Only applies to direct stream
- if (item.SupportsDirectStream)
+ // The profile describes what the device supports
+ // If device requirements are satisfied then allow both direct stream and direct play
+ if (item.SupportsDirectPlay)
{
- if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+ if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay))
{
- if (options.EnableDirectStream)
+ if (options.EnableDirectPlay)
{
- playMethods.Add(PlayMethod.DirectStream);
+ return (directPlayProfile, PlayMethod.DirectPlay, 0);
}
}
else
{
- transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+ transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
}
}
- // The profile describes what the device supports
- // If device requirements are satisfied then allow both direct stream and direct play
- if (item.SupportsDirectPlay)
+ // While options takes the network and other factors into account. Only applies to direct stream
+ if (item.SupportsDirectStream)
{
- if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay))
+ if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
{
- if (options.EnableDirectPlay)
+ if (options.EnableDirectStream)
{
- playMethods.Add(PlayMethod.DirectPlay);
+ return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons);
}
}
else
{
- transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+ transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
}
}
- if (playMethods.Count > 0)
- {
- transcodeReasons.Clear();
- }
- else
- {
- transcodeReasons = transcodeReasons.Distinct().ToList();
- }
-
- return (playMethods, transcodeReasons);
+ return (directPlayProfile, null, transcodeReasons);
}
- private static List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
+ private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
{
var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video;
var containerSupported = false;
var audioSupported = false;
var videoSupported = false;
+ TranscodeReason reasons = 0;
foreach (var profile in directPlayProfiles)
{
@@ -541,20 +457,20 @@ namespace MediaBrowser.Model.Dlna
var list = new List<TranscodeReason>();
if (!containerSupported)
{
- list.Add(TranscodeReason.ContainerNotSupported);
+ reasons |= TranscodeReason.ContainerNotSupported;
}
if (videoStream != null && !videoSupported)
{
- list.Add(TranscodeReason.VideoCodecNotSupported);
+ reasons |= TranscodeReason.VideoCodecNotSupported;
}
if (audioStream != null && !audioSupported)
{
- list.Add(TranscodeReason.AudioCodecNotSupported);
+ reasons |= TranscodeReason.AudioCodecNotSupported;
}
- return list;
+ return reasons;
}
private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
@@ -599,30 +515,29 @@ namespace MediaBrowser.Model.Dlna
return item.DefaultSubtitleStreamIndex;
}
- private static void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile)
+ private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, TranscodingProfile transcodingProfile)
{
- if (string.IsNullOrEmpty(transcodingProfile.AudioCodec))
- {
- playlistItem.AudioCodecs = Array.Empty<string>();
- }
- else
- {
- playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
- }
+ var container = transcodingProfile.Container;
+ var protocol = transcodingProfile.Protocol;
- playlistItem.Container = transcodingProfile.Container;
- playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
- playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+ item.TranscodingContainer = container;
+ item.TranscodingSubProtocol = protocol;
- if (string.IsNullOrEmpty(transcodingProfile.VideoCodec))
+ if (playlistItem.PlayMethod == PlayMethod.Transcode)
{
- playlistItem.VideoCodecs = Array.Empty<string>();
+ playlistItem.Container = container;
+ playlistItem.SubProtocol = protocol;
}
- else
+
+ playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+ if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)
+ && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
{
- playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(',');
+ playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
}
+ playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
+
playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
@@ -638,14 +553,21 @@ namespace MediaBrowser.Model.Dlna
{
playlistItem.SegmentLength = transcodingProfile.SegmentLength;
}
+ }
- playlistItem.SubProtocol = transcodingProfile.Protocol;
+ private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
+ {
+ var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
+ var protocol = "http";
- if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)
- && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
- {
- playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
- }
+ item.TranscodingContainer = container;
+ item.TranscodingSubProtocol = protocol;
+
+ playlistItem.Container = container;
+ playlistItem.SubProtocol = protocol;
+
+ playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
}
private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
@@ -674,13 +596,30 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream.Index;
}
+ // Collect candidate audio streams
+ IEnumerable<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream };
+ if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
+ {
+ if (audioStream?.IsDefault == true)
+ {
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault);
+ }
+ else
+ {
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language);
+ }
+ }
+
+ candidateAudioStreams = candidateAudioStreams.ToArray();
+
var videoStream = item.VideoStream;
// TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough
- var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay);
- var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream);
- bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay);
- bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay);
+ var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, options, PlayMethod.DirectPlay);
+ var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
+ bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0);
+ bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0);
+ var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult;
_logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@@ -689,189 +628,299 @@ namespace MediaBrowser.Model.Dlna
isEligibleForDirectPlay,
isEligibleForDirectStream);
- var transcodeReasons = new List<TranscodeReason>();
-
+ DirectPlayProfile directPlayProfile = null;
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
- var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream);
+ var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioStreams, subtitleStream, isEligibleForDirectPlay, isEligibleForDirectStream);
var directPlay = directPlayInfo.PlayMethod;
+ transcodeReasons |= directPlayInfo.TranscodeReasons;
if (directPlay != null)
{
+ directPlayProfile = directPlayInfo.Profile;
playlistItem.PlayMethod = directPlay.Value;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video);
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
+ playlistItem.VideoCodecs = new[] { videoStream.Codec };
+
+ if (directPlay == PlayMethod.DirectPlay)
+ {
+ playlistItem.SubProtocol = "http";
+
+ var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
+ if (audioStreamIndex.HasValue)
+ {
+ playlistItem.AudioStreamIndex = audioStreamIndex;
+ playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec };
+ }
+ }
+ else if (directPlay == PlayMethod.DirectStream)
+ {
+ playlistItem.AudioStreamIndex = audioStream?.Index;
+ if (audioStream != null)
+ {
+ playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
+ }
+
+ SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
+ BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec);
+ }
if (subtitleStream != null)
{
- var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null);
+ var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
}
-
- return playlistItem;
}
- transcodeReasons.AddRange(directPlayInfo.TranscodeReasons);
+ _logger.LogInformation(
+ "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}",
+ options.Profile.Name ?? "Anonymous Profile",
+ item.Path ?? "Unknown path",
+ directPlayInfo.PlayMethod,
+ directPlayInfo.AudioStreamIndex ?? audioStream?.Index,
+ playlistItem.SubtitleStreamIndex,
+ directPlayInfo.TranscodeReasons);
}
- if (directPlayEligibilityResult.Reason.HasValue)
+ playlistItem.TranscodeReasons = transcodeReasons;
+
+ if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream)
{
- transcodeReasons.Add(directPlayEligibilityResult.Reason.Value);
+ // Can't direct play, find the transcoding profile
+ // If we do this for direct-stream we will overwrite the info
+ var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem);
+ if (transcodingProfile != null)
+ {
+ SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
+
+ BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec);
+
+ if (subtitleStream != null)
+ {
+ var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
+
+ playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
+ playlistItem.SubtitleFormat = subtitleProfile.Format;
+ playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
+ }
+
+ if (playlistItem.PlayMethod != PlayMethod.DirectPlay)
+ {
+ playlistItem.PlayMethod = PlayMethod.Transcode;
+ }
+ }
}
- if (directStreamEligibilityResult.Reason.HasValue)
+ _logger.LogInformation(
+ "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}",
+ options.Profile.Name ?? "Anonymous Profile",
+ item.Path ?? "Unknown path",
+ options.AudioStreamIndex,
+ options.SubtitleStreamIndex,
+ playlistItem.PlayMethod,
+ playlistItem.TranscodeReasons,
+ playlistItem.ToUrl("media:", "<token>"));
+
+ item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
+ return playlistItem;
+ }
+
+ private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
+ {
+ if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
- transcodeReasons.Add(directStreamEligibilityResult.Reason.Value);
+ return null;
}
- // Can't direct play, find the transcoding profile
- TranscodingProfile transcodingProfile = null;
- foreach (var i in options.Profile.TranscodingProfiles)
+ var transcodingProfiles = options.Profile.TranscodingProfiles
+ .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context);
+
+ if (options.AllowVideoStreamCopy)
{
- if (i.Type == playlistItem.MediaType && i.Context == options.Context)
+ // prefer direct copy profile
+ float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
+ int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
+
+ transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile =>
{
- transcodingProfile = i;
- break;
- }
+ var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
+
+ if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream.Codec))
+ {
+ var videoCodec = transcodingProfile.VideoCodec;
+ var container = transcodingProfile.Container;
+ var appliedVideoConditions = options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.Video &&
+ i.ContainsAnyCodec(videoCodec, container))
+ .Select(i =>
+ i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
+ var conditionsSatisfied = !appliedVideoConditions.Any() || !appliedVideoConditions.Any(satisfied => !satisfied);
+ return conditionsSatisfied ? 1 : 2;
+ }
+
+ return 3;
+ })
+ .OrderBy(lookup => lookup.Key)
+ .SelectMany(lookup => lookup);
}
- if (transcodingProfile != null)
+ return transcodingProfiles.FirstOrDefault();
+ }
+
+ private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
+ {
+ // prefer matching video codecs
+ var videoCodecs = ContainerProfile.SplitValue(videoCodec);
+ var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream.Codec) ? videoStream.Codec : null;
+ playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs;
+
+ // copy video codec options as a starting point, this applies to transcode and direct-stream
+ playlistItem.MaxFramerate = videoStream.AverageFrameRate;
+ var qualifier = videoStream.Codec;
+ if (videoStream.Level.HasValue)
{
- if (!item.SupportsTranscoding)
+ playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (videoStream.BitDepth.HasValue)
+ {
+ playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (!string.IsNullOrEmpty(videoStream.Profile))
+ {
+ playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
+ }
+
+ if (videoStream.Level != 0)
+ {
+ playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
+ }
+
+ // prefer matching audio codecs, could do beter here
+ var audioCodecs = ContainerProfile.SplitValue(audioCodec);
+ var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
+ playlistItem.AudioCodecs = audioCodecs;
+ if (directAudioStream != null)
+ {
+ audioStream = directAudioStream;
+ playlistItem.AudioStreamIndex = audioStream.Index;
+ playlistItem.AudioCodecs = new[] { audioStream.Codec };
+
+ // copy matching audio codec options
+ playlistItem.AudioSampleRate = audioStream.SampleRate;
+ playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
+
+ if (!string.IsNullOrEmpty(audioStream.Profile))
{
- return null;
+ playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant());
}
- if (subtitleStream != null)
+ if (audioStream.Level != 0)
{
- var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
-
- playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
- playlistItem.SubtitleFormat = subtitleProfile.Format;
- playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
+ playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString());
}
+ }
+
+ int? width = videoStream?.Width;
+ int? height = videoStream?.Height;
+ int? bitDepth = videoStream?.BitDepth;
+ int? videoBitrate = videoStream?.BitRate;
+ double? videoLevel = videoStream?.Level;
+ string videoProfile = videoStream?.Profile;
+ float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ bool? isAnamorphic = videoStream?.IsAnamorphic;
+ bool? isInterlaced = videoStream?.IsInterlaced;
+ string videoCodecTag = videoStream?.CodecTag;
+ bool? isAvc = videoStream?.IsAVC;
- playlistItem.PlayMethod = PlayMethod.Transcode;
+ TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
+ int? packetLength = videoStream?.PacketLength;
+ int? refFrames = videoStream?.RefFrames;
- SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile);
+ int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
- var isFirstAppliedCodecProfile = true;
- foreach (var i in options.Profile.CodecProfiles)
+ var appliedVideoConditions = options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.Video &&
+ i.ContainsAnyCodec(videoCodec, container) &&
+ i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
+ var isFirstAppliedCodecProfile = true;
+ foreach (var i in appliedVideoConditions)
+ {
+ var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
+ foreach (var transcodingVideoCodec in transcodingVideoCodecs)
{
- if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container))
+ if (i.ContainsAnyCodec(transcodingVideoCodec, container))
{
- bool applyConditions = true;
- foreach (ProfileCondition applyCondition in i.ApplyConditions)
- {
- int? width = videoStream?.Width;
- int? height = videoStream?.Height;
- int? bitDepth = videoStream?.BitDepth;
- int? videoBitrate = videoStream?.BitRate;
- double? videoLevel = videoStream?.Level;
- string videoProfile = videoStream?.Profile;
- float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
- bool? isAnamorphic = videoStream?.IsAnamorphic;
- bool? isInterlaced = videoStream?.IsInterlaced;
- string videoCodecTag = videoStream?.CodecTag;
- bool? isAvc = videoStream?.IsAVC;
-
- TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
- int? packetLength = videoStream?.PacketLength;
- int? refFrames = videoStream?.RefFrames;
-
- int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
- int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
-
- if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
- {
- // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
- applyConditions = false;
- break;
- }
- }
-
- if (applyConditions)
- {
- var transcodingVideoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
- foreach (var transcodingVideoCodec in transcodingVideoCodecs)
- {
- if (i.ContainsAnyCodec(transcodingVideoCodec, transcodingProfile.Container))
- {
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
- }
- }
- }
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
+ isFirstAppliedCodecProfile = false;
+ continue;
}
}
+ }
- // Honor requested max channels
- playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+ // Honor requested max channels
+ playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
- int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
- playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
+ int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
+ playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
- isFirstAppliedCodecProfile = true;
- foreach (var i in options.Profile.CodecProfiles)
+ bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream);
+ int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate;
+ int? audioChannels = audioStream == null ? null : audioStream.Channels;
+ string audioProfile = audioStream == null ? null : audioStream.Profile;
+ int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate;
+ int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth;
+
+ var appliedAudioConditions = options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.Video &&
+ i.ContainsAnyCodec(audioCodec, container) &&
+ i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
+ isFirstAppliedCodecProfile = true;
+ foreach (var i in appliedAudioConditions)
+ {
+ var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
+ foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{
- if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container))
+ if (i.ContainsAnyCodec(transcodingAudioCodec, container))
{
- bool applyConditions = true;
- foreach (ProfileCondition applyCondition in i.ApplyConditions)
- {
- bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream);
- int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate;
- int? audioChannels = audioStream == null ? null : audioStream.Channels;
- string audioProfile = audioStream == null ? null : audioStream.Profile;
- int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate;
- int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth;
-
- if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))
- {
- // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
- applyConditions = false;
- break;
- }
- }
-
- if (applyConditions)
- {
- var transcodingAudioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec);
- foreach (var transcodingAudioCodec in transcodingAudioCodecs)
- {
- if (i.ContainsAnyCodec(transcodingAudioCodec, transcodingProfile.Container))
- {
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
- isFirstAppliedCodecProfile = false;
- }
- }
- }
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
+ isFirstAppliedCodecProfile = false;
+ break;
}
}
+ }
- var maxBitrateSetting = options.GetMaxBitrate(false);
- // Honor max rate
- if (maxBitrateSetting.HasValue)
- {
- var availableBitrateForVideo = maxBitrateSetting.Value;
-
- if (playlistItem.AudioBitrate.HasValue)
- {
- availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
- }
+ var maxBitrateSetting = options.GetMaxBitrate(false);
+ // Honor max rate
+ if (maxBitrateSetting.HasValue)
+ {
+ var availableBitrateForVideo = maxBitrateSetting.Value;
- // Make sure the video bitrate is lower than bitrate settings but at least 64k
- long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
- var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000);
- playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+ if (playlistItem.AudioBitrate.HasValue)
+ {
+ availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
}
- }
- playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+ // Make sure the video bitrate is lower than bitrate settings but at least 64k
+ var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
+ playlistItem.VideoBitrate = Math.Clamp(currentValue, 64_000, availableBitrateForVideo);
+ }
- return playlistItem;
+ _logger.LogInformation(
+ "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}",
+ options.Profile?.Name ?? "Anonymous Profile",
+ item.Path ?? "Unknown path",
+ playlistItem?.PlayMethod,
+ audioStream?.Index,
+ playlistItem?.SubtitleStreamIndex,
+ playlistItem?.TranscodeReasons);
}
private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
@@ -1000,63 +1049,30 @@ namespace MediaBrowser.Model.Dlna
return 7168000;
}
- private (PlayMethod? PlayMethod, List<TranscodeReason> TranscodeReasons) GetVideoDirectPlayProfile(
+ private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
VideoOptions options,
MediaSourceInfo mediaSource,
MediaStream videoStream,
MediaStream audioStream,
+ IEnumerable<MediaStream> candidateAudioStreams,
+ MediaStream subtitleStream,
+ bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
{
if (options.ForceDirectPlay)
{
- return (PlayMethod.DirectPlay, new List<TranscodeReason>());
+ return (null, PlayMethod.DirectPlay, audioStream?.Index, 0);
}
if (options.ForceDirectStream)
{
- return (PlayMethod.DirectStream, new List<TranscodeReason>());
+ return (null, PlayMethod.DirectStream, audioStream?.Index, 0);
}
DeviceProfile profile = options.Profile;
string container = mediaSource.Container;
- // See if it can be direct played
- DirectPlayProfile directPlay = null;
- foreach (var p in profile.DirectPlayProfiles)
- {
- if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream))
- {
- directPlay = p;
- break;
- }
- }
-
- if (directPlay == null)
- {
- _logger.LogDebug(
- "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}",
- container,
- videoStream?.Codec ?? "no video",
- audioStream?.Codec ?? "no audio",
- profile.Name ?? "unknown profile",
- mediaSource.Path ?? "unknown path");
-
- return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
- }
-
- var conditions = new List<ProfileCondition>();
- foreach (var p in profile.ContainerProfiles)
- {
- if (p.Type == DlnaProfileType.Video
- && p.ContainsContainer(container))
- {
- foreach (var c in p.Conditions)
- {
- conditions.Add(c);
- }
- }
- }
-
+ // video
int? width = videoStream?.Width;
int? height = videoStream?.Height;
int? bitDepth = videoStream?.BitDepth;
@@ -1068,12 +1084,9 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
-
- int? audioBitrate = audioStream?.BitRate;
- int? audioChannels = audioStream?.Channels;
- string audioProfile = audioStream?.Profile;
- int? audioSampleRate = audioStream?.SampleRate;
- int? audioBitDepth = audioStream?.BitDepth;
+ // audio
+ var defaultLanguage = audioStream?.Language ?? string.Empty;
+ var defaultMarked = audioStream?.IsDefault ?? false;
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -1082,118 +1095,165 @@ namespace MediaBrowser.Model.Dlna
int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
- // Check container conditions
- foreach (ProfileCondition i in conditions)
- {
- if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
- {
- LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource);
-
- var transcodeReason = GetTranscodeReasonForFailedCondition(i);
- var transcodeReasons = transcodeReason.HasValue
- ? new List<TranscodeReason> { transcodeReason.Value }
- : new List<TranscodeReason>();
-
- return (null, transcodeReasons);
- }
- }
-
- string videoCodec = videoStream?.Codec;
+ var checkVideoConditions = (ProfileCondition[] conditions) =>
+ conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
- conditions = new List<ProfileCondition>();
- foreach (var i in profile.CodecProfiles)
- {
- if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container))
- {
- bool applyConditions = true;
- foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ // Check container conditions
+ var containerProfileReasons = AggregateFailureConditions(
+ mediaSource,
+ profile,
+ "VideoCodecProfile",
+ profile.ContainerProfiles
+ .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container))
+ .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions)));
+
+ // Check video conditions
+ var videoCodecProfileReasons = AggregateFailureConditions(
+ mediaSource,
+ profile,
+ "VideoCodecProfile",
+ profile.CodecProfiles
+ .Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container))
+ .SelectMany(codecProfile =>
{
- if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ var failedApplyConditions = checkVideoConditions(codecProfile.ApplyConditions);
+ if (!failedApplyConditions.Any())
{
- // LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource);
- applyConditions = false;
- break;
+ return Array.Empty<ProfileCondition>();
}
- }
- if (applyConditions)
- {
- foreach (ProfileCondition c in i.Conditions)
- {
- conditions.Add(c);
- }
- }
- }
- }
+ var failedConditions = checkVideoConditions(codecProfile.Conditions);
+ return failedApplyConditions.Concat(failedConditions);
+ }));
- foreach (ProfileCondition i in conditions)
- {
- if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
- {
- LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource);
+ // Check audiocandidates profile conditions
+ var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked));
- var transcodeReason = GetTranscodeReasonForFailedCondition(i);
- var transcodeReasons = transcodeReason.HasValue
- ? new List<TranscodeReason> { transcodeReason.Value }
- : new List<TranscodeReason>();
+ TranscodeReason subtitleProfileReasons = 0;
+ if (subtitleStream != null)
+ {
+ var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.DirectPlay, _transcoderSupport, container, null);
- return (null, transcodeReasons);
+ if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
+ && subtitleProfile.Method != SubtitleDeliveryMethod.External
+ && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
+ {
+ _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay);
+ subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported;
}
}
- if (audioStream != null)
- {
- string audioCodec = audioStream.Codec;
- conditions = new List<ProfileCondition>();
- bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream);
-
- foreach (var i in profile.CodecProfiles)
+ var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons };
+ var rank = (ref TranscodeReason a) =>
{
- if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container))
+ var index = 1;
+ foreach (var flag in rankings)
{
- bool applyConditions = true;
- foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ var reason = a & flag;
+ if (reason != 0)
{
- if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))
- {
- // LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource);
- applyConditions = false;
- break;
- }
+ a = reason;
+ return index;
}
- if (applyConditions)
- {
- foreach (ProfileCondition c in i.Conditions)
- {
- conditions.Add(c);
- }
- }
+ index++;
}
- }
- foreach (ProfileCondition i in conditions)
+ return index;
+ };
+
+ // Check DirectPlay profiles to see if it can be direct played
+ var analyzedProfiles = profile.DirectPlayProfiles
+ .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
+ .Select((directPlayProfile, order) =>
{
- if (!ConditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))
+ TranscodeReason directPlayProfileReasons = 0;
+ TranscodeReason audioCodecProfileReasons = 0;
+
+ // Check container type
+ if (!directPlayProfile.SupportsContainer(container))
{
- LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource);
+ directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
+ }
- var transcodeReason = GetTranscodeReasonForFailedCondition(i);
- var transcodeReasons = transcodeReason.HasValue
- ? new List<TranscodeReason> { transcodeReason.Value }
- : new List<TranscodeReason>();
+ // Check video codec
+ string videoCodec = videoStream?.Codec;
+ if (!directPlayProfile.SupportsVideoCodec(videoCodec))
+ {
+ directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
+ }
- return (null, transcodeReasons);
+ // Check audio codec
+ var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
+ if (selectedAudioStream == null)
+ {
+ directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
}
- }
+ else
+ {
+ audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
+ }
+
+ var failureReasons = directPlayProfileReasons | containerProfileReasons | videoCodecProfileReasons | audioCodecProfileReasons | subtitleProfileReasons;
+ var directStreamFailureReasons = failureReasons & (~DirectStreamReasons);
+
+ PlayMethod? playMethod = null;
+ if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
+ {
+ playMethod = PlayMethod.DirectPlay;
+ }
+ else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile != null)
+ {
+ playMethod = PlayMethod.DirectStream;
+ }
+
+ var ranked = rank(ref failureReasons);
+ return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
+ })
+ .OrderByDescending(analysis => analysis.Result.PlayMethod)
+ .ThenBy(analysis => analysis.Order)
+ .ToArray()
+ .ToLookup(analysis => analysis.Result.PlayMethod != null);
+
+ var profileMatch = analyzedProfiles[true]
+ .Select(analysis => analysis.Result)
+ .FirstOrDefault();
+ if (profileMatch.Profile != null)
+ {
+ return profileMatch;
+ }
+
+ var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason;
+ if (failureReasons == 0)
+ {
+ failureReasons = TranscodeReason.DirectPlayError;
}
- if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
+ return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
+ }
+
+ private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault)
+ {
+ var profile = options.Profile;
+ var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault);
+
+ var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
+ if (audioStream?.IsExternal == true)
{
- return (PlayMethod.DirectStream, new List<TranscodeReason>());
+ audioStreamFailureReasons |= TranscodeReason.AudioIsExternal;
}
- return (null, new List<TranscodeReason> { TranscodeReason.ContainerBitrateExceedsLimit });
+ return audioStreamFailureReasons;
+ }
+
+ private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable<ProfileCondition> conditions)
+ {
+ return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) =>
+ {
+ LogConditionFailure(profile, type, i, mediaSource);
+ var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
+ return reasons | transcodeReasons;
+ });
}
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
@@ -1209,39 +1269,21 @@ namespace MediaBrowser.Model.Dlna
mediaSource.Path ?? "Unknown path");
}
- private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay(
+ private TranscodeReason IsEligibleForDirectPlay(
MediaSourceInfo item,
long maxBitrate,
- MediaStream subtitleStream,
- MediaStream audioStream,
VideoOptions options,
PlayMethod playMethod)
{
- if (subtitleStream != null)
- {
- var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null);
-
- if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
- && subtitleProfile.Method != SubtitleDeliveryMethod.External
- && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
- {
- _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
- return (false, TranscodeReason.SubtitleCodecNotSupported);
- }
- }
-
- bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
+ bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod);
if (!result)
{
- return (false, TranscodeReason.ContainerBitrateExceedsLimit);
+ return TranscodeReason.ContainerBitrateExceedsLimit;
}
-
- if (audioStream?.IsExternal == true)
+ else
{
- return (false, TranscodeReason.AudioIsExternal);
+ return 0;
}
-
- return (true, null);
}
public static SubtitleProfile GetSubtitleProfile(
@@ -1401,7 +1443,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+ private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
{
// Don't restrict by bitrate if coming from an external domain
if (item.IsRemote)
@@ -1465,6 +1507,47 @@ namespace MediaBrowser.Model.Dlna
}
}
+ private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
+ IEnumerable<CodecProfile> codecProfiles,
+ string container,
+ string codec,
+ int? audioChannels,
+ int? audioBitrate,
+ int? audioSampleRate,
+ int? audioBitDepth,
+ string audioProfile,
+ bool? isSecondaryAudio)
+ {
+ return codecProfiles
+ .Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) &&
+ profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)))
+ .SelectMany(profile => profile.Conditions)
+ .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
+ }
+
+ private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
+ IEnumerable<CodecProfile> codecProfiles,
+ string container,
+ string codec,
+ int? audioChannels,
+ int? audioBitrate,
+ int? audioSampleRate,
+ int? audioBitDepth,
+ bool checkConditions)
+ {
+ var conditions = codecProfiles
+ .Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) &&
+ profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)))
+ .SelectMany(profile => profile.Conditions);
+
+ if (!checkConditions)
+ {
+ return conditions;
+ }
+
+ return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth));
+ }
+
private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
{
foreach (ProfileCondition condition in conditions)
@@ -1744,10 +1827,22 @@ namespace MediaBrowser.Model.Dlna
var values = value
.Split('|', StringSplitOptions.RemoveEmptyEntries);
- if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
+ if (condition.Condition == ProfileConditionType.Equals)
{
item.SetOption(qualifier, "profile", string.Join(',', values));
}
+ else if (condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ var currentValue = item.GetOption(qualifier, "profile");
+ if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue))
+ {
+ item.SetOption(qualifier, "profile", currentValue);
+ }
+ else
+ {
+ item.SetOption(qualifier, "profile", string.Join(',', values));
+ }
+ }
break;
}
@@ -1905,29 +2000,5 @@ namespace MediaBrowser.Model.Dlna
return true;
}
-
- private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream)
- {
- // Check container type
- if (!profile.SupportsContainer(container))
- {
- return false;
- }
-
- // Check video codec
- string videoCodec = videoStream?.Codec;
- if (!profile.SupportsVideoCodec(videoCodec))
- {
- return false;
- }
-
- // Check audio codec
- if (audioStream != null && !profile.SupportsAudioCodec(audioStream.Codec))
- {
- return false;
- }
-
- return true;
- }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index a678c54e7..79dfff5c2 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -23,7 +23,6 @@ namespace MediaBrowser.Model.Dlna
AudioCodecs = Array.Empty<string>();
VideoCodecs = Array.Empty<string>();
SubtitleCodecs = Array.Empty<string>();
- TranscodeReasons = Array.Empty<TranscodeReason>();
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
@@ -103,7 +102,7 @@ namespace MediaBrowser.Model.Dlna
public string PlaySessionId { get; set; }
- public TranscodeReason[] TranscodeReasons { get; set; }
+ public TranscodeReason TranscodeReasons { get; set; }
public Dictionary<string, string> StreamOptions { get; private set; }
@@ -799,7 +798,7 @@ namespace MediaBrowser.Model.Dlna
if (!item.IsDirectStream)
{
- list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct())));
+ list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
}
return list;
diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs
index 4194f17c6..0cb80af54 100644
--- a/MediaBrowser.Model/Dlna/VideoOptions.cs
+++ b/MediaBrowser.Model/Dlna/VideoOptions.cs
@@ -10,5 +10,7 @@ namespace MediaBrowser.Model.Dlna
public int? AudioStreamIndex { get; set; }
public int? SubtitleStreamIndex { get; set; }
+
+ public bool AllowVideoStreamCopy { get; set; }
}
}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 049e14333..bb9848848 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -109,7 +109,7 @@ namespace MediaBrowser.Model.Dto
public int? AnalyzeDurationMs { get; set; }
[JsonIgnore]
- public TranscodeReason[] TranscodeReasons { get; set; }
+ public TranscodeReason TranscodeReasons { get; set; }
public int? DefaultAudioStreamIndex { get; set; }
@@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dto
public MediaStream GetDefaultAudioStream(int? defaultIndex)
{
- if (defaultIndex.HasValue)
+ if (defaultIndex.HasValue && defaultIndex != -1)
{
var val = defaultIndex.Value;
diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs
index e50baf604..6bf1eb0c0 100644
--- a/MediaBrowser.Model/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs
@@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
[assembly: InternalsVisibleTo("Jellyfin.Model.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index 3c95df66d..9da9f3323 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -1,32 +1,44 @@
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.Session
{
+ [Flags]
public enum TranscodeReason
{
- ContainerNotSupported = 0,
- VideoCodecNotSupported = 1,
- AudioCodecNotSupported = 2,
- ContainerBitrateExceedsLimit = 3,
- AudioBitrateNotSupported = 4,
- AudioChannelsNotSupported = 5,
- VideoResolutionNotSupported = 6,
- UnknownVideoStreamInfo = 7,
- UnknownAudioStreamInfo = 8,
- AudioProfileNotSupported = 9,
- AudioSampleRateNotSupported = 10,
- AnamorphicVideoNotSupported = 11,
- InterlacedVideoNotSupported = 12,
- SecondaryAudioNotSupported = 13,
- RefFramesNotSupported = 14,
- VideoBitDepthNotSupported = 15,
- VideoBitrateNotSupported = 16,
- VideoFramerateNotSupported = 17,
- VideoLevelNotSupported = 18,
- VideoProfileNotSupported = 19,
- AudioBitDepthNotSupported = 20,
- SubtitleCodecNotSupported = 21,
- DirectPlayError = 22,
- AudioIsExternal = 23
+ // Primary
+ ContainerNotSupported = 1 << 0,
+ VideoCodecNotSupported = 1 << 1,
+ AudioCodecNotSupported = 1 << 2,
+ SubtitleCodecNotSupported = 1 << 3,
+ AudioIsExternal = 1 << 4,
+ SecondaryAudioNotSupported = 1 << 5,
+
+ // Video Constraints
+ VideoProfileNotSupported = 1 << 6,
+ VideoLevelNotSupported = 1 << 7,
+ VideoResolutionNotSupported = 1 << 8,
+ VideoBitDepthNotSupported = 1 << 9,
+ VideoFramerateNotSupported = 1 << 10,
+ RefFramesNotSupported = 1 << 11,
+ AnamorphicVideoNotSupported = 1 << 12,
+ InterlacedVideoNotSupported = 1 << 13,
+
+ // Audio Constraints
+ AudioChannelsNotSupported = 1 << 14,
+ AudioProfileNotSupported = 1 << 15,
+ AudioSampleRateNotSupported = 1 << 16,
+ AudioBitDepthNotSupported = 1 << 17,
+
+ // Bitrate Constraints
+ ContainerBitrateExceedsLimit = 1 << 18,
+ VideoBitrateNotSupported = 1 << 19,
+ AudioBitrateNotSupported = 1 << 20,
+
+ // Errors
+ UnknownVideoStreamInfo = 1 << 21,
+ UnknownAudioStreamInfo = 1 << 22,
+ DirectPlayError = 1 << 23,
}
}
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index 68ab691f8..f876fa961 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -1,17 +1,10 @@
#nullable disable
#pragma warning disable CS1591
-using System;
-
namespace MediaBrowser.Model.Session
{
public class TranscodingInfo
{
- public TranscodingInfo()
- {
- TranscodeReasons = Array.Empty<TranscodeReason>();
- }
-
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
@@ -36,6 +29,6 @@ namespace MediaBrowser.Model.Session
public HardwareEncodingType? HardwareAccelerationType { get; set; }
- public TranscodeReason[] TranscodeReasons { get; set; }
+ public TranscodeReason TranscodeReason { get; set; }
}
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs
new file mode 100644
index 000000000..4fa91fa5e
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Enum flag to json array converter.
+/// </summary>
+/// <typeparam name="T">The type of enum.</typeparam>
+public class JsonFlagEnumConverter<T> : JsonConverter<T>
+ where T : struct, Enum
+{
+ private static readonly T[] _enumValues = Enum.GetValues<T>();
+
+ /// <inheritdoc />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+ foreach (var enumValue in _enumValues)
+ {
+ if (value.HasFlag(enumValue))
+ {
+ writer.WriteStringValue(enumValue.ToString());
+ }
+ }
+
+ writer.WriteEndArray();
+ }
+}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs
new file mode 100644
index 000000000..b74caf345
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Json flag enum converter factory.
+/// </summary>
+public class JsonFlagEnumConverterFactory : JsonConverterFactory
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsEnum && typeToConvert.IsDefined(typeof(FlagsAttribute));
+ }
+
+ /// <inheritdoc />
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonFlagEnumConverter<>).MakeGenericType(typeToConvert));
+ }
+}
diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
index 2cd89dc3b..97cbee971 100644
--- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs
+++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
@@ -36,6 +36,7 @@ namespace Jellyfin.Extensions.Json
new JsonGuidConverter(),
new JsonNullableGuidConverter(),
new JsonVersionConverter(),
+ new JsonFlagEnumConverterFactory(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 4918e2e82..8cead4bf2 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -14,6 +14,12 @@
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
+ <ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
diff --git a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs
new file mode 100644
index 000000000..c645ca9a6
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs
@@ -0,0 +1,466 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.MediaBrowser.Model.Tests
+{
+ public class StreamBuilderTests
+ {
+ [Theory]
+ // Chrome
+ [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // Firefox
+ [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // Safari
+ [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ // AndroidPixel
+ [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ // Yatse
+ [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // RokuSSPlus
+ [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ // JellyfinMediaPlayer
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ // Chrome-NoHLS
+ [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
+ [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
+ [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // TranscodeMedia
+ [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
+ [InlineData("TranscodeMedia", "mkv-av1-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
+ [InlineData("TranscodeMedia", "mkv-av1-vorbis-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")]
+ [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
+ [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
+ [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")]
+ // DirectMedia
+ [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
+ [InlineData("DirectMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
+ [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
+ [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
+ // LowBandwidth
+ [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ // Null
+ [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
+ public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
+ {
+ var options = await GetVideoOptions(deviceName, mediaSource);
+ BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+ }
+
+ [Theory]
+ // Chrome
+ [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 <BUG: this is direct played>
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // Firefox
+ [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // Safari
+ [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ // AndroidPixel
+ [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
+ // Yatse
+ [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
+ [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ // RokuSSPlus
+ [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+ // JellyfinMediaPlayer
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
+ public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
+ {
+ var options = await GetVideoOptions(deviceName, mediaSource);
+ options.AudioStreamIndex = 1;
+ options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1;
+
+ var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+ Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex);
+ Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
+ }
+
+ [Theory]
+ // Chrome
+ [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450
+ [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ // Firefox
+ [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
+ [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
+ // Yatse
+ [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ // RokuSSPlus
+ [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
+ public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
+ {
+ var options = await GetVideoOptions(deviceName, mediaSource);
+ var streamCount = options.MediaSources[0].MediaStreams.Count;
+ options.AudioStreamIndex = streamCount - 2;
+ options.SubtitleStreamIndex = streamCount - 1;
+
+ var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
+ Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex);
+ Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
+ }
+
+ private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
+ {
+ if (string.IsNullOrEmpty(transcodeProtocol))
+ {
+ transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "HLS.ts";
+ }
+
+ var builder = GetStreamBuilder();
+
+ var val = builder.BuildVideoItem(options);
+ Assert.NotNull(val);
+
+ if (playMethod != null)
+ {
+ Assert.Equal(playMethod, val.PlayMethod);
+ }
+
+ Assert.Equal(why, val.TranscodeReasons);
+
+ var audioStreamIndexInput = options.AudioStreamIndex;
+ var targetVideoStream = val.TargetVideoStream;
+ var targetAudioStream = val.TargetAudioStream;
+
+ var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId);
+ Assert.NotNull(mediaSource);
+ var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
+ var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
+ // TODO: check AudioStreamIndex vs options.AudioStreamIndex
+ var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
+
+ var uri = ParseUri(val);
+
+ if (playMethod == PlayMethod.DirectPlay)
+ {
+ // check expected container
+ var containers = ContainerProfile.SplitValue(mediaSource.Container);
+ // TODO: test transcode too
+ // Assert.Contains(uri.Extension, containers);
+
+ // check expected video codec (1)
+ Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
+ Assert.Single(val.TargetVideoCodec);
+
+ // check expected audio codecs (1)
+ Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
+ Assert.Single(val.TargetAudioCodec);
+ // Assert.Single(val.AudioCodecs);
+
+ if (transcodeMode == "DirectStream")
+ {
+ Assert.Equal(val.Container, uri.Extension);
+ }
+ }
+ else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
+ {
+ Assert.NotNull(val.Container);
+ Assert.NotEmpty(val.VideoCodecs);
+ Assert.NotEmpty(val.AudioCodecs);
+
+ // check expected container (todo: this could be a test param)
+ if (transcodeProtocol == "http")
+ {
+ // Assert.Equal("webm", val.Container);
+ Assert.Equal(val.Container, uri.Extension);
+ Assert.Equal("stream", uri.Filename);
+ Assert.Equal("http", val.SubProtocol);
+ }
+ else if (transcodeProtocol == "HLS.mp4")
+ {
+ Assert.Equal("mp4", val.Container);
+ Assert.Equal("m3u8", uri.Extension);
+ Assert.Equal("master", uri.Filename);
+ Assert.Equal("hls", val.SubProtocol);
+ }
+ else
+ {
+ Assert.Equal("ts", val.Container);
+ Assert.Equal("m3u8", uri.Extension);
+ Assert.Equal("master", uri.Filename);
+ Assert.Equal("hls", val.SubProtocol);
+ }
+
+ // Full transcode
+ if (transcodeMode == "Transcode")
+ {
+ if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
+ {
+ Assert.All(
+ videoStreams,
+ stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
+ }
+
+ // todo: fill out tests here
+ }
+
+ // DirectStream and Remux
+ else
+ {
+ // check expected video codec (1)
+ Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
+ Assert.Single(val.TargetVideoCodec);
+
+ if (transcodeMode == "DirectStream")
+ {
+ if (!targetAudioStream.IsExternal)
+ {
+ // check expected audio codecs (1)
+ Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+ }
+ }
+ else if (transcodeMode == "Remux")
+ {
+ // check expected audio codecs (1)
+ Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
+ Assert.Single(val.AudioCodecs);
+ }
+
+ // video details
+ var videoStream = targetVideoStream;
+ Assert.False(val.EstimateContentLength);
+ Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
+ Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
+ Assert.Equal(videoStream.Level, val.TargetVideoLevel);
+ Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
+ Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
+
+ // audio codec not supported
+ if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
+ {
+ // audio stream specified
+ if (options.AudioStreamIndex >= 0)
+ {
+ // TODO:fixme
+ if (!targetAudioStream.IsExternal)
+ {
+ Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+ }
+ }
+
+ // audio stream not specified
+ else
+ {
+ // TODO:fixme
+ Assert.All(audioStreams, stream =>
+ {
+ if (!stream.IsExternal)
+ {
+ Assert.DoesNotContain(stream.Codec, val.AudioCodecs);
+ }
+ });
+ }
+ }
+ }
+ }
+ else if (playMethod == null)
+ {
+ Assert.Null(val.SubProtocol);
+ Assert.Equal("stream", uri.Filename);
+
+ Assert.False(val.EstimateContentLength);
+ Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
+ }
+
+ return val;
+ }
+
+ private static async ValueTask<T> TestData<T>(string name)
+ {
+ var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json");
+ using (var stream = File.OpenRead(path))
+ {
+ var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options);
+ if (value != null)
+ {
+ return value;
+ }
+
+ throw new SerializationException("Invalid test data: " + name);
+ }
+ }
+
+ private StreamBuilder GetStreamBuilder()
+ {
+ var transcodeSupport = new Mock<ITranscoderSupport>();
+ var logger = new NullLogger<StreamBuilderTests>();
+
+ return new StreamBuilder(transcodeSupport.Object, logger);
+ }
+
+ private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources)
+ {
+ var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src))
+ .Select(val => val.Result)
+ .ToArray();
+ var mediaSourceId = mediaSources[0]?.Id;
+
+ var dp = await TestData<DeviceProfile>(deviceProfile);
+
+ return new VideoOptions()
+ {
+ ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"),
+ MediaSourceId = mediaSourceId,
+ MediaSources = mediaSources,
+ DeviceId = "test-deviceId",
+ Profile = dp,
+ AllowAudioStreamCopy = true,
+ AllowVideoStreamCopy = true,
+ };
+ }
+
+ private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val)
+ {
+ var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2);
+ var path = href[0];
+
+ var queryString = href.ElementAtOrDefault(1);
+ var query = string.IsNullOrEmpty(queryString) ? System.Web.HttpUtility.ParseQueryString(queryString ?? string.Empty) : new NameValueCollection();
+
+ var filename = System.IO.Path.GetFileNameWithoutExtension(path);
+ var extension = System.IO.Path.GetExtension(path);
+ if (extension.Length > 0)
+ {
+ extension = extension.Substring(1);
+ }
+
+ return (path, query, filename, extension);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json
new file mode 100644
index 000000000..68ce3ea4a
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json
@@ -0,0 +1,332 @@
+{
+ "Name": "Jellyfin Android",
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 2147483647,
+ "MaxAlbumArtHeight": 2147483647,
+ "MaxStreamingBitrate": 8000000,
+ "MaxStaticBitrate": 8000000,
+ "MusicStreamingTranscodingBitrate": 128000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "mp4",
+ "AudioCodec": "mp3,aac,alac,ac3",
+ "VideoCodec": "h263,mpeg4,h264,hevc,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp4",
+ "AudioCodec": "mp3,aac,alac,ac3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "fmp4",
+ "AudioCodec": "mp3,aac,ac3,eac3",
+ "VideoCodec": "h263,mpeg4,h264,hevc,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "fmp4",
+ "AudioCodec": "mp3,aac,ac3,eac3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis,opus",
+ "VideoCodec": "vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis,opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mkv",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd",
+ "VideoCodec": "h263,mpeg4,h264,hevc,av1,vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mkv",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3",
+ "AudioCodec": "mp3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ogg",
+ "AudioCodec": "vorbis,opus,flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "wav",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mpegts",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd",
+ "VideoCodec": "mpeg4,h264,hevc",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mpegts",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flv",
+ "AudioCodec": "mp3,aac",
+ "VideoCodec": "mpeg4,h264",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flv",
+ "AudioCodec": "mp3,aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flac",
+ "AudioCodec": "flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "3gp",
+ "AudioCodec": "3gpp,aac,flac",
+ "VideoCodec": "h263,mpeg4,h264,hevc",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "3gp",
+ "AudioCodec": "3gpp,aac,flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "mp1,mp2,mp3,aac,ac3,eac3,dts,mlp,truehd",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mkv",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp1,mp2,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "ContainerProfiles": [
+ {
+ "Type": "Video",
+ "Container": "mp4",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "mp4",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "fmp4",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "fmp4",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "webm",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "webm",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "mkv",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "mkv",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "mp3",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "ogg",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "wav",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "mpegts",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "mpegts",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "flv",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "flv",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "aac",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "flac",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Video",
+ "Container": "3gp",
+ "$type": "ContainerProfile"
+ },
+ {
+ "Type": "Audio",
+ "Container": "3gp",
+ "$type": "ContainerProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ttml",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ttml",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "webvtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json
new file mode 100644
index 000000000..5d1f5f162
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json
@@ -0,0 +1,430 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 384000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis,opus",
+ "VideoCodec": "vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp4,m4v",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "VideoCodec": "h264,vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mov",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "VideoCodec": "h264",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ogg",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "VideoCodec": "vp8,vp9,av1,vpx",
+ "AudioCodec": "vorbis,opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "aac",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline|high 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "52",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "120",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json
new file mode 100644
index 000000000..81bb97ac8
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json
@@ -0,0 +1,448 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 384000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis,opus",
+ "VideoCodec": "vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp4,m4v",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "VideoCodec": "h264,vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mov",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "VideoCodec": "h264",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ogg",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 2,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 2,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "VideoCodec": "vp8,vp9,av1,vpx",
+ "AudioCodec": "vorbis,opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "aac",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline|high 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "52",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "120",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json
new file mode 100644
index 000000000..d1df7341e
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json
@@ -0,0 +1,90 @@
+{
+ "Name": "Jellyfin Media Player",
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxStreamingBitrate": 20000000,
+ "MaxStaticBitrate": 20000000,
+ "MusicStreamingTranscodingBitrate": 1280000,
+ "TimelineOffsetSeconds": 5,
+ "DirectPlayProfiles": [
+ {
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json
new file mode 100644
index 000000000..9874793d3
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json
@@ -0,0 +1,441 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 384000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis,opus",
+ "VideoCodec": "vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp4,m4v",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "VideoCodec": "h264,vp8,vp9,av1",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ogg",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 2,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "opus",
+ "Type": "Audio",
+ "AudioCodec": "opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 2,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "VideoCodec": "vp8,vp9,av1,vpx",
+ "AudioCodec": "vorbis,opus",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3,opus,flac,alac,vorbis",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "aac",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "52",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "120",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json
new file mode 100644
index 000000000..da9a1a4ad
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json
@@ -0,0 +1,137 @@
+{
+ "Name": "Jellyfin Media Player",
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxStreamingBitrate": 8000000,
+ "MaxStaticBitrate": 8000000,
+ "MusicStreamingTranscodingBitrate": 1280000,
+ "TimelineOffsetSeconds": 5,
+ "DirectPlayProfiles": [
+ {
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Type": "Audio",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video",
+ "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "jpeg",
+ "Type": "Photo",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json
new file mode 100644
index 000000000..82b73fb0f
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json
@@ -0,0 +1,137 @@
+{
+ "Name": "Jellyfin Media Player",
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxStreamingBitrate": 120000,
+ "MaxStaticBitrate": 100000,
+ "MusicStreamingTranscodingBitrate": 3840,
+ "TimelineOffsetSeconds": 5,
+ "DirectPlayProfiles": [
+ {
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Type": "Audio",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video",
+ "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "jpeg",
+ "Type": "Photo",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json
new file mode 100644
index 000000000..d463bd896
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json
@@ -0,0 +1,9 @@
+{
+ "Name": "Jellyfin Media Player",
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxStreamingBitrate": 120000,
+ "MaxStaticBitrate": 100000,
+ "MusicStreamingTranscodingBitrate": 3840,
+ "TimelineOffsetSeconds": 5,
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json
new file mode 100644
index 000000000..37b923558
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json
@@ -0,0 +1,211 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 192000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "mp4,m4v,mov",
+ "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac",
+ "VideoCodec": "h264,h265,hevc,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mkv,webm",
+ "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis",
+ "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264,mpeg2video",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 1,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264,h265,hevc,mpeg2video",
+ "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "51",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ttml",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json
new file mode 100644
index 000000000..542bf6370
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json
@@ -0,0 +1,211 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 192000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "mp4,m4v,mov",
+ "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac",
+ "VideoCodec": "h264,h265,hevc,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mkv,webm",
+ "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis",
+ "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264,h265,hevc,mpeg2video",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": " 2",
+ "MinSegments": 1,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "VideoCodec": "h264,h265,hevc,mpeg2video",
+ "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "51",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ttml",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json
new file mode 100644
index 000000000..3b5a0c254
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json
@@ -0,0 +1,357 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 384000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "webm",
+ "AudioCodec": "vorbis",
+ "VideoCodec": "vp8,vp9",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp4,m4v",
+ "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
+ "VideoCodec": "h264,vp8,vp9",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mov",
+ "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
+ "VideoCodec": "h264",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "aac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "flac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4a",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "m4b",
+ "AudioCodec": "alac",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "webm",
+ "AudioCodec": "webma",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 2,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "aac",
+ "Type": "Audio",
+ "AudioCodec": "aac",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "wav",
+ "Type": "Audio",
+ "AudioCodec": "wav",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "AudioCodec": "aac,ac3,eac3,flac,alac",
+ "VideoCodec": "hevc,h264",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "AudioCodec": "aac,mp3,ac3,eac3",
+ "VideoCodec": "h264",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true
+ },
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "AudioCodec": "vorbis",
+ "VideoCodec": "vp8,vpx",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "2"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis",
+ "VideoCodec": "h264",
+ "Context": "Static",
+ "Protocol": "http"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "52",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "h264",
+ "$type": "CodecProfile"
+ },
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "NotEquals",
+ "Property": "IsAnamorphic",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "183",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "NotEquals",
+ "Property": "IsInterlaced",
+ "Value": "true",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json
new file mode 100644
index 000000000..9fc1ae6bb
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json
@@ -0,0 +1,139 @@
+{
+ "Name": "Jellyfin Media Player",
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxStreamingBitrate": 20000000,
+ "MaxStaticBitrate": 20000000,
+ "MusicStreamingTranscodingBitrate": 1280000,
+ "TimelineOffsetSeconds": 5,
+ "TranscodingProfiles": [
+ {
+ "Type": "Audio",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp4",
+ "Type": "Video",
+ "AudioCodec": "aac,flac,alac",
+ "VideoCodec": "hevc,h264",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "AudioCodec": "aac,mp3",
+ "VideoCodec": "h264",
+ "Context": "Streaming",
+ "Protocol": "hls",
+ "MaxAudioChannels": "2",
+ "MinSegments": "2",
+ "BreakOnNonKeyFrames": true,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "webm",
+ "Type": "Video",
+ "AudioCodec": "vorbis",
+ "VideoCodec": "vp9,vp8,vpx,av1",
+ "Context": "Streaming",
+ "Protocol": "http",
+ "MaxAudioChannels": "2",
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "jpeg",
+ "Type": "Photo",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json
new file mode 100644
index 000000000..256c8dc2f
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json
@@ -0,0 +1,189 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 192000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "",
+ "AudioCodec": "aac",
+ "VideoCodec": "",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg",
+ "AudioCodec": "",
+ "VideoCodec": "",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "",
+ "AudioCodec": "",
+ "VideoCodec": "",
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": true,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "VideoCodec": "",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "VideoCodec": "",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "",
+ "Container": "",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ },
+ {
+ "Container": "mov",
+ "Type": "Video",
+ "MimeType": "video/webm",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json
new file mode 100644
index 000000000..256c8dc2f
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json
@@ -0,0 +1,189 @@
+{
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 192000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "",
+ "AudioCodec": "aac",
+ "VideoCodec": "",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg",
+ "AudioCodec": "",
+ "VideoCodec": "",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "",
+ "AudioCodec": "",
+ "VideoCodec": "",
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": true,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "VideoCodec": "",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Static",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "VideoCodec": "",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MaxAudioChannels": "6",
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "BreakOnNonKeyFrames": false,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "VideoAudio",
+ "Conditions": [
+ {
+ "Condition": "Equals",
+ "Property": "IsSecondaryAudio",
+ "Value": "false",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "",
+ "Container": "",
+ "$type": "CodecProfile"
+ }
+ ],
+ "ResponseProfiles": [
+ {
+ "Container": "m4v",
+ "Type": "Video",
+ "MimeType": "video/mp4",
+ "$type": "ResponseProfile"
+ },
+ {
+ "Container": "mov",
+ "Type": "Video",
+ "MimeType": "video/webm",
+ "$type": "ResponseProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "vtt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "smi",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json
new file mode 100644
index 000000000..da185aacf
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json
@@ -0,0 +1,73 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "av1",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p AV1 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json
new file mode 100644
index 000000000..774dba32a
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json
@@ -0,0 +1,72 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "av1",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p AV1 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "vorbis",
+ "CodecTag": "ogg",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Vorbis - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json
new file mode 100644
index 000000000..0a85a1353
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json
@@ -0,0 +1,73 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "vp9",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p VP9 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Profile 0",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json
new file mode 100644
index 000000000..2b932ff52
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json
@@ -0,0 +1,72 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "vp9",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p VP9 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Profile 0",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json
new file mode 100644
index 000000000..56b04b789
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json
@@ -0,0 +1,73 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "vp9",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p VP9 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Profile 0",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "vorbis",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Vorbis - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "RequiredHttpHeaders": {},
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json
new file mode 100644
index 000000000..1ee7eade9
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json
@@ -0,0 +1,72 @@
+{
+ "Id": "a766d122b58e45d9492d17af66748bf5",
+ "Path": "/Media/MyVideo-720p.mkv",
+ "Container": "mkv,webm",
+ "Size": 835317696,
+ "Name": "MyVideo-1080p",
+ "ETag": "579a34c6d5dfb23f61539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "vp9",
+ "Language": "eng",
+ "ColorTransfer": "bt709",
+ "ColorPrimaries": "bt709",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p VP9 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Profile 0",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": -99
+ },
+ {
+ "Codec": "vorbis",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Vorbis - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "webvtt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json
new file mode 100644
index 000000000..21911843d
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json
@@ -0,0 +1,72 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json
new file mode 100644
index 000000000..77954a31a
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json
@@ -0,0 +1,72 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "webvtt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json
new file mode 100644
index 000000000..70bbb9d0d
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json
@@ -0,0 +1,87 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 3
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json
new file mode 100644
index 000000000..036e41f07
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json
@@ -0,0 +1,87 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 2,
+ "DefaultSubtitleStreamIndex": 3
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json
new file mode 100644
index 000000000..b81c4597f
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json
@@ -0,0 +1,89 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsExternal": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 3
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json
new file mode 100644
index 000000000..b71fd4a6a
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json
@@ -0,0 +1,71 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json
new file mode 100644
index 000000000..4c6409e7b
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json
@@ -0,0 +1,75 @@
+{
+ "Id": "f6eab7118618ab26e61e495a1853481a",
+ "Path": "/Media/MyVideo-WEBDL-2160p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 6521110016,
+ "Name": "MyVideo WEBDL-2160p",
+ "ETag": "a2fb84b618ba2467fe377543f879e9bf",
+ "RunTimeTicks": 34318510080,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "eng",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "TimeBase": "1/16000",
+ "VideoRange": "HDR",
+ "DisplayTitle": "4K HEVC HDR",
+ "BitRate": 14715079,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 2160,
+ "Width": 3840,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main 10",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p10le",
+ "Level": 150
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 15201382,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json
new file mode 100644
index 000000000..385bb7260
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json
@@ -0,0 +1,89 @@
+{
+ "Id": "f6eab7118618ab26e61e495a1853481a",
+ "Path": "/Media/MyVideo-WEBDL-2160p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 6521110016,
+ "Name": "MyVideo WEBDL-2160p",
+ "ETag": "a2fb84b618ba2467fe377543f879e9bf",
+ "RunTimeTicks": 34318510080,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "eng",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "TimeBase": "1/16000",
+ "VideoRange": "HDR",
+ "DisplayTitle": "4K HEVC HDR",
+ "BitRate": 14715079,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 2160,
+ "Width": 3840,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main 10",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p10le",
+ "Level": 150
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 15201382,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 3
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json
new file mode 100644
index 000000000..fd1950bde
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json
@@ -0,0 +1,91 @@
+{
+ "Id": "f6eab7118618ab26e61e495a1853481a",
+ "Path": "/Media/MyVideo-WEBDL-2160p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 6521110016,
+ "Name": "MyVideo WEBDL-2160p",
+ "ETag": "a2fb84b618ba2467fe377543f879e9bf",
+ "RunTimeTicks": 34318510080,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "eng",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "TimeBase": "1/16000",
+ "VideoRange": "HDR",
+ "DisplayTitle": "4K HEVC HDR",
+ "BitRate": 14715079,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 2160,
+ "Width": 3840,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main 10",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p10le",
+ "Level": 150
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "IsExternal": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203,
+ "Path": "/Media/MyVideo-WEBDL-2160p.eng.m4a"
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 15201382,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 3
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json
new file mode 100644
index 000000000..dde7c15ea
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json
@@ -0,0 +1,74 @@
+{
+ "Id": "f6eab7118618ab26e61e495a1853481a",
+ "Path": "/Media/MyVideo-WEBDL-2160p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 6521110016,
+ "Name": "MyVideo WEBDL-2160p",
+ "ETag": "a2fb84b618ba2467fe377543f879e9bf",
+ "RunTimeTicks": 34318510080,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hev1",
+ "Language": "eng",
+ "ColorSpace": "bt2020nc",
+ "ColorTransfer": "smpte2084",
+ "ColorPrimaries": "bt2020",
+ "TimeBase": "1/16000",
+ "VideoRange": "HDR",
+ "DisplayTitle": "4K HEVC HDR",
+ "BitRate": 14715079,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 2160,
+ "Width": 3840,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main 10",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p10le",
+ "Level": 150
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 15201382,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json
new file mode 100644
index 000000000..9ea55b805
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json
@@ -0,0 +1,102 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "ac3",
+ "CodecTag": "ac-3",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - Dolby Digital - 5.1 - Default",
+ "ChannelLayout": "5.1",
+ "BitRate": 384000,
+ "Channels": 6,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Index": 1,
+ "Score": 202
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 2,
+ "Score": 203
+ },
+ {
+ "Codec": "mov_text",
+ "CodecTag": "tx3g",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 4,
+ "Score": 6422,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true,
+ "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt"
+ }
+ ],
+ "Bitrate": 2590008,
+ "RequiredHttpHeaders": {},
+ "DefaultSubtitleStreamIndex": 1
+}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs
new file mode 100644
index 000000000..c8652b323
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs
@@ -0,0 +1,28 @@
+using System.Text.Json;
+using Jellyfin.Extensions.Json.Converters;
+using MediaBrowser.Model.Session;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests.Json.Converters;
+
+public class JsonFlagEnumTests
+{
+ private readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ Converters =
+ {
+ new JsonFlagEnumConverter<TranscodeReason>()
+ }
+ };
+
+ [Theory]
+ [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\"]")]
+ [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoBitDepthNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\",\"VideoBitDepthNotSupported\"]")]
+ [InlineData((TranscodeReason)0, "[]")]
+ public void Serialize_Transcode_Reason(TranscodeReason transcodeReason, string output)
+ {
+ var result = JsonSerializer.Serialize(transcodeReason, _jsonOptions);
+
+ Assert.Equal(output, result);
+ }
+}