mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-09 01:08:45 +01:00
Merge pull request #17041 from Shadowghost/media-source-handling-fixes
Media source handling fixes
This commit is contained in:
@@ -229,7 +229,7 @@ namespace Emby.Server.Implementations.Library
|
||||
list.Add(source);
|
||||
}
|
||||
|
||||
return SortMediaSources(list).ToArray();
|
||||
return SortMediaSources(list, item.Id).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />>
|
||||
@@ -386,6 +386,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
sources = sources
|
||||
.Where(source => !Guid.TryParse(source.Id, out var sourceId)
|
||||
|| sourceId.Equals(item.Id)
|
||||
|| _libraryManager.GetItemById<BaseItem>(sourceId, user) is not null)
|
||||
.ToArray();
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
|
||||
@@ -540,24 +546,32 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources, Guid preferredItemId = default)
|
||||
{
|
||||
return sources.OrderBy(i =>
|
||||
{
|
||||
if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
|
||||
// The source belonging to the queried item sorts first so it stays the default that gets played.
|
||||
var preferredId = preferredItemId.IsEmpty()
|
||||
? null
|
||||
: preferredItemId.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
return sources
|
||||
.OrderByDescending(i => preferredId is not null && string.Equals(i.Id, preferredId, StringComparison.OrdinalIgnoreCase))
|
||||
.ThenBy(i =>
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
|
||||
.ThenByDescending(i =>
|
||||
{
|
||||
var stream = i.VideoStream;
|
||||
return 1;
|
||||
})
|
||||
.ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
|
||||
.ThenByDescending(i =>
|
||||
{
|
||||
var stream = i.VideoStream;
|
||||
|
||||
return stream?.Width ?? 0;
|
||||
})
|
||||
.Where(i => i.Type != MediaSourceType.Placeholder);
|
||||
return stream?.Width ?? 0;
|
||||
})
|
||||
.Where(i => i.Type != MediaSourceType.Placeholder);
|
||||
}
|
||||
|
||||
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
||||
|
||||
@@ -92,7 +92,8 @@ public class ChapterImagesTask : IScheduledTask
|
||||
EnableImages = false
|
||||
},
|
||||
SourceTypes = [SourceType.Library],
|
||||
IsVirtualItem = false
|
||||
IsVirtualItem = false,
|
||||
IncludeOwnedItems = true
|
||||
})
|
||||
.OfType<Video>()
|
||||
.ToList();
|
||||
|
||||
@@ -68,6 +68,7 @@ public class MediaSegmentExtractionTask : IScheduledTask
|
||||
DtoOptions = new DtoOptions(true),
|
||||
SourceTypes = [SourceType.Library],
|
||||
Recursive = true,
|
||||
IncludeOwnedItems = true,
|
||||
Limit = pagesize
|
||||
};
|
||||
|
||||
|
||||
@@ -981,6 +981,7 @@ public class ItemsController : BaseJellyfinApiController
|
||||
MediaTypes = mediaTypes,
|
||||
IsVirtualItem = false,
|
||||
CollapseBoxSetItems = false,
|
||||
IncludeOwnedItems = true,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
AncestorIds = ancestorIds,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
|
||||
@@ -213,7 +213,7 @@ public class MediaInfoController : BaseJellyfinApiController
|
||||
Request.HttpContext.GetNormalizedRemoteIP());
|
||||
}
|
||||
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate, item.Id);
|
||||
}
|
||||
|
||||
if (autoOpenLiveStream.Value)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -102,7 +102,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
DtoOptions = new DtoOptions(true),
|
||||
SourceTypes = new[] { SourceType.Library },
|
||||
Parent = library,
|
||||
Recursive = true
|
||||
Recursive = true,
|
||||
IncludeOwnedItems = true
|
||||
};
|
||||
|
||||
if (skipIfAudioTrackMatches)
|
||||
|
||||
@@ -60,6 +60,7 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
|
||||
DtoOptions = new DtoOptions(true),
|
||||
SourceTypes = [SourceType.Library],
|
||||
Recursive = true,
|
||||
IncludeOwnedItems = true,
|
||||
Limit = Pagesize
|
||||
};
|
||||
|
||||
|
||||
99
tests/Jellyfin.Api.Tests/Helpers/MediaInfoHelperTests.cs
Normal file
99
tests/Jellyfin.Api.Tests/Helpers/MediaInfoHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user