aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Channels/IHasCacheKey.cs4
-rw-r--r--MediaBrowser.Controller/Channels/ISearchableChannel.cs21
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs24
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs11
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs49
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs8
-rw-r--r--MediaBrowser.Controller/LiveTv/ChannelInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs312
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs178
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs2
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs174
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs44
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs44
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs5
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs1
20 files changed, 686 insertions, 211 deletions
diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
index 9fae43033..7d5207c34 100644
--- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs
+++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
@@ -11,6 +9,6 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>System.String.</returns>
- string GetCacheKey(string userId);
+ string? GetCacheKey(string? userId);
}
}
diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
deleted file mode 100644
index b87943a6e..000000000
--- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Channels
-{
- public interface ISearchableChannel
- {
- /// <summary>
- /// Searches the specified search term.
- /// </summary>
- /// <param name="searchInfo">The search information.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
- Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
index 8ad93387e..8ecc68bab 100644
--- a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
+++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
@@ -1,6 +1,4 @@
-#nullable disable
-
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index ddcc994a0..5f9840b1b 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -62,7 +62,9 @@ namespace MediaBrowser.Controller.Entities
".edl",
".bif",
".smi",
- ".ttml"
+ ".ttml",
+ ".lrc",
+ ".elrc"
};
/// <summary>
@@ -831,7 +833,7 @@ namespace MediaBrowser.Controller.Entities
return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
}
- public bool CanDelete(User user)
+ public virtual bool CanDelete(User user)
{
var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
@@ -962,7 +964,13 @@ namespace MediaBrowser.Controller.Entities
AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
- return builder.ToString().RemoveDiacritics();
+ var result = builder.ToString().RemoveDiacritics();
+ if (!result.All(char.IsAscii))
+ {
+ result = result.Transliterated();
+ }
+
+ return result;
}
public BaseItem GetParent()
@@ -1578,18 +1586,24 @@ namespace MediaBrowser.Controller.Entities
list.AddRange(parent.Tags);
}
+ foreach (var folder in LibraryManager.GetCollectionFolders(this))
+ {
+ list.AddRange(folder.Tags);
+ }
+
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private bool IsVisibleViaTags(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ var allTags = GetInheritedTags();
+ if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
- if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 992bb19bb..676a47c88 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
@@ -95,6 +96,16 @@ namespace MediaBrowser.Controller.Entities
return GetLibraryOptions(Path);
}
+ public override bool IsVisible(User user)
+ {
+ if (GetLibraryOptions().Enabled)
+ {
+ return base.IsVisible(user);
+ }
+
+ return false;
+ }
+
private static LibraryOptions LoadLibraryOptions(string path)
{
try
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 1f13c833b..8bfcf5dee 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -331,8 +331,25 @@ namespace MediaBrowser.Controller.Entities
}
}
+ private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item)
+ {
+ // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
+ if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
+ {
+ Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.ContainingFolderPath);
+ return false;
+ }
+
+ return true;
+ }
+
private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
+ if (!IsLibraryFolderAccessible(directoryService, this))
+ {
+ return;
+ }
+
cancellationToken.ThrowIfCancellationRequested();
var validChildren = new List<BaseItem>();
@@ -369,6 +386,11 @@ namespace MediaBrowser.Controller.Entities
foreach (var child in nonCachedChildren)
{
+ if (!IsLibraryFolderAccessible(directoryService, child))
+ {
+ continue;
+ }
+
if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
{
validChildren.Add(currentChild);
@@ -392,8 +414,8 @@ namespace MediaBrowser.Controller.Entities
validChildren.Add(child);
}
- // If any items were added or removed....
- if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
+ // If it's an AggregateFolder, don't remove
+ if (!IsRoot && currentChildren.Count != validChildren.Count)
{
// That's all the new and changed ones - now see if there are any that are missing
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
@@ -408,7 +430,10 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
}
+ }
+ if (newItems.Count > 0)
+ {
LibraryManager.CreateItems(newItems, this, cancellationToken);
}
}
@@ -435,15 +460,7 @@ namespace MediaBrowser.Controller.Entities
progress.Report(percent);
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
});
if (validChildrenNeedGeneration)
@@ -475,15 +492,7 @@ namespace MediaBrowser.Controller.Entities
if (recursive)
{
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
}
});
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index bace703ad..44a1a85e3 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Library
MediaProtocol GetPathProtocol(string path);
- void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
+ void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user);
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index ec34a868b..93073cc79 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
@@ -19,7 +17,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from artist.
@@ -28,7 +26,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from genre.
@@ -37,6 +35,6 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
index 699c15f93..52581df45 100644
--- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
@@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.LiveTv
public string ChannelGroup { get; set; }
/// <summary>
- /// Gets or sets the the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b6738e7cc..eb375c8a2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1,6 +1,8 @@
#nullable disable
#pragma warning disable CS1591
+// We need lowercase normalized string for ffmpeg
+#pragma warning disable CA1308
using System;
using System.Collections.Generic;
@@ -26,6 +28,14 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public partial class EncodingHelper
{
+ /// <summary>
+ /// The codec validation regex.
+ /// This regular expression matches strings that consist of alphanumeric characters, hyphens,
+ /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
+ /// This should matches all common valid codecs.
+ /// </summary>
+ public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
+
private const string QsvAlias = "qs";
private const string VaapiAlias = "va";
private const string D3d11vaAlias = "dx11";
@@ -51,6 +61,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
+ private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
+
+ private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -94,7 +107,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "wmav2", 2 },
{ "libmp3lame", 2 },
{ "libfdk_aac", 6 },
- { "aac_at", 6 },
{ "ac3", 6 },
{ "eac3", 6 },
{ "dca", 6 },
@@ -253,6 +265,15 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
}
+ private bool IsVideoToolboxFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("videotoolbox")
+ && _mediaEncoder.SupportsFilter("yadif_videotoolbox")
+ && _mediaEncoder.SupportsFilter("overlay_videotoolbox")
+ && _mediaEncoder.SupportsFilter("tonemap_videotoolbox")
+ && _mediaEncoder.SupportsFilter("scale_vt");
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
@@ -272,12 +293,15 @@ namespace MediaBrowser.Controller.MediaEncoding
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
- return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
+ var isVideoToolBoxDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
- || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -305,7 +329,23 @@ namespace MediaBrowser.Controller.MediaEncoding
// Native VPP tonemapping may come to QSV in the future.
return state.VideoStream.VideoRange == VideoRange.HDR
- && state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
+ && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
+ }
+
+ private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream is null
+ || !options.EnableVideoToolboxTonemapping
+ || GetVideoColorBitDepth(state) != 10)
+ {
+ return false;
+ }
+
+ // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
+ // All other HDR formats working.
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
}
/// <summary>
@@ -362,7 +402,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libtheora";
}
- return codec.ToLowerInvariant();
+ if (_validationRegex.IsMatch(codec))
+ {
+ return codec.ToLowerInvariant();
+ }
}
return "copy";
@@ -400,7 +443,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
- if (string.IsNullOrEmpty(container))
+ if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
{
return null;
}
@@ -656,6 +699,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
+ if (!_validationRegex.IsMatch(codec))
+ {
+ codec = "aac";
+ }
+
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
{
// Use Apple's aac encoder if available as it provides best audio quality
@@ -703,6 +751,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return "dca";
}
+ if (string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ // The ffmpeg upstream breaks the AudioToolbox ALAC encoder in version 6.1 but fixes it in version 7.0.
+ // Since ALAC is lossless in quality and the AudioToolbox encoder is not faster,
+ // its only benefit is a smaller file size.
+ // To prevent problems, use the ffmpeg native encoder instead.
+ return "alac";
+ }
+
return codec.ToLowerInvariant();
}
@@ -1071,7 +1128,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- // no videotoolbox hw filter.
+ // videotoolbox hw filter does not require device selection
args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias));
}
else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase))
@@ -1197,7 +1254,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
- if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4))
+ if (!isSwDecoder)
{
arg.Append(" -noautoscale");
}
@@ -1214,23 +1271,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("264", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("avc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("265", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
}
public static string GetBitStreamArgs(MediaStream stream)
@@ -1284,7 +1341,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return ".ts";
}
- public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
+ private string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
if (state.OutputVideoBitrate is null)
{
@@ -1348,6 +1405,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
+ if (string.Equals(videoCodec, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ // The `maxrate` and `bufsize` options can potentially lead to performance regression
+ // and even encoder hangs, especially when the value is very high.
+ return FormattableString.Invariant($" -b:v {bitrate} -qmin -1 -qmax -1");
+ }
+
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
@@ -1818,6 +1883,31 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -gops_per_idr 1";
}
}
+ else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox)
+ || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_videotoolbox)
+ {
+ switch (encodingOptions.EncoderPreset)
+ {
+ case "veryslow":
+ case "slower":
+ case "slow":
+ case "medium":
+ param += " -prio_speed 0";
+ break;
+
+ case "fast":
+ case "faster":
+ case "veryfast":
+ case "superfast":
+ case "ultrafast":
+ param += " -prio_speed 1";
+ break;
+
+ default:
+ param += " -prio_speed 1";
+ break;
+ }
+ }
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
{
// Values 0-3, 0 being highest quality but slower
@@ -2181,7 +2271,16 @@ namespace MediaBrowser.Controller.MediaEncoding
return false;
}
- if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
+ // DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
+
+ var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
+ && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
+ || (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
+ || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)))
{
return false;
}
@@ -4954,22 +5053,29 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null, null);
}
- var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
+ var isMacOS = OperatingSystem.IsMacOS();
+ var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
- if (!options.EnableHardwareEncoding)
+ // legacy videotoolbox pipeline (disable hw filters)
+ if (!isVtEncoder
+ || !isVtFullSupported
+ || !_mediaEncoder.SupportsFilter("alphasrc"))
{
- return swFilterChain;
+ return GetSwVidFilterChain(state, options, vidEncoder);
}
- if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0)
- {
- // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg
- return swFilterChain;
- }
+ // preferred videotoolbox + metal filters pipeline
+ return GetAppleVidFiltersPreferred(state, options, vidDecoder, vidEncoder);
+ }
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFiltersPreferred(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
var reqW = state.BaseRequest.Width;
@@ -4977,33 +5083,121 @@ namespace MediaBrowser.Controller.MediaEncoding
var reqMaxW = state.BaseRequest.MaxWidth;
var reqMaxH = state.BaseRequest.MaxHeight;
var threeDFormat = state.MediaSource.Video3DFormat;
- var newfilters = new List<string>();
- var noOverlay = swFilterChain.OverlayFilters.Count == 0;
- var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox");
- // fallback to software filters if we are using filters not supported by hardware yet.
- var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint);
- if (!useHardwareFilters)
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
+ var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
+
+ var scaleFormat = string.Empty;
+ // Use P010 for Metal tone mapping, otherwise force an 8bit output.
+ if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase))
{
- return swFilterChain;
+ if (doMetalTonemap)
+ {
+ if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase))
+ {
+ scaleFormat = "p010le";
+ }
+ }
+ else
+ {
+ scaleFormat = "nv12";
+ }
}
- // ffmpeg cannot use videotoolbox to scale
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
- newfilters.Add(swScaleFilter);
+ var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
+ var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var hasAssSubs = hasSubs
+ && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+
+ if (!isVtEncoder)
+ {
+ // should not happen.
+ return (null, null, null);
+ }
- // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent
- // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here
- // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion
- newfilters.Add("hwupload");
+ /* Make main filters for video stream */
+ var mainFilters = new List<string>();
+ // hw deint
if (doDeintH2645)
{
var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
- newfilters.Add(deintFilter);
+ mainFilters.Add(deintFilter);
+ }
+
+ if (doVtTonemap)
+ {
+ const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709";
+
+ // scale_vt can handle scaling & tonemapping in one shot, just like vpp_qsv.
+ hwScaleFilter = string.IsNullOrEmpty(hwScaleFilter)
+ ? "scale_vt=" + VtTonemapArgs
+ : hwScaleFilter + ":" + VtTonemapArgs;
+ }
+
+ // hw scale & vt tonemap
+ mainFilters.Add(hwScaleFilter);
+
+ // Metal tonemap
+ if (doMetalTonemap)
+ {
+ var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12");
+ mainFilters.Add(tonemapFilter);
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List<string>();
+ var overlayFilters = new List<string>();
+
+ if (hasSubs)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
+
+ subFilters.Add("hwupload=derive_device=videotoolbox");
+ overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
}
- return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
+ var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ overlayFilters.Any(f => !string.IsNullOrEmpty(f));
+
+ // This is a workaround for ffmpeg's hwupload implementation
+ // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
+ // will cause the encoder to produce incorrect frames.
+ if (needFiltering)
+ {
+ // INPUT videotoolbox/memory surface(vram/uma)
+ // this will pass-through automatically if in/out format matches.
+ mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
+ mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
}
/// <summary>
@@ -5995,22 +6189,22 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
+ // For example: https://trac.ffmpeg.org/ticket/10884
+ // Disable it for now.
+ const bool UseHwSurface = false;
+
if (is8bitSwFormatsVt)
{
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "h264", bitDepth, false);
- }
-
- if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "mpeg2video", bitDepth, false);
+ return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface);
}
- if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "mpeg4", bitDepth, false);
+ return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface);
}
}
@@ -6019,12 +6213,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, false);
+ return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface);
}
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, false);
+ return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface);
}
}
@@ -6265,6 +6459,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
inputModifier += " -re";
}
+ else if (encodingOptions.EnableSegmentDeletion
+ && state.VideoStream is not null
+ && state.TranscodingType == TranscodingJobType.Hls
+ && IsCopyCodec(state.OutputVideoCodec)
+ && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateOption)
+ {
+ // Set an input read rate limit 10x for using SegmentDeletion with stream-copy
+ // to prevent ffmpeg from exiting prematurely (due to fast drive)
+ inputModifier += " -readrate 10";
+ }
var flags = new List<string>();
if (state.IgnoreInputDts)
@@ -6464,7 +6668,7 @@ namespace MediaBrowser.Controller.MediaEncoding
while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparison.OrdinalIgnoreCase))
{
- var removed = shiftAudioCodecs[0];
+ var removed = audioCodecs[0];
audioCodecs.RemoveAt(0);
audioCodecs.Add(removed);
}
@@ -6498,7 +6702,7 @@ namespace MediaBrowser.Controller.MediaEncoding
while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparison.OrdinalIgnoreCase))
{
- var removed = shiftVideoCodecs[0];
+ var removed = videoCodecs[0];
videoCodecs.RemoveAt(0);
videoCodecs.Add(removed);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index c2cef4978..e696fa52c 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -149,6 +149,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="maxWidth">The maximum width.</param>
/// <param name="interval">The interval.</param>
/// <param name="allowHwAccel">Allow for hardware acceleration.</param>
+ /// <param name="enableHwEncoding">Use hardware mjpeg encoder.</param>
/// <param name="threads">The input/output thread count for ffmpeg.</param>
/// <param name="qualityScale">The qscale value for ffmpeg.</param>
/// <param name="priority">The process priority for the ffmpeg process.</param>
@@ -163,6 +164,7 @@ namespace MediaBrowser.Controller.MediaEncoding
int maxWidth,
TimeSpan interval,
bool allowHwAccel,
+ bool enableHwEncoding,
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
index 1e6d5933c..2b6540ea8 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
@@ -137,6 +137,11 @@ public sealed class TranscodingJob : IDisposable
public TranscodingThrottler? TranscodingThrottler { get; set; }
/// <summary>
+ /// Gets or sets transcoding segment cleaner.
+ /// </summary>
+ public TranscodingSegmentCleaner? TranscodingSegmentCleaner { get; set; }
+
+ /// <summary>
/// Gets or sets last ping date.
/// </summary>
public DateTime LastPingDate { get; set; }
@@ -239,6 +244,7 @@ public sealed class TranscodingJob : IDisposable
{
#pragma warning disable CA1849 // Can't await in lock block
TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+ TranscodingSegmentCleaner?.Stop();
var process = Process;
@@ -276,5 +282,7 @@ public sealed class TranscodingJob : IDisposable
CancellationTokenSource = null;
TranscodingThrottler?.Dispose();
TranscodingThrottler = null;
+ TranscodingSegmentCleaner?.Dispose();
+ TranscodingSegmentCleaner = null;
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
new file mode 100644
index 000000000..67bfcb02f
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.MediaEncoding;
+
+/// <summary>
+/// Transcoding segment cleaner.
+/// </summary>
+public class TranscodingSegmentCleaner : IDisposable
+{
+ private readonly TranscodingJob _job;
+ private readonly ILogger<TranscodingSegmentCleaner> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
+ private Timer? _timer;
+ private int _segmentLength;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class.
+ /// </summary>
+ /// <param name="job">Transcoding job dto.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingSegmentCleaner}"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="segmentLength">The segment length of this transcoding job.</param>
+ public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength)
+ {
+ _job = job;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _segmentLength = segmentLength;
+ }
+
+ /// <summary>
+ /// Start timer.
+ /// </summary>
+ public void Start()
+ {
+ _timer = new Timer(TimerCallback, null, 20000, 20000);
+ }
+
+ /// <summary>
+ /// Stop cleaner.
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ DisposeTimer();
+ }
+ }
+
+ private EncodingOptions GetOptions()
+ {
+ return _config.GetEncodingOptions();
+ }
+
+ private async void TimerCallback(object? state)
+ {
+ if (_job.HasExited)
+ {
+ DisposeTimer();
+ return;
+ }
+
+ var options = GetOptions();
+ var enableSegmentDeletion = options.EnableSegmentDeletion;
+ var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20);
+
+ if (enableSegmentDeletion)
+ {
+ var downloadPositionTicks = _job.DownloadPositionTicks ?? 0;
+ var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds);
+
+ if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds)
+ {
+ var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength;
+
+ if (idxMaxToDelete > 0)
+ {
+ await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int delayMs)
+ {
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
+ _logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path);
+
+ await Task.Delay(delayMs).ConfigureAwait(false);
+
+ try
+ {
+ if (job.Type == TranscodingJobType.Hls)
+ {
+ DeleteHlsSegmentFiles(path, idxMin, idxMax);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Error deleting segment file(s) {Path}", path);
+ }
+ }
+
+ private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax)
+ {
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx)
+ && (idx >= idxMin && idx <= idxMax));
+
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ try
+ {
+ _logger.LogDebug("Deleting HLS segment file {0}", file);
+ _fileSystem.DeleteFile(file);
+ }
+ catch (IOException ex)
+ {
+ (exs ??= new List<Exception>()).Add(ex);
+ _logger.LogDebug(ex, "Error deleting HLS segment file {Path}", file);
+ }
+ }
+
+ if (exs is not null)
+ {
+ throw new AggregateException("Error deleting HLS segment files", exs);
+ }
+ }
+
+ private void DisposeTimer()
+ {
+ if (_timer is not null)
+ {
+ _timer.Dispose();
+ _timer = null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
index 813f13eae..b95e6ed51 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
@@ -115,7 +115,7 @@ public class TranscodingThrottler : IDisposable
var options = GetOptions();
- if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
+ if (options.EnableThrottling && IsThrottleAllowed(_job, Math.Max(options.ThrottleDelaySeconds, 60)))
{
await PauseTranscoding().ConfigureAwait(false);
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 0a706c307..06386f2b8 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Model.Session;
@@ -21,26 +22,38 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam>
/// <typeparam name="TStateType">The type of the T state type.</typeparam>
- public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable
+ public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IAsyncDisposable
where TStateType : WebSocketListenerState, new()
where TReturnDataType : class
{
+ private readonly Channel<bool> _channel = Channel.CreateUnbounded<bool>(new UnboundedChannelOptions
+ {
+ AllowSynchronousContinuations = false,
+ SingleReader = true,
+ SingleWriter = false
+ });
+
+ private readonly object _activeConnectionsLock = new();
+
/// <summary>
/// The _active connections.
/// </summary>
- private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections =
- new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
+ private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
/// <summary>
/// The logger.
/// </summary>
protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+ private readonly Task _messageConsumerTask;
+
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
{
ArgumentNullException.ThrowIfNull(logger);
Logger = logger;
+
+ _messageConsumerTask = HandleMessages();
}
/// <summary>
@@ -113,75 +126,93 @@ namespace MediaBrowser.Controller.Net
InitialDelayMs = dueTimeMs
};
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
- _activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state));
+ _activeConnections.Add((message.Connection, cancellationTokenSource, state));
}
}
- protected async Task SendData(bool force)
+ protected void SendData(bool force)
{
- Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples;
+ _channel.Writer.TryWrite(force);
+ }
- lock (_activeConnections)
+ private async Task HandleMessages()
+ {
+ while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false))
{
- tuples = _activeConnections
- .Where(c =>
+ while (_channel.Reader.TryRead(out var force))
+ {
+ try
{
- if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested)
- {
- var state = c.Item3;
+ (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples;
- if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs)
+ var now = DateTime.UtcNow;
+ lock (_activeConnectionsLock)
+ {
+ if (_activeConnections.Count == 0)
{
- return true;
+ continue;
}
+
+ tuples = _activeConnections
+ .Where(c =>
+ {
+ if (c.Connection.State != WebSocketState.Open || c.CancellationTokenSource.IsCancellationRequested)
+ {
+ return false;
+ }
+
+ var state = c.State;
+ return force || (now - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs;
+ })
+ .ToArray();
}
- return false;
- })
- .ToArray();
- }
+ if (tuples.Length == 0)
+ {
+ continue;
+ }
- IEnumerable<Task> GetTasks()
- {
- foreach (var tuple in tuples)
- {
- yield return SendData(tuple);
+ var data = await GetDataToSend().ConfigureAwait(false);
+ if (data is null)
+ {
+ continue;
+ }
+
+ IEnumerable<Task> GetTasks()
+ {
+ foreach (var tuple in tuples)
+ {
+ yield return SendDataInternal(data, tuple);
+ }
+ }
+
+ await Task.WhenAll(GetTasks()).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to send updates to websockets");
+ }
}
}
-
- await Task.WhenAll(GetTasks()).ConfigureAwait(false);
}
- private async Task SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple)
+ private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
{
- var connection = tuple.Item1;
-
try
{
- var state = tuple.Item3;
+ var (connection, cts, state) = tuple;
+ var cancellationToken = cts.Token;
+ await connection.SendAsync(
+ new OutboundWebSocketMessage<TReturnDataType> { MessageType = Type, Data = data },
+ cancellationToken).ConfigureAwait(false);
- var cancellationToken = tuple.Item2.Token;
-
- var data = await GetDataToSend().ConfigureAwait(false);
-
- if (data is not null)
- {
- await connection.SendAsync(
- new OutboundWebSocketMessage<TReturnDataType>
- {
- MessageType = Type,
- Data = data
- },
- cancellationToken).ConfigureAwait(false);
-
- state.DateLastSendUtc = DateTime.UtcNow;
- }
+ state.DateLastSendUtc = DateTime.UtcNow;
}
catch (OperationCanceledException)
{
- if (tuple.Item2.IsCancellationRequested)
+ if (tuple.CancellationTokenSource.IsCancellationRequested)
{
DisposeConnection(tuple);
}
@@ -199,11 +230,11 @@ namespace MediaBrowser.Controller.Net
/// <param name="message">The message.</param>
private void Stop(WebSocketMessageInfo message)
{
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
- var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection);
+ var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection);
- if (connection is not null)
+ if (connection != default)
{
DisposeConnection(connection);
}
@@ -214,17 +245,17 @@ namespace MediaBrowser.Controller.Net
/// Disposes the connection.
/// </summary>
/// <param name="connection">The connection.</param>
- private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
+ private void DisposeConnection((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) connection)
{
- Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Connection.RemoteEndPoint, GetType().Name);
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
// connection.Item1.Dispose();
try
{
- connection.Item2.Cancel();
- connection.Item2.Dispose();
+ connection.CancellationTokenSource.Cancel();
+ connection.CancellationTokenSource.Dispose();
}
catch (ObjectDisposedException ex)
{
@@ -237,36 +268,37 @@ namespace MediaBrowser.Controller.Net
Logger.LogError(ex, "Error disposing websocket");
}
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
_activeConnections.Remove(connection);
}
}
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
+ protected virtual async ValueTask DisposeAsyncCore()
{
- if (dispose)
+ try
{
- lock (_activeConnections)
+ _channel.Writer.TryComplete();
+ await _messageConsumerTask.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Disposing the message consumer failed");
+ }
+
+ lock (_activeConnectionsLock)
+ {
+ foreach (var connection in _activeConnections.ToArray())
{
- foreach (var connection in _activeConnections.ToArray())
- {
- DisposeConnection(connection);
- }
+ DisposeConnection(connection);
}
}
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
{
- Dispose(true);
+ await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index bb68a3b6d..cbe4bd87f 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
namespace MediaBrowser.Controller.Playlists
@@ -11,6 +12,28 @@ namespace MediaBrowser.Controller.Playlists
public interface IPlaylistManager
{
/// <summary>
+ /// Gets the playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Playlist.</returns>
+ Playlist GetPlaylistForUser(Guid playlistId, Guid userId);
+
+ /// <summary>
+ /// Creates the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistCreationRequest"/>.</param>
+ /// <returns>The created playlist.</returns>
+ Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request);
+
+ /// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task UpdatePlaylist(PlaylistUpdateRequest request);
+
+ /// <summary>
/// Gets the playlists.
/// </summary>
/// <param name="userId">The user identifier.</param>
@@ -18,11 +41,20 @@ namespace MediaBrowser.Controller.Playlists
IEnumerable<Playlist> GetPlaylists(Guid userId);
/// <summary>
- /// Creates the playlist.
+ /// Adds a share to the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUserUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task AddUserToShares(PlaylistUserUpdateRequest request);
+
+ /// <summary>
+ /// Removes a share from the playlist.
/// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task&lt;Playlist&gt;.</returns>
- Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="share">The share.</param>
+ /// <returns>Task.</returns>
+ Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share);
/// <summary>
/// Adds to playlist.
@@ -31,7 +63,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
@@ -39,7 +71,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryIds">The entry ids.</param>
/// <returns>Task.</returns>
- Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
+ Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
/// <summary>
/// Gets the playlists folder.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index ca032e7f6..34b34e578 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -16,24 +16,23 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
{
public class Playlist : Folder, IHasShares
{
- public static readonly IReadOnlyList<string> SupportedExtensions = new[]
- {
+ public static readonly IReadOnlyList<string> SupportedExtensions =
+ [
".m3u",
".m3u8",
".pls",
".wpl",
".zpl"
- };
+ ];
public Playlist()
{
- Shares = Array.Empty<Share>();
+ Shares = [];
OpenAccess = false;
}
@@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists
public bool OpenAccess { get; set; }
- public Share[] Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
@@ -130,7 +129,7 @@ namespace MediaBrowser.Controller.Playlists
protected override List<BaseItem> LoadChildren()
{
// Save a trip to the database
- return new List<BaseItem>();
+ return [];
}
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -145,7 +144,7 @@ namespace MediaBrowser.Controller.Playlists
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
- return new List<BaseItem>();
+ return [];
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
@@ -167,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -192,9 +191,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- GenreIds = new[] { musicGenre.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ GenreIds = [musicGenre.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -204,9 +203,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- ArtistIds = new[] { musicArtist.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ ArtistIds = [musicArtist.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -217,8 +216,8 @@ namespace MediaBrowser.Controller.Playlists
{
Recursive = true,
IsFolder = false,
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- MediaTypes = new[] { mediaType },
+ OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
+ MediaTypes = [mediaType],
EnableTotalRecordCount = false,
DtoOptions = options
};
@@ -226,7 +225,7 @@ namespace MediaBrowser.Controller.Playlists
return folder.GetItemList(query);
}
- return new[] { item };
+ return [item];
}
public override bool IsVisible(User user)
@@ -248,12 +247,17 @@ namespace MediaBrowser.Controller.Playlists
}
var shares = Shares;
- if (shares.Length == 0)
+ if (shares.Count == 0)
{
return false;
}
- return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
+ return shares.Any(s => s.UserId.Equals(userId));
+ }
+
+ public override bool CanDelete(User user)
+ {
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.Id.Equals(OwnerUserId);
}
public override bool IsVisibleStandalone(User user)
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index d4de97651..7fe2f64af 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -78,5 +78,10 @@ namespace MediaBrowser.Controller.Providers
return filePaths;
}
+
+ public bool IsAccessible(string path)
+ {
+ return _fileSystem.GetFileSystemEntryPaths(path).Any();
+ }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 48d627691..6d7550ab5 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -16,5 +16,7 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path);
IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
+
+ bool IsAccessible(string path);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 3a12a56f1..76d5d3a3f 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -134,6 +134,7 @@ namespace MediaBrowser.Controller.Session
/// <value>The now playing item.</value>
public BaseItemDto NowPlayingItem { get; set; }
+ [JsonIgnore]
public BaseItem FullNowPlayingItem { get; set; }
public BaseItemDto NowViewingItem { get; set; }