Reduce allocations, simplifed code, faster implementation, included tests - StreamInfo.ToUrl (#9369)
* Rework PR 6168 * Fix testpull/13793/head
parent
cb931e0062
commit
9657708b38
@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace Jellyfin.Model.Tests.Dlna;
|
||||
|
||||
public class LegacyStreamInfo : StreamInfo
|
||||
{
|
||||
public LegacyStreamInfo(Guid itemId, DlnaProfileType mediaType)
|
||||
{
|
||||
ItemId = itemId;
|
||||
MediaType = mediaType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The 10.6 ToUrl code from StreamInfo.cs with which to compare new version.
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">The base url to use.</param>
|
||||
/// <param name="accessToken">The Access token.</param>
|
||||
/// <returns>A url.</returns>
|
||||
public string ToUrl_Original(string baseUrl, string? accessToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(baseUrl);
|
||||
|
||||
var list = new List<string>();
|
||||
foreach (NameValuePair pair in BuildParams(this, accessToken))
|
||||
{
|
||||
if (string.IsNullOrEmpty(pair.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to keep the url clean by omitting defaults
|
||||
if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
|
||||
|
||||
list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
|
||||
}
|
||||
|
||||
string queryString = string.Join('&', list);
|
||||
|
||||
return GetUrl(baseUrl, queryString);
|
||||
}
|
||||
|
||||
private string GetUrl(string baseUrl, string queryString)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(baseUrl);
|
||||
|
||||
string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
|
||||
|
||||
baseUrl = baseUrl.TrimEnd('/');
|
||||
|
||||
if (MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
|
||||
}
|
||||
|
||||
if (SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
|
||||
}
|
||||
|
||||
private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
|
||||
{
|
||||
var list = new List<NameValuePair>();
|
||||
|
||||
string audioCodecs = item.AudioCodecs.Count == 0 ?
|
||||
string.Empty :
|
||||
string.Join(',', item.AudioCodecs);
|
||||
|
||||
string videoCodecs = item.VideoCodecs.Count == 0 ?
|
||||
string.Empty :
|
||||
string.Join(',', item.VideoCodecs);
|
||||
|
||||
list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
|
||||
list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
|
||||
list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
|
||||
list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
list.Add(new NameValuePair("VideoCodec", videoCodecs));
|
||||
list.Add(new NameValuePair("AudioCodec", audioCodecs));
|
||||
list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
|
||||
list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
|
||||
long startPositionTicks = item.StartPositionTicks;
|
||||
|
||||
if (item.SubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
list.Add(new NameValuePair("StartTimeTicks", string.Empty));
|
||||
list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
|
||||
|
||||
if (item.SegmentLength.HasValue)
|
||||
{
|
||||
list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (item.MinSegments.HasValue)
|
||||
{
|
||||
list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
|
||||
list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty));
|
||||
|
||||
string? liveStreamId = item.MediaSource?.LiveStreamId;
|
||||
list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
|
||||
|
||||
if (!item.IsDirectStream)
|
||||
{
|
||||
if (item.RequireNonAnamorphic)
|
||||
{
|
||||
list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
|
||||
|
||||
if (item.EnableSubtitlesInManifest)
|
||||
{
|
||||
list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.EnableMpegtsM2TsMode)
|
||||
{
|
||||
list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.EstimateContentLength)
|
||||
{
|
||||
list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
|
||||
{
|
||||
list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.CopyTimestamps)
|
||||
{
|
||||
list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.RequireAvc)
|
||||
{
|
||||
list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (item.EnableAudioVbrEncoding)
|
||||
{
|
||||
list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
|
||||
|
||||
string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
|
||||
string.Empty :
|
||||
string.Join(",", item.SubtitleCodecs);
|
||||
|
||||
list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
|
||||
list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
|
||||
|
||||
foreach (var pair in item.StreamOptions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pair.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// strip spaces to avoid having to encode h264 profile names
|
||||
list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
var transcodeReasonsValues = item.TranscodeReasons.GetUniqueFlags().ToArray();
|
||||
if (!item.IsDirectStream && transcodeReasonsValues.Length > 0)
|
||||
{
|
||||
list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Model.Tests.Dlna;
|
||||
|
||||
public class StreamInfoTests
|
||||
{
|
||||
private const string BaseUrl = "/test/";
|
||||
private const int RandomSeed = 298347823;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random float.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <returns>A random <see cref="float"/>.</returns>
|
||||
private static float RandomFloat(Random random)
|
||||
{
|
||||
var buffer = new byte[4];
|
||||
random.NextBytes(buffer);
|
||||
return BitConverter.ToSingle(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random array.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <param name="elementType">The element <see cref="Type"/> of the array.</param>
|
||||
/// <returns>An <see cref="Array"/> of <see cref="Type"/>.</returns>
|
||||
private static object? RandomArray(Random random, Type? elementType)
|
||||
{
|
||||
if (elementType == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (elementType == typeof(string))
|
||||
{
|
||||
return RandomStringArray(random);
|
||||
}
|
||||
|
||||
if (elementType == typeof(int))
|
||||
{
|
||||
return RandomIntArray(random);
|
||||
}
|
||||
|
||||
if (elementType.IsEnum)
|
||||
{
|
||||
var values = Enum.GetValues(elementType);
|
||||
return RandomIntArray(random, 0, values.Length - 1);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported array type " + elementType.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random length string.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <param name="minLength">The minimum length of the string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <returns>The string.</returns>
|
||||
private static string RandomString(Random random, int minLength = 0, int maxLength = 256)
|
||||
{
|
||||
var len = random.Next(minLength, maxLength);
|
||||
var sb = new StringBuilder(len);
|
||||
|
||||
while (len > 0)
|
||||
{
|
||||
sb.Append((char)random.Next(65, 97));
|
||||
len--;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random long.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <param name="min">Min value.</param>
|
||||
/// <param name="max">Max value.</param>
|
||||
/// <returns>A random <see cref="long"/> between <paramref name="min"/> and <paramref name="max"/>.</returns>
|
||||
private static long RandomLong(Random random, long min = -9223372036854775808, long max = 9223372036854775807)
|
||||
{
|
||||
long result = random.Next((int)(min >> 32), (int)(max >> 32));
|
||||
result <<= 32;
|
||||
result |= (long)random.Next((int)(min >> 32) << 32, (int)(max >> 32) << 32);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random string array containing between <paramref name="minLength"/> and <paramref name="maxLength"/>.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <param name="minLength">The minimum number of elements.</param>
|
||||
/// <param name="maxLength">The maximum number of elements.</param>
|
||||
/// <returns>A random <see cref="string[]"/> instance.</returns>
|
||||
private static string[] RandomStringArray(Random random, int minLength = 0, int maxLength = 9)
|
||||
{
|
||||
var len = random.Next(minLength, maxLength);
|
||||
var arr = new List<string>(len);
|
||||
while (len > 0)
|
||||
{
|
||||
arr.Add(RandomString(random, 1, 30));
|
||||
len--;
|
||||
}
|
||||
|
||||
return arr.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random int array containing between <paramref name="minLength"/> and <paramref name="maxLength"/>.
|
||||
/// </summary>
|
||||
/// <param name="random">The <see cref="Random"/> instance.</param>
|
||||
/// <param name="minLength">The minimum number of elements.</param>
|
||||
/// <param name="maxLength">The maximum number of elements.</param>
|
||||
/// <returns>A random <see cref="int[]"/> instance.</returns>
|
||||
private static int[] RandomIntArray(Random random, int minLength = 0, int maxLength = 9)
|
||||
{
|
||||
var len = random.Next(minLength, maxLength);
|
||||
var arr = new List<int>(len);
|
||||
while (len > 0)
|
||||
{
|
||||
arr.Add(random.Next());
|
||||
len--;
|
||||
}
|
||||
|
||||
return arr.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills most properties with random data.
|
||||
/// </summary>
|
||||
/// <param name="destination">The instance to fill with data.</param>
|
||||
private static void FillAllProperties<T>(T destination)
|
||||
{
|
||||
var random = new Random(RandomSeed);
|
||||
var objectType = destination!.GetType();
|
||||
foreach (var property in objectType.GetProperties())
|
||||
{
|
||||
if (!(property.CanRead && property.CanWrite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var type = property.PropertyType;
|
||||
// If nullable, then set it to null, 25% of the time.
|
||||
if (Nullable.GetUnderlyingType(type) != null)
|
||||
{
|
||||
if (random.Next(0, 4) == 0)
|
||||
{
|
||||
// Set it to null.
|
||||
property.SetValue(destination, null);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == typeof(Guid))
|
||||
{
|
||||
property.SetValue(destination, Guid.NewGuid());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
Array values = Enum.GetValues(property.PropertyType);
|
||||
property.SetValue(destination, values.GetValue(random.Next(0, values.Length - 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == typeof(long))
|
||||
{
|
||||
property.SetValue(destination, RandomLong(random));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
property.SetValue(destination, RandomString(random));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
property.SetValue(destination, random.Next(0, 1) == 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == typeof(float))
|
||||
{
|
||||
property.SetValue(destination, RandomFloat(random));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
property.SetValue(destination, RandomArray(random, type.GetElementType()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[InlineData(DlnaProfileType.Audio)]
|
||||
[InlineData(DlnaProfileType.Video)]
|
||||
[InlineData(DlnaProfileType.Photo)]
|
||||
[Theory]
|
||||
public void Test_Blank_Url_Method(DlnaProfileType type)
|
||||
{
|
||||
var streamInfo = new LegacyStreamInfo(Guid.Empty, type)
|
||||
{
|
||||
DeviceProfile = new DeviceProfile()
|
||||
};
|
||||
|
||||
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
|
||||
|
||||
// New version will return and & after the ? due to optional parameters.
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fuzzy_Comparison()
|
||||
{
|
||||
var streamInfo = new LegacyStreamInfo(Guid.Empty, DlnaProfileType.Video)
|
||||
{
|
||||
DeviceProfile = new DeviceProfile()
|
||||
};
|
||||
for (int i = 0; i < 100000; i++)
|
||||
{
|
||||
FillAllProperties(streamInfo);
|
||||
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
|
||||
|
||||
// New version will return and & after the ? due to optional parameters.
|
||||
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue