aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-05-10 20:32:40 +0200
committerGitHub <noreply@github.com>2026-05-10 20:32:40 +0200
commit4f238ca9b3f57bf54dfa332d8b6f4d456651b907 (patch)
tree9de7feaf2949501f68eb92504daa1a1e7b6d2f1a
parent42870986a8a9eb5649a1365c2510813a00a1da1a (diff)
parent6d6dee9492c2c31bc53c250a72650cd88a300ec3 (diff)
Merge pull request #16803 from nyanmisaka/new-profile-condition-video-rotation
Add videoRotation profile condition
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs11
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs56
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs3
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs43
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs1
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json162
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json56
10 files changed, 308 insertions, 39 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 10f2f04af6..34826982af 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -92,6 +92,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string CodecTag { get; set; }
/// <summary>
+ /// Gets or sets the rotation.
+ /// </summary>
+ /// <value>The video rotation angle, usually 0 or +-90/180.</value>
+ public string Rotation { get; set; }
+
+ /// <summary>
/// Gets or sets the framerate.
/// </summary>
/// <value>The framerate.</value>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index a0e04eae63..04b13a6f3c 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2466,6 +2466,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ var requestedRotations = state.GetRequestedRotations(videoStream.Codec);
+ if (requestedRotations.Length > 0)
+ {
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ if (rotation != 0
+ && !requestedRotations.Contains(rotation.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
// Video width must fall within requested value
if (request.MaxWidth.HasValue
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 7d0384ef27..3a1897a244 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -571,62 +571,50 @@ namespace MediaBrowser.Controller.MediaEncoding
public string[] GetRequestedProfiles(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.Profile))
- {
- return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var profile = BaseRequest.Profile;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(profile) && !string.IsNullOrEmpty(codec))
{
- var profile = BaseRequest.GetOption(codec, "profile");
-
- if (!string.IsNullOrEmpty(profile))
- {
- return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ profile = BaseRequest.GetOption(codec, "profile");
}
- return Array.Empty<string>();
+ return (profile ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedRangeTypes(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
- {
- return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var rangetype = BaseRequest.VideoRangeType;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(rangetype) && !string.IsNullOrEmpty(codec))
{
- var rangetype = BaseRequest.GetOption(codec, "rangetype");
-
- if (!string.IsNullOrEmpty(rangetype))
- {
- return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ rangetype = BaseRequest.GetOption(codec, "rangetype");
}
- return Array.Empty<string>();
+ return (rangetype ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedCodecTags(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
+ var codectag = BaseRequest.CodecTag;
+
+ if (string.IsNullOrEmpty(codectag) && !string.IsNullOrEmpty(codec))
{
- return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ codectag = BaseRequest.GetOption(codec, "codectag");
}
- if (!string.IsNullOrEmpty(codec))
- {
- var codectag = BaseRequest.GetOption(codec, "codectag");
+ return (codectag ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ }
- if (!string.IsNullOrEmpty(codectag))
- {
- return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ public string[] GetRequestedRotations(string codec)
+ {
+ var rotation = BaseRequest.Rotation;
+
+ if (string.IsNullOrEmpty(rotation) && !string.IsNullOrEmpty(codec))
+ {
+ rotation = BaseRequest.GetOption(codec, "rotation");
}
- return Array.Empty<string>();
+ return (rotation ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string GetRequestedLevel(string codec)
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 79ee683a2d..a6018f369d 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -33,6 +33,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="numAudioStreams">The number of audio streams.</param>
/// <param name="videoCodecTag">The video codec tag.</param>
/// <param name="isAvc">A value indicating whether the video is AVC.</param>
+ /// <param name="videoRotation">The video rotation angle, usually 0 or +-90/180.</param>
/// <returns><b>True</b> if the condition is satisfied.</returns>
public static bool IsVideoConditionSatisfied(
ProfileCondition condition,
@@ -53,7 +54,8 @@ namespace MediaBrowser.Model.Dlna
int? numVideoStreams,
int? numAudioStreams,
string? videoCodecTag,
- bool? isAvc)
+ bool? isAvc,
+ int? videoRotation)
{
switch (condition.Property)
{
@@ -93,6 +95,8 @@ namespace MediaBrowser.Model.Dlna
return IsConditionSatisfied(condition, numVideoStreams);
case ProfileConditionValue.VideoTimestamp:
return IsConditionSatisfied(condition, timestamp);
+ case ProfileConditionValue.VideoRotation:
+ return IsConditionSatisfied(condition, videoRotation);
default:
return true;
}
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
index b66a15840b..c6171c7ab2 100644
--- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
+++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Model.Dlna
AudioSampleRate = 22,
AudioBitDepth = 23,
VideoRangeType = 24,
- NumStreams = 25
+ NumStreams = 25,
+ VideoRotation = 26
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index c9697c685c..44697837ca 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Dlna
internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit;
internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal;
internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons;
- internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported;
+ internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported | TranscodeReason.VideoRotationNotSupported;
internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons;
internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported;
@@ -380,6 +380,9 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.VideoRangeType:
return TranscodeReason.VideoRangeTypeNotSupported;
+ case ProfileConditionValue.VideoRotation:
+ return TranscodeReason.VideoRotationNotSupported;
+
case ProfileConditionValue.VideoTimestamp:
// TODO
return 0;
@@ -1040,6 +1043,7 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
+ int? videoRotation = videoStream?.Rotation;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -1054,7 +1058,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc, videoRotation)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var condition in appliedVideoConditions)
@@ -2059,6 +2063,38 @@ namespace MediaBrowser.Model.Dlna
break;
}
+ case ProfileConditionValue.VideoRotation:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ continue;
+ }
+
+ // change from split by | to comma
+ // strip spaces to avoid having to encode
+ var values = value
+ .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "rotation", string.Join(',', values));
+ }
+ else if (condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ var currentValue = item.GetOption(qualifier, "rotation");
+ if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue, StringComparison.OrdinalIgnoreCase)))
+ {
+ item.SetOption(qualifier, "rotation", currentValue);
+ }
+ else
+ {
+ item.SetOption(qualifier, "rotation", string.Join(',', values));
+ }
+ }
+
+ break;
+ }
+
case ProfileConditionValue.Height:
{
if (!enableNonQualifiedConditions)
@@ -2281,6 +2317,7 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
+ int? videoRotation = videoStream?.Rotation;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -2290,7 +2327,7 @@ namespace MediaBrowser.Model.Dlna
int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
- return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
+ return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc, videoRotation));
}
/// <summary>
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index 902bab9a6e..4ea60f115a 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Session
VideoResolutionNotSupported = 1 << 8,
VideoBitDepthNotSupported = 1 << 9,
VideoFramerateNotSupported = 1 << 10,
+ VideoRotationNotSupported = 1 << 27,
RefFramesNotSupported = 1 << 11,
AnamorphicVideoNotSupported = 1 << 12,
InterlacedVideoNotSupported = 1 << 13,
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 8269ae58cd..0b103debad 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -171,6 +171,9 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
+ [InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-4000k-r180", PlayMethod.DirectPlay)] // #13712
+ // AndroidTV NoHevcRotation
+ [InlineData("AndroidTVExoPlayer-NoHevcRotation", "mp4-hevc-aac-4000k-r180", PlayMethod.Transcode, TranscodeReason.VideoRotationNotSupported, "Transcode")] // #13712
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json
new file mode 100644
index 0000000000..341638bc52
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json
@@ -0,0 +1,162 @@
+{
+ "Name": "Jellyfin AndroidTV-ExoPlayer",
+ "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": "m4v,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,mp4,webm",
+ "AudioCodec": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw",
+ "VideoCodec": "h264,hevc,vp8,vp9,mpeg,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw,,pa,flac,wav,wma,ogg,oga,webma,ape,opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "jpg,jpeg,png,gif,web",
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "51",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "Equals",
+ "Property": "VideoRotation",
+ "Value": "0",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "$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,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "vtt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "idx",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json
new file mode 100644
index 0000000000..393b10171d
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json
@@ -0,0 +1,56 @@
+{
+ "Id": "b7a9e2d4c815f36b0d9241a7e58c3f42",
+ "Path": "/Media/MyVideo-1080p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 1421636271,
+ "Name": "MyVideo-1080p",
+ "ETag": "d8e2a1b5c4f907e8a1d2b3c4e5f6a7b8",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hvc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p HEVC SDR",
+ "NalLengthSize": "0",
+ "BitRate": 4014613,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 1080,
+ "Width": 1920,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 50,
+ "Rotation": 180
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 125427,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ }
+ ],
+ "Bitrate": 4331578,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}