diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f49f8ed9bd..3be6331c8f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -78,6 +78,7 @@
- [Marenz](https://github.com/Marenz)
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
+ - [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
- [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8b4813f566..3c9c5d9c1b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c38f1af912..18ada6aeb5 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -52,7 +53,7 @@ namespace Emby.Server.Implementations.Library
private readonly IDirectoryService _directoryService;
private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
@@ -468,12 +469,10 @@ namespace Emby.Server.Implementations.Library
public async Task> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
MediaSourceInfo mediaSource;
ILiveStream liveStream;
- try
+ using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
{
var (provider, keyId) = GetProvider(request.OpenToken);
@@ -493,10 +492,6 @@ namespace Emby.Server.Implementations.Library
_openStreams[mediaSource.LiveStreamId] = liveStream;
}
- finally
- {
- _liveStreamSemaphore.Release();
- }
try
{
@@ -837,9 +832,7 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
- await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
-
- try
+ using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
{
if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
{
@@ -858,10 +851,6 @@ namespace Emby.Server.Implementations.Library
}
}
}
- finally
- {
- _liveStreamSemaphore.Release();
- }
}
private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
@@ -898,7 +887,7 @@ namespace Emby.Server.Implementations.Library
CloseLiveStream(key).GetAwaiter().GetResult();
}
- _liveStreamSemaphore.Dispose();
+ _liveStreamLocker.Dispose();
}
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index dda1e9d561..590cdc33f0 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -294,9 +294,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!System.IO.File.Exists(playlistPath))
{
- var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
if (!System.IO.File.Exists(playlistPath))
{
@@ -326,10 +324,6 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
}
- finally
- {
- transcodingLock.Release();
- }
}
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
@@ -1442,95 +1436,80 @@ public class DynamicHlsController : BaseJellyfinApiController
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
+ using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
+ var startTranscoding = false;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
+
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+ if (segmentId == -1)
+ {
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
+ }
+ else if (currentTranscodingIndex is null)
+ {
+ _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
+ startTranscoding = true;
+ }
+ else if (segmentId < currentTranscodingIndex.Value)
+ {
+ _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
+ startTranscoding = true;
+ }
+ else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
+ {
+ _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
+ startTranscoding = true;
+ }
+
+ if (startTranscoding)
+ {
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
+ {
+ await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+ .ConfigureAwait(false);
+
+ if (currentTranscodingIndex.HasValue)
+ {
+ DeleteLastFile(playlistPath, segmentExtension, 0);
+ }
+
+ streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+
+ state.WaitForPath = segmentPath;
+ job = await _transcodeManager.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, false, segmentId),
+ Request.HttpContext.User.GetUserId(),
+ TranscodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+ catch
+ {
+ state.Dispose();
+ throw;
+ }
+
+ // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
else
{
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (segmentId == -1)
+ job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ if (job?.TranscodingThrottler is not null)
{
- _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
- startTranscoding = true;
- segmentId = 0;
+ await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
}
- else if (currentTranscodingIndex is null)
- {
- _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (segmentId < currentTranscodingIndex.Value)
- {
- _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
- startTranscoding = true;
- }
-
- if (startTranscoding)
- {
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
- .ConfigureAwait(false);
-
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
-
- streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
-
- state.WaitForPath = segmentPath;
- job = await _transcodeManager.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, false, segmentId),
- Request.HttpContext.User.GetUserId(),
- TranscodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
- catch
- {
- state.Dispose();
- throw;
- }
-
- // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- else
- {
- job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job?.TranscodingThrottler is not null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
- }
- }
- }
- }
- finally
- {
- if (!released)
- {
- transcodingLock.Release();
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 5385979d4a..cb178a61d8 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -93,9 +93,7 @@ public static class FileStreamResponseHelpers
return new OkResult();
}
- var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
+ using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false))
{
TranscodingJob? job;
if (!File.Exists(outputPath))
@@ -117,9 +115,5 @@ public static class FileStreamResponseHelpers
var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType);
}
- finally
- {
- transcodingLock.Release();
- }
}
}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 0ed1578c70..7c4155bfc6 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,6 +26,7 @@
+
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index b960feb7f3..f6854157a6 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -37,7 +38,7 @@ public class TrickplayManager : ITrickplayManager
private readonly IDbContextFactory _dbProvider;
private readonly IApplicationPaths _appPaths;
- private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
///
@@ -107,93 +108,92 @@ public class TrickplayManager : ITrickplayManager
var imgTempDir = string.Empty;
var outputDir = GetTrickplayDirectory(video, width);
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
- if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
- {
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
- return;
- }
-
- // Extract images
- // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
- var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
-
- if (mediaSource is null)
- {
- _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
- return;
- }
-
- var mediaPath = mediaSource.Path;
- var mediaStream = mediaSource.VideoStream;
- var container = mediaSource.Container;
-
- _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
- imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
- mediaPath,
- container,
- mediaSource,
- mediaStream,
- width,
- TimeSpan.FromMilliseconds(options.Interval),
- options.EnableHwAcceleration,
- options.ProcessThreads,
- options.Qscale,
- options.ProcessPriority,
- _encodingHelper,
- cancellationToken).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
- {
- throw new InvalidOperationException("Null or invalid directory from media encoder.");
- }
-
- var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
- .Select(i => i.FullName)
- .OrderBy(i => i)
- .ToList();
-
- // Create tiles
- var trickplayInfo = CreateTiles(images, width, options, outputDir);
-
- // Save tiles info
try
{
- if (trickplayInfo is not null)
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
{
- trickplayInfo.ItemId = video.Id;
- await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
-
- _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+ return;
}
- else
+
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+
+ if (mediaSource is null)
{
- throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
+
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
+
+ var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
+
+ // Create tiles
+ var trickplayInfo = CreateTiles(images, width, options, outputDir);
+
+ // Save tiles info
+ try
+ {
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
}
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error while saving trickplay tiles info.");
-
- // Make sure no files stay in metadata folders on failure
- // if tiles info wasn't saved.
- Directory.Delete(outputDir, true);
+ _logger.LogError(ex, "Error creating trickplay images.");
}
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating trickplay images.");
- }
- finally
- {
- _resourcePool.Release();
-
- if (!string.IsNullOrEmpty(imgTempDir))
+ finally
{
- Directory.Delete(imgTempDir, true);
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
}
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
index c19a12ae7a..09bc01f748 100644
--- a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
@@ -96,9 +96,10 @@ public interface ITranscodeManager
public void OnTranscodeEndRequest(TranscodingJob job);
///
- /// Gets the transcoding lock.
+ /// Transcoding lock.
///
/// The output path of the transcoded file.
- /// A .
- public SemaphoreSlim GetTranscodingLock(string outputPath);
+ /// The cancellation token.
+ /// An .
+ ValueTask LockAsync(string outputPath, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 299f294b29..ff91a60a79 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -22,7 +23,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
- public sealed class AttachmentExtractor : IAttachmentExtractor
+ public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
@@ -30,8 +31,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly ConcurrentDictionary _semaphoreLocks =
- new ConcurrentDictionary();
+ private readonly AsyncKeyedLocker _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public AttachmentExtractor(
ILogger logger,
@@ -84,11 +88,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!Directory.Exists(outputPath))
{
@@ -99,10 +99,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
public async Task ExtractAllAttachmentsExternal(
@@ -111,11 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(Path.Join(outputPath, id)))
{
@@ -131,10 +123,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractAllAttachmentsInternal(
@@ -256,11 +244,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
@@ -271,10 +255,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractAttachmentInternal(
@@ -379,5 +359,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
var prefix = filename.AsSpan(0, 1);
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
}
+
+ ///
+ public void Dispose()
+ {
+ _semaphoreLocks.Dispose();
+ }
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4dbefca4bb..f86d14fc86 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@@ -60,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
- private readonly SemaphoreSlim _thumbnailResourcePool;
+ private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
private readonly object _runningProcessesLock = new object();
private readonly List _runningProcesses = new List();
@@ -116,7 +117,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
var semaphoreCount = 2 * Environment.ProcessorCount;
- _thumbnailResourcePool = new SemaphoreSlim(semaphoreCount, semaphoreCount);
+ _thumbnailResourcePool = new(semaphoreCount);
}
///
@@ -754,8 +755,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion;
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -776,10 +776,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
ranToCompletion = false;
}
}
- finally
- {
- _thumbnailResourcePool.Release();
- }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);
@@ -908,8 +904,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion = false;
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -963,10 +958,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
StopProcess(processWrapper, 1000);
}
}
- finally
- {
- _thumbnailResourcePool.Release();
- }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a4e8194c15..be63513a72 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -1,4 +1,4 @@
-
+
@@ -22,6 +22,7 @@
+
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 8fd1f9fc1e..8f1cc3f645 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -12,6 +11,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -28,7 +28,7 @@ using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public sealed class SubtitleEncoder : ISubtitleEncoder
+ public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
@@ -41,8 +41,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
///
/// The _semaphoreLocks.
///
- private readonly ConcurrentDictionary _semaphoreLocks =
- new ConcurrentDictionary();
+ private readonly AsyncKeyedLocker _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public SubtitleEncoder(
ILogger logger,
@@ -293,16 +296,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentException("Unsupported format: " + format);
}
- ///
- /// Gets the lock.
- ///
- /// The filename.
- /// System.Object.
- private SemaphoreSlim GetLock(string filename)
- {
- return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
- }
-
///
/// Converts the text subtitle to SRT.
///
@@ -313,21 +306,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// Task.
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{
- var semaphore = GetLock(outputPath);
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
///
@@ -472,7 +457,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// Task.
private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
{
- var semaphores = new List();
+ var locks = new List>();
var extractableStreams = new List();
try
@@ -484,16 +469,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
- var semaphore = GetLock(outputPath);
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ var @lock = _semaphoreLocks.GetOrAdd(outputPath);
+ await @lock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
if (File.Exists(outputPath))
{
- semaphore.Release();
+ @lock.Dispose();
continue;
}
- semaphores.Add(semaphore);
+ locks.Add(@lock);
extractableStreams.Add(subtitleStream);
}
@@ -508,9 +493,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
finally
{
- foreach (var semaphore in semaphores)
+ foreach (var @lock in locks)
{
- semaphore.Release();
+ @lock.Dispose();
}
}
}
@@ -657,16 +642,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = GetLock(outputPath);
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
+ var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
if (subtitleStream.IsExternal)
@@ -682,10 +663,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractTextSubtitleInternal(
@@ -901,6 +878,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
+ ///
+ public void Dispose()
+ {
+ _semaphoreLocks.Dispose();
+ }
+
#pragma warning disable CA1034 // Nested types should not be visible
// Only public for the unit tests
public readonly record struct SubtitleInfo
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index d79e4441aa..ab3eb3298b 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -4,10 +4,12 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
@@ -43,7 +45,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private readonly IAttachmentExtractor _attachmentExtractor;
private readonly List _activeTranscodingJobs = new();
- private readonly Dictionary _transcodingLocks = new();
+ private readonly AsyncKeyedLocker _transcodingLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
///
/// Initializes a new instance of the class.
@@ -224,11 +230,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
-
job.Stop();
if (delete(job.Path!))
@@ -625,11 +626,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(path);
- }
-
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
@@ -705,21 +701,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- ///
- public SemaphoreSlim GetTranscodingLock(string outputPath)
- {
- lock (_transcodingLocks)
- {
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
- {
- result = new SemaphoreSlim(1, 1);
- _transcodingLocks[outputPath] = result;
- }
-
- return result;
- }
- }
-
private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@@ -742,10 +723,23 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
+ ///
+ /// Transcoding lock.
+ ///
+ /// The output path of the transcoded file.
+ /// The cancellation token.
+ /// An .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ValueTask LockAsync(string outputPath, CancellationToken cancellationToken)
+ {
+ return _transcodingLocks.LockAsync(outputPath, cancellationToken);
+ }
+
///
public void Dispose()
{
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
_sessionManager.PlaybackStart -= OnPlaybackProgress;
+ _transcodingLocks.Dispose();
}
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 65a8f4e832..213328a39f 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -38,7 +39,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
- private readonly SemaphoreSlim _parallelEncodingLimit;
+ private readonly AsyncNonKeyedLocker _parallelEncodingLimit;
private bool _disposed;
@@ -68,7 +69,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
semaphoreCount = 2 * Environment.ProcessorCount;
}
- _parallelEncodingLimit = new(semaphoreCount, semaphoreCount);
+ _parallelEncodingLimit = new(semaphoreCount);
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
@@ -193,18 +194,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
{
if (!File.Exists(cacheFilePath))
{
- // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
- await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false);
-
string resultPath;
- try
+
+ // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
+ using (await _parallelEncodingLimit.LockAsync().ConfigureAwait(false))
{
resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
}
- finally
- {
- _parallelEncodingLimit.Release();
- }
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 23c4c0a9a4..4a02f90f95 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -21,4 +21,8 @@
+
+
+
+
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index bc968f8ee1..a7de5c65b1 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -50,7 +51,7 @@ namespace Jellyfin.LiveTv.Channels
private readonly IFileSystem _fileSystem;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
- private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _resourcePool = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _disposed = false;
@@ -811,9 +812,7 @@ namespace Jellyfin.LiveTv.Channels
{
}
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
try
{
@@ -860,10 +859,6 @@ namespace Jellyfin.LiveTv.Channels
return result;
}
- finally
- {
- _resourcePool.Release();
- }
}
private async Task CacheResponse(ChannelItemResult result, string path)
diff --git a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
index e7e927b2d0..a9a0db057b 100644
--- a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
@@ -14,6 +14,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using AsyncKeyedLock;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
@@ -68,7 +69,7 @@ namespace Jellyfin.LiveTv.EmbyTV
private readonly ConcurrentDictionary _epgChannels =
new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new(1);
private bool _disposed;
@@ -1444,9 +1445,7 @@ namespace Jellyfin.LiveTv.EmbyTV
return;
}
- await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
-
- try
+ using (await _recordingDeleteSemaphore.LockAsync().ConfigureAwait(false))
{
if (_disposed)
{
@@ -1499,10 +1498,6 @@ namespace Jellyfin.LiveTv.EmbyTV
}
}
}
- finally
- {
- _recordingDeleteSemaphore.Release();
- }
}
private void DeleteLibraryItemsForTimers(List timers)
diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
index 5a826a1da0..c58889740a 100644
--- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
+++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
true
@@ -11,6 +11,7 @@
+
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index 3b20cd160b..b237f5b166 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -16,6 +16,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
@@ -35,7 +36,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _tokenLock = new(1);
private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -573,27 +574,25 @@ namespace Jellyfin.LiveTv.Listings
}
}
- await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _tokenLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
- var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
- savedToken.Name = result;
- savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
- return result;
- }
- catch (HttpRequestException ex)
- {
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ try
{
- _tokens.Clear();
- _lastErrorResponse = DateTime.UtcNow;
+ var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
+ savedToken.Name = result;
+ savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
+ return result;
}
+ catch (HttpRequestException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ {
+ _tokens.Clear();
+ _lastErrorResponse = DateTime.UtcNow;
+ }
- throw;
- }
- finally
- {
- _tokenSemaphore.Release();
+ throw;
+ }
}
}
@@ -801,7 +800,7 @@ namespace Jellyfin.LiveTv.Listings
if (disposing)
{
- _tokenSemaphore?.Dispose();
+ _tokenLock?.Dispose();
}
_disposed = true;