Keep the queried item's media source as the playback default

This commit is contained in:
Shadowghost
2026-06-05 19:41:08 +02:00
parent 4459147788
commit 6c931dcdda
4 changed files with 112 additions and 4 deletions

View File

@@ -213,7 +213,7 @@ public class MediaInfoController : BaseJellyfinApiController
Request.HttpContext.GetNormalizedRemoteIP());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate, item.Id);
}
if (autoOpenLiveStream.Value)

View File

@@ -163,7 +163,7 @@ public class UniversalAudioController : BaseJellyfinApiController
Request.HttpContext.GetNormalizedRemoteIP());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate, item.Id);
foreach (var source in info.MediaSources)
{

View File

@@ -351,11 +351,20 @@ public class MediaInfoHelper
/// </summary>
/// <param name="result">Playback info response.</param>
/// <param name="maxBitrate">Max bitrate.</param>
public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
/// <param name="preferredItemId">The id of the queried item, whose own media source must stay the default.</param>
public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate, Guid preferredItemId = default)
{
var originalList = result.MediaSources.ToList();
result.MediaSources = result.MediaSources.OrderBy(i =>
// The queried item's source carries the user's resume state for that version, so it must stay the
// default the client plays. An unfavorable bitrate means transcoding it, not switching to a sibling version.
var preferredId = preferredItemId.IsEmpty()
? null
: preferredItemId.ToString("N", CultureInfo.InvariantCulture);
result.MediaSources = result.MediaSources
.OrderByDescending(i => preferredId is not null && string.Equals(i.Id, preferredId, StringComparison.OrdinalIgnoreCase))
.ThenBy(i =>
{
// Nothing beats direct playing a file
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)

View File

@@ -0,0 +1,99 @@
using System;
using System.Globalization;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Helpers
{
public class MediaInfoHelperTests
{
private static MediaInfoHelper CreateHelper()
{
return new MediaInfoHelper(
Mock.Of<IUserManager>(),
Mock.Of<ILibraryManager>(),
Mock.Of<IMediaSourceManager>(),
Mock.Of<IMediaEncoder>(),
Mock.Of<IServerConfigurationManager>(),
Mock.Of<ILogger<MediaInfoHelper>>(),
Mock.Of<INetworkManager>(),
Mock.Of<IDeviceManager>());
}
private static MediaSourceInfo CreateSource(Guid itemId, int bitrate, bool supportsDirectPlay = true)
{
return new MediaSourceInfo
{
Id = itemId.ToString("N", CultureInfo.InvariantCulture),
Protocol = MediaProtocol.File,
Bitrate = bitrate,
SupportsDirectPlay = supportsDirectPlay,
SupportsDirectStream = true,
SupportsTranscoding = true
};
}
[Fact]
public void SortMediaSources_PreferredItemExceedsBitrate_StaysDefault()
{
// The version the user was watching (the queried item) must stay the default
// even when a sibling version fits the bitrate limit better, since the resume
// position belongs to that exact version.
var preferredItemId = Guid.NewGuid();
var preferredSource = CreateSource(preferredItemId, bitrate: 80_000_000, supportsDirectPlay: false);
var siblingSource = CreateSource(Guid.NewGuid(), bitrate: 8_000_000);
var result = new PlaybackInfoResponse
{
MediaSources = [siblingSource, preferredSource]
};
CreateHelper().SortMediaSources(result, maxBitrate: 20_000_000, preferredItemId);
Assert.Equal(preferredSource.Id, result.MediaSources[0].Id);
}
[Fact]
public void SortMediaSources_NoPreferredItem_OrdersByPlayability()
{
var directPlay = CreateSource(Guid.NewGuid(), bitrate: 8_000_000);
var transcodeOnly = CreateSource(Guid.NewGuid(), bitrate: 8_000_000, supportsDirectPlay: false);
transcodeOnly.SupportsDirectStream = false;
var result = new PlaybackInfoResponse
{
MediaSources = [transcodeOnly, directPlay]
};
CreateHelper().SortMediaSources(result, maxBitrate: 20_000_000);
Assert.Equal(directPlay.Id, result.MediaSources[0].Id);
}
[Fact]
public void SortMediaSources_PreferredIdNotInSources_KeepsPlayabilityOrder()
{
var directPlay = CreateSource(Guid.NewGuid(), bitrate: 8_000_000);
var transcodeOnly = CreateSource(Guid.NewGuid(), bitrate: 8_000_000, supportsDirectPlay: false);
transcodeOnly.SupportsDirectStream = false;
var result = new PlaybackInfoResponse
{
MediaSources = [transcodeOnly, directPlay]
};
CreateHelper().SortMediaSources(result, maxBitrate: 20_000_000, Guid.NewGuid());
Assert.Equal(directPlay.Id, result.MediaSources[0].Id);
}
}
}