diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 0a416aedbe..4c3ef2c7f7 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -98,7 +99,7 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, [FromQuery] string? transcodingContainer, - [FromQuery] string? transcodingProtocol, + [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] bool? enableRemoteMedia, @@ -156,7 +157,7 @@ public class UniversalAudioController : BaseJellyfinApiController } var isStatic = mediaSource.SupportsDirectStream; - if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.Hls) { // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // ffmpeg option -> file extension @@ -232,7 +233,7 @@ public class UniversalAudioController : BaseJellyfinApiController string[] containers, string? transcodingContainer, string? audioCodec, - string? transcodingProtocol, + MediaStreamProtocol? transcodingProtocol, bool? breakOnNonKeyFrames, int? transcodingAudioChannels, int? maxAudioSampleRate, @@ -267,7 +268,7 @@ public class UniversalAudioController : BaseJellyfinApiController Context = EncodingContext.Streaming, Container = transcodingContainer ?? "mp3", AudioCodec = audioCodec ?? "mp3", - Protocol = transcodingProtocol ?? "http", + Protocol = transcodingProtocol ?? MediaStreamProtocol.Http, BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } diff --git a/Jellyfin.Data/Enums/MediaStreamProtocol.cs b/Jellyfin.Data/Enums/MediaStreamProtocol.cs new file mode 100644 index 0000000000..965edd6c1d --- /dev/null +++ b/Jellyfin.Data/Enums/MediaStreamProtocol.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; + +namespace Jellyfin.Data.Enums; + +/// +/// Media streaming protocol. +/// +[DefaultValue(Http)] +public enum MediaStreamProtocol +{ + /// + /// HTTP. + /// + Http = 0, + + /// + /// HTTP Live Streaming. + /// + Hls = 1 +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index e6b7f4d9b3..7d9449b746 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -557,7 +557,7 @@ namespace MediaBrowser.Model.Dlna private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile) { var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); - var protocol = "http"; + var protocol = MediaStreamProtocol.Http; item.TranscodingContainer = container; item.TranscodingSubProtocol = protocol; @@ -648,7 +648,7 @@ namespace MediaBrowser.Model.Dlna if (directPlay == PlayMethod.DirectPlay) { - playlistItem.SubProtocol = "http"; + playlistItem.SubProtocol = MediaStreamProtocol.Http; var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index; if (audioStreamIndex.HasValue) @@ -803,7 +803,7 @@ namespace MediaBrowser.Model.Dlna var videoCodecs = ContainerProfile.SplitValue(videoCodec); // Enforce HLS video codec restrictions - if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + if (playlistItem.SubProtocol == MediaStreamProtocol.Hls) { videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray(); } @@ -840,7 +840,7 @@ namespace MediaBrowser.Model.Dlna var audioCodecs = ContainerProfile.SplitValue(audioCodec); // Enforce HLS audio codec restrictions - if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + if (playlistItem.SubProtocol == MediaStreamProtocol.Hls) { if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { @@ -1358,9 +1358,9 @@ namespace MediaBrowser.Model.Dlna PlayMethod playMethod, ITranscoderSupport transcoderSupport, string? outputContainer, - string? transcodingSubProtocol) + MediaStreamProtocol? transcodingSubProtocol) { - if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))) + if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.Hls)) { // Look for supported embedded subs of the same format foreach (var profile in subtitleProfiles) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index fc146df306..cd6d34be2e 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Model.Dlna public string? Container { get; set; } - public string? SubProtocol { get; set; } + public MediaStreamProtocol SubProtocol { get; set; } public long StartPositionTicks { get; set; } @@ -670,7 +670,7 @@ namespace MediaBrowser.Model.Dlna if (MediaType == DlnaProfileType.Audio) { - if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + if (SubProtocol == MediaStreamProtocol.Hls) { return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } @@ -678,7 +678,7 @@ namespace MediaBrowser.Model.Dlna return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + if (SubProtocol == MediaStreamProtocol.Hls) { return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } @@ -716,9 +716,7 @@ namespace MediaBrowser.Model.Dlna long startPositionTicks = item.StartPositionTicks; - var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase); - - if (isHls) + if (item.SubProtocol == MediaStreamProtocol.Hls) { list.Add(new NameValuePair("StartTimeTicks", string.Empty)); } @@ -780,7 +778,7 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - if (isHls) + if (item.SubProtocol == MediaStreamProtocol.Hls) { list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); @@ -831,7 +829,7 @@ namespace MediaBrowser.Model.Dlna var list = new List(); // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase) + long startPositionTicks = SubProtocol == MediaStreamProtocol.Hls ? 0 : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index b4f6ec255b..8f4f3e2f86 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; using System.Xml.Serialization; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Dlna { @@ -26,7 +27,7 @@ namespace MediaBrowser.Model.Dlna public string AudioCodec { get; set; } = string.Empty; [XmlAttribute("protocol")] - public string Protocol { get; set; } = string.Empty; + public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.Http; [DefaultValue(false)] [XmlAttribute("estimateContentLength")] diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 520832aeee..b7236b1e85 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; @@ -102,7 +104,7 @@ namespace MediaBrowser.Model.Dto public string TranscodingUrl { get; set; } - public string TranscodingSubProtocol { get; set; } + public MediaStreamProtocol TranscodingSubProtocol { get; set; } public string TranscodingContainer { get; set; } diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs index f35880a046..7e9befa8c6 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs @@ -1,6 +1,7 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json; namespace MediaBrowser.Providers.Plugins.Omdb { @@ -12,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb /// public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) + if (reader.IsNull()) { return null; } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs new file mode 100644 index 0000000000..06ecfc5580 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// +/// Json unknown enum converter. +/// +/// The type of enum. +public class JsonDefaultStringEnumConverter : JsonConverter + where T : struct, Enum +{ + private readonly JsonConverter _baseConverter; + + /// + /// Initializes a new instance of the class. + /// + /// The base json converter. + public JsonDefaultStringEnumConverter(JsonConverter baseConverter) + { + _baseConverter = baseConverter; + } + + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.IsNull() || reader.IsEmptyString()) + { + var customValueAttribute = typeToConvert.GetCustomAttribute(); + if (customValueAttribute?.Value is null) + { + throw new InvalidOperationException($"Default value not set for '{typeToConvert.Name}'"); + } + + return (T)customValueAttribute.Value; + } + + return _baseConverter.Read(ref reader, typeToConvert, options); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + _baseConverter.Write(writer, value, options); + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs new file mode 100644 index 0000000000..5a9bf546e5 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// +/// Utilizes the JsonStringEnumConverter and sets a default value if not provided. +/// +public class JsonDefaultStringEnumConverterFactory : JsonConverterFactory +{ + private static readonly JsonStringEnumConverter _baseConverterFactory = new(); + + /// + public override bool CanConvert(Type typeToConvert) + { + return _baseConverterFactory.CanConvert(typeToConvert) + && typeToConvert.IsDefined(typeof(DefaultValueAttribute)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var baseConverter = _baseConverterFactory.CreateConverter(typeToConvert, options); + var converterType = typeof(JsonDefaultStringEnumConverter<>).MakeGenericType(typeToConvert); + + return (JsonConverter?)Activator.CreateInstance(converterType, baseConverter); + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs index ea6d141cb2..2964c69430 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Extensions.Json.Converters { /// public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.TokenType == JsonTokenType.Null + => reader.IsNull() ? Guid.Empty : ReadInternal(ref reader); diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs index 28437023fe..94004fa49a 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs @@ -15,10 +15,7 @@ namespace Jellyfin.Extensions.Json.Converters /// public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - // Token is empty string. - if (reader.TokenType == JsonTokenType.String - && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) - || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty))) + if (reader.IsEmptyString()) { return null; } diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 9e6d4c3f87..cbe5849ec1 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -38,6 +38,7 @@ namespace Jellyfin.Extensions.Json new JsonNullableGuidConverter(), new JsonVersionConverter(), new JsonFlagEnumConverterFactory(), + new JsonDefaultStringEnumConverterFactory(), new JsonStringEnumConverter(), new JsonNullableStructConverterFactory(), new JsonDateTimeConverter(), diff --git a/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs b/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs new file mode 100644 index 0000000000..d06508a26e --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace Jellyfin.Extensions.Json; + +/// +/// Extensions for Utf8JsonReader and Utf8JsonWriter. +/// +public static class Utf8JsonExtensions +{ + /// + /// Determines if the reader contains an empty string. + /// + /// The reader. + /// Whether the reader contains an empty string. + public static bool IsEmptyString(this Utf8JsonReader reader) + => reader.TokenType == JsonTokenType.String + && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) + || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty)); + + /// + /// Determines if the reader contains a null value. + /// + /// The reader. + /// Whether the reader contains null. + public static bool IsNull(this Utf8JsonReader reader) + => reader.TokenType == JsonTokenType.Null; +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs new file mode 100644 index 0000000000..4fd9fd290e --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs @@ -0,0 +1,112 @@ +using System.Text.Json; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters; + +public class JsonDefaultStringEnumConverterTests +{ + private readonly JsonSerializerOptions _jsonOptions = new() { Converters = { new JsonDefaultStringEnumConverterFactory() } }; + + /// + /// Test to ensure that `null` and empty string are deserialized to the default value. + /// + /// The input string. + /// The expected enum value. + [Theory] + [InlineData("\"\"", MediaStreamProtocol.Http)] + [InlineData("\"Http\"", MediaStreamProtocol.Http)] + [InlineData("\"Hls\"", MediaStreamProtocol.Hls)] + public void Deserialize_Enum_Direct(string input, MediaStreamProtocol output) + { + var value = JsonSerializer.Deserialize(input, _jsonOptions); + Assert.Equal(output, value); + } + + /// + /// Test to ensure that `null` and empty string are deserialized to the default value. + /// + /// The input string. + /// The expected enum value. + [Theory] + [InlineData(null, MediaStreamProtocol.Http)] + [InlineData("\"\"", MediaStreamProtocol.Http)] + [InlineData("\"Http\"", MediaStreamProtocol.Http)] + [InlineData("\"Hls\"", MediaStreamProtocol.Hls)] + public void Deserialize_Enum(string? input, MediaStreamProtocol output) + { + input ??= "null"; + var json = $"{{ \"EnumValue\": {input} }}"; + var value = JsonSerializer.Deserialize(json, _jsonOptions); + Assert.NotNull(value); + Assert.Equal(output, value.EnumValue); + } + + /// + /// Test to ensure that empty string is deserialized to the default value, + /// and `null` is deserialized to `null`. + /// + /// The input string. + /// The expected enum value. + [Theory] + [InlineData(null, null)] + [InlineData("\"\"", MediaStreamProtocol.Http)] + [InlineData("\"Http\"", MediaStreamProtocol.Http)] + [InlineData("\"Hls\"", MediaStreamProtocol.Hls)] + public void Deserialize_Enum_Nullable(string? input, MediaStreamProtocol? output) + { + input ??= "null"; + var json = $"{{ \"EnumValue\": {input} }}"; + var value = JsonSerializer.Deserialize(json, _jsonOptions); + Assert.NotNull(value); + Assert.Equal(output, value.EnumValue); + } + + /// + /// Ensures that the roundtrip serialization & deserialization is successful. + /// + /// Input enum. + /// Output enum. + [Theory] + [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)] + [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)] + public void Enum_RoundTrip(MediaStreamProtocol input, MediaStreamProtocol output) + { + var inputObj = new TestClass { EnumValue = input }; + + var outputObj = JsonSerializer.Deserialize(JsonSerializer.Serialize(inputObj, _jsonOptions), _jsonOptions); + + Assert.NotNull(outputObj); + Assert.Equal(output, outputObj.EnumValue); + } + + /// + /// Ensures that the roundtrip serialization & deserialization is successful, including null. + /// + /// Input enum. + /// Output enum. + [Theory] + [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)] + [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)] + [InlineData(null, null)] + public void Enum_RoundTrip_Nullable(MediaStreamProtocol? input, MediaStreamProtocol? output) + { + var inputObj = new NullTestClass { EnumValue = input }; + + var outputObj = JsonSerializer.Deserialize(JsonSerializer.Serialize(inputObj, _jsonOptions), _jsonOptions); + + Assert.NotNull(outputObj); + Assert.Equal(output, outputObj.EnumValue); + } + + private sealed class TestClass + { + public MediaStreamProtocol EnumValue { get; set; } + } + + private sealed class NullTestClass + { + public MediaStreamProtocol? EnumValue { get; set; } + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 909de8f72f..183997fdbf 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.Serialization; using System.Text.Json; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -388,21 +389,21 @@ namespace Jellyfin.Model.Tests // Assert.Equal("webm", val.Container); Assert.Equal(streamInfo.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - Assert.Equal("http", streamInfo.SubProtocol); + Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol); } else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) { Assert.Equal("mp4", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", streamInfo.SubProtocol); + Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol); } else { Assert.Equal("ts", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", streamInfo.SubProtocol); + Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol); } // Full transcode @@ -488,7 +489,7 @@ namespace Jellyfin.Model.Tests } else if (playMethod is null) { - Assert.Null(streamInfo.SubProtocol); + Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol); Assert.Equal("stream", uri.Filename); Assert.False(streamInfo.EstimateContentLength);