AsyncKeyedLock migration

This commit is contained in:
Mark Cilia Vincenti 2024-01-03 16:47:25 +01:00
parent 9fb6742400
commit d1677dc680
8 changed files with 125 additions and 185 deletions

View File

@ -4,6 +4,7 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="6.2.6" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />

View File

@ -294,9 +294,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!System.IO.File.Exists(playlistPath)) if (!System.IO.File.Exists(playlistPath))
{ {
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath); using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{ {
if (!System.IO.File.Exists(playlistPath)) if (!System.IO.File.Exists(playlistPath))
{ {
@ -326,10 +324,6 @@ public class DynamicHlsController : BaseJellyfinApiController
} }
} }
} }
finally
{
transcodingLock.Release();
}
} }
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); 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); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
} }
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath); using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
try
{ {
var startTranscoding = false;
if (System.IO.File.Exists(segmentPath)) if (System.IO.File.Exists(segmentPath))
{ {
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); 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 else
{ {
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; if (job?.TranscodingThrottler is not null)
if (segmentId == -1)
{ {
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested"); await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
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
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler is not null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
}
}
}
}
finally
{
if (!released)
{
transcodingLock.Release();
} }
} }

View File

@ -93,9 +93,7 @@ public static class FileStreamResponseHelpers
return new OkResult(); return new OkResult();
} }
var transcodingLock = transcodeManager.GetTranscodingLock(outputPath); using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false))
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{ {
TranscodingJob? job; TranscodingJob? job;
if (!File.Exists(outputPath)) if (!File.Exists(outputPath))
@ -117,9 +115,5 @@ public static class FileStreamResponseHelpers
var stream = new ProgressiveFileStream(outputPath, job, transcodeManager); var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType); return new FileStreamResult(stream, contentType);
} }
finally
{
transcodingLock.Release();
}
} }
} }

View File

@ -96,9 +96,10 @@ public interface ITranscodeManager
public void OnTranscodeEndRequest(TranscodingJob job); public void OnTranscodeEndRequest(TranscodingJob job);
/// <summary> /// <summary>
/// Gets the transcoding lock. /// Transcoding lock.
/// </summary> /// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param> /// <param name="outputPath">The output path of the transcoded file.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="SemaphoreSlim"/>.</returns> /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
public SemaphoreSlim GetTranscodingLock(string outputPath); ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken);
} }

View File

@ -8,6 +8,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncKeyedLock;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -22,7 +23,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments namespace MediaBrowser.MediaEncoding.Attachments
{ {
public sealed class AttachmentExtractor : IAttachmentExtractor public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{ {
private readonly ILogger<AttachmentExtractor> _logger; private readonly ILogger<AttachmentExtractor> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
@ -30,8 +31,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
new ConcurrentDictionary<string, SemaphoreSlim>(); {
o.PoolSize = 20;
o.PoolInitialFill = 1;
});
public AttachmentExtractor( public AttachmentExtractor(
ILogger<AttachmentExtractor> logger, ILogger<AttachmentExtractor> logger,
@ -84,11 +88,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{ {
if (!Directory.Exists(outputPath)) if (!Directory.Exists(outputPath))
{ {
@ -99,10 +99,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
} }
finally
{
semaphore.Release();
}
} }
public async Task ExtractAllAttachmentsExternal( public async Task ExtractAllAttachmentsExternal(
@ -111,11 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{ {
if (!File.Exists(Path.Join(outputPath, id))) if (!File.Exists(Path.Join(outputPath, id)))
{ {
@ -131,10 +123,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
} }
} }
} }
finally
{
semaphore.Release();
}
} }
private async Task ExtractAllAttachmentsInternal( private async Task ExtractAllAttachmentsInternal(
@ -256,11 +244,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1)); using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{ {
if (!File.Exists(outputPath)) if (!File.Exists(outputPath))
{ {
@ -271,10 +255,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
} }
finally
{
semaphore.Release();
}
} }
private async Task ExtractAttachmentInternal( private async Task ExtractAttachmentInternal(
@ -379,5 +359,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
var prefix = filename.AsSpan(0, 1); var prefix = filename.AsSpan(0, 1);
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename); return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
} }
/// <inheritdoc />
public void Dispose()
{
_semaphoreLocks.Dispose();
}
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup> <PropertyGroup>
@ -22,6 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="BDInfo" /> <PackageReference Include="BDInfo" />
<PackageReference Include="libse" /> <PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Microsoft.Extensions.Http" />

View File

@ -1,7 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
@ -11,6 +10,7 @@ using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncKeyedLock;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -18,6 +18,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -27,7 +28,7 @@ using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Subtitles namespace MediaBrowser.MediaEncoding.Subtitles
{ {
public sealed class SubtitleEncoder : ISubtitleEncoder public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{ {
private readonly ILogger<SubtitleEncoder> _logger; private readonly ILogger<SubtitleEncoder> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
@ -40,8 +41,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary> /// <summary>
/// The _semaphoreLocks. /// The _semaphoreLocks.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
new ConcurrentDictionary<string, SemaphoreSlim>(); {
o.PoolSize = 20;
o.PoolInitialFill = 1;
});
public SubtitleEncoder( public SubtitleEncoder(
ILogger<SubtitleEncoder> logger, ILogger<SubtitleEncoder> logger,
@ -317,16 +321,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentException("Unsupported format: " + format); throw new ArgumentException("Unsupported format: " + format);
} }
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string filename)
{
return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
}
/// <summary> /// <summary>
/// Converts the text subtitle to SRT. /// Converts the text subtitle to SRT.
/// </summary> /// </summary>
@ -337,21 +331,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <returns>Task.</returns> /// <returns>Task.</returns>
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{ {
var semaphore = GetLock(outputPath); using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{ {
if (!File.Exists(outputPath)) if (!File.Exists(outputPath))
{ {
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
} }
} }
finally
{
semaphore.Release();
}
} }
/// <summary> /// <summary>
@ -484,16 +470,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputPath, string outputPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var semaphore = GetLock(outputPath); using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
try
{ {
if (!File.Exists(outputPath)) if (!File.Exists(outputPath))
{ {
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
if (subtitleStream.IsExternal) if (subtitleStream.IsExternal)
@ -509,10 +491,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
} }
finally
{
semaphore.Release();
}
} }
private async Task ExtractTextSubtitleInternal( private async Task ExtractTextSubtitleInternal(
@ -728,6 +706,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
} }
} }
/// <inheritdoc />
public void Dispose()
{
_semaphoreLocks.Dispose();
}
#pragma warning disable CA1034 // Nested types should not be visible #pragma warning disable CA1034 // Nested types should not be visible
// Only public for the unit tests // Only public for the unit tests
public readonly record struct SubtitleInfo public readonly record struct SubtitleInfo

View File

@ -4,10 +4,12 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -42,7 +44,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private readonly IAttachmentExtractor _attachmentExtractor; private readonly IAttachmentExtractor _attachmentExtractor;
private readonly List<TranscodingJob> _activeTranscodingJobs = new(); private readonly List<TranscodingJob> _activeTranscodingJobs = new();
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new(); private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
{
o.PoolSize = 20;
o.PoolInitialFill = 1;
});
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class. /// Initializes a new instance of the <see cref="TranscodeManager"/> class.
@ -223,11 +229,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
} }
} }
lock (_transcodingLocks)
{
_transcodingLocks.Remove(job.Path!);
}
job.Stop(); job.Stop();
if (delete(job.Path!)) if (delete(job.Path!))
@ -624,11 +625,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
} }
} }
lock (_transcodingLocks)
{
_transcodingLocks.Remove(path);
}
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{ {
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId); _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
@ -704,21 +700,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
} }
} }
/// <inheritdoc />
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) private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{ {
if (!string.IsNullOrWhiteSpace(e.PlaySessionId)) if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@ -741,10 +722,23 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
} }
} }
/// <summary>
/// Transcoding lock.
/// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="SemaphoreSlim"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
{
return _transcodingLocks.LockAsync(outputPath, cancellationToken);
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_sessionManager.PlaybackProgress -= OnPlaybackProgress; _sessionManager.PlaybackProgress -= OnPlaybackProgress;
_sessionManager.PlaybackStart -= OnPlaybackProgress; _sessionManager.PlaybackStart -= OnPlaybackProgress;
_transcodingLocks.Dispose();
} }
} }