aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2024-02-28 17:09:23 -0700
committerGitHub <noreply@github.com>2024-02-28 17:09:23 -0700
commitf3c333f4d5bb272b5ffcff29af337ca31e8c374b (patch)
tree008525e157be39a25e013fd3b039d4680760eb68 /MediaBrowser.MediaEncoding
parent54eb81395ef8d3d4cb064b56361ce94fc72b38b5 (diff)
parent4f0f364ac941dc4a856512c9bf0e6b93fdf7b3ab (diff)
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
Diffstat (limited to 'MediaBrowser.MediaEncoding')
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs46
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs21
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs65
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs23
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs280
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs59
7 files changed, 330 insertions, 167 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 299f294b2..ff91a60a7 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<AttachmentExtractor> _logger;
private readonly IApplicationPaths _appPaths;
@@ -30,8 +31,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
- new ConcurrentDictionary<string, SemaphoreSlim>();
+ private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public AttachmentExtractor(
ILogger<AttachmentExtractor> 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);
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _semaphoreLocks.Dispose();
+ }
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 0d1d27ae8..fdca28390 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -45,7 +45,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg4_cuvid",
"vp8_cuvid",
"vp9_cuvid",
- "av1_cuvid"
+ "av1_cuvid",
+ "h264_rkmpp",
+ "hevc_rkmpp",
+ "mpeg1_rkmpp",
+ "mpeg2_rkmpp",
+ "mpeg4_rkmpp",
+ "vp8_rkmpp",
+ "vp9_rkmpp",
+ "av1_rkmpp"
};
private static readonly string[] _requiredEncoders = new[]
@@ -82,7 +90,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
"av1_vaapi",
"h264_v4l2m2m",
"h264_videotoolbox",
- "hevc_videotoolbox"
+ "hevc_videotoolbox",
+ "h264_rkmpp",
+ "hevc_rkmpp"
};
private static readonly string[] _requiredFilters = new[]
@@ -116,9 +126,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libplacebo",
"scale_vulkan",
"overlay_vulkan",
- "hwupload_vaapi",
// videotoolbox
- "yadif_videotoolbox"
+ "yadif_videotoolbox",
+ // rkrga
+ "scale_rkrga",
+ "vpp_rkrga",
+ "overlay_rkrga"
};
private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4dbefca4b..cc6971c1b 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<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
@@ -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);
}
/// <inheritdoc />
@@ -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;
@@ -1120,6 +1111,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return allVobs
.Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()))
.Select(i => i.FullName)
+ .Order()
.ToList();
}
@@ -1136,6 +1128,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return directoryFiles
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
.Select(f => f.FullName)
+ .Order()
.ToList();
}
@@ -1159,31 +1152,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Generate concat configuration entries for each file and write to file
- using (StreamWriter sw = new StreamWriter(concatFilePath))
+ using StreamWriter sw = new StreamWriter(concatFilePath);
+ foreach (var path in files)
{
- foreach (var path in files)
- {
- var mediaInfoResult = GetMediaInfo(
- new MediaInfoRequest
+ var mediaInfoResult = GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Video,
+ MediaSource = new MediaSourceInfo
{
- MediaType = DlnaProfileType.Video,
- MediaSource = new MediaSourceInfo
- {
- Path = path,
- Protocol = MediaProtocol.File,
- VideoType = videoType
- }
- },
- CancellationToken.None).GetAwaiter().GetResult();
-
- var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
-
- // Add file path stanza to concat configuration
- sw.WriteLine("file '{0}'", path);
-
- // Add duration stanza to concat configuration
- sw.WriteLine("duration {0}", duration);
- }
+ Path = path,
+ Protocol = MediaProtocol.File,
+ VideoType = videoType
+ }
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
+
+ var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
+
+ // Add file path stanza to concat configuration
+ sw.WriteLine("file '{0}'", path);
+
+ // Add duration stanza to concat configuration
+ sw.WriteLine("duration {0}", duration);
}
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a4e8194c1..be63513a7 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="BDInfo" />
<PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 629c30060..317aba418 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -30,6 +30,8 @@ namespace MediaBrowser.MediaEncoding.Probing
private const string ArtistReplaceValue = " | ";
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
+ private readonly string[] _webmVideoCodecs = { "av1", "vp8", "vp9" };
+ private readonly string[] _webmAudioCodecs = { "opus", "vorbis" };
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -114,7 +116,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (data.Format is not null)
{
- info.Container = NormalizeFormat(data.Format.FormatName);
+ info.Container = NormalizeFormat(data.Format.FormatName, info.MediaStreams);
if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value))
{
@@ -260,7 +262,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return info;
}
- private string NormalizeFormat(string format)
+ private string NormalizeFormat(string format, IReadOnlyList<MediaStream> mediaStreams)
{
if (string.IsNullOrWhiteSpace(format))
{
@@ -288,9 +290,20 @@ namespace MediaBrowser.MediaEncoding.Probing
{
splitFormat[i] = "mkv";
}
+
+ // Handle WebM
+ else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
+ {
+ // Limit WebM to supported codecs
+ if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
+ || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
+ {
+ splitFormat[i] = string.Empty;
+ }
+ }
}
- return string.Join(',', splitFormat);
+ return string.Join(',', splitFormat.Where(s => !string.IsNullOrEmpty(s)));
}
private int? GetEstimatedAudioBitrate(string codec, int? channels)
@@ -742,6 +755,10 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ // Graphical subtitle may have width and height info
+ stream.Width = streamInfo.Width;
+ stream.Height = streamInfo.Height;
+
if (string.IsNullOrEmpty(stream.Title))
{
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 459d854bf..4b1b1bbc6 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -11,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;
@@ -27,7 +28,7 @@ using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public sealed class SubtitleEncoder : ISubtitleEncoder
+ public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{
private readonly ILogger<SubtitleEncoder> _logger;
private readonly IApplicationPaths _appPaths;
@@ -40,8 +41,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary>
/// The _semaphoreLocks.
/// </summary>
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
- new ConcurrentDictionary<string, SemaphoreSlim>();
+ private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public SubtitleEncoder(
ILogger<SubtitleEncoder> logger,
@@ -194,36 +198,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- string outputFormat;
- string outputCodec;
+ await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
- if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
- || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
- || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
- {
- // Extract
- outputCodec = "copy";
- outputFormat = subtitleStream.Codec;
- }
- else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase))
- {
- // Extract
- outputCodec = "copy";
- outputFormat = "srt";
- }
- else
- {
- // Extract
- outputCodec = "srt";
- outputFormat = "srt";
- }
-
- // Extract
+ var outputFormat = GetTextSubtitleFormat(subtitleStream);
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
- await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
- .ConfigureAwait(false);
-
return new SubtitleInfo()
{
Path = outputPath,
@@ -318,16 +297,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
/// <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>
/// Converts the text subtitle to SRT.
/// </summary>
/// <param name="subtitleStream">The subtitle stream.</param>
@@ -337,21 +306,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <returns>Task.</returns>
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();
- }
}
/// <summary>
@@ -467,6 +428,203 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
}
+ private string GetTextSubtitleFormat(MediaStream subtitleStream)
+ {
+ if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase))
+ {
+ return subtitleStream.Codec;
+ }
+ else
+ {
+ return "srt";
+ }
+ }
+
+ private bool IsCodecCopyable(string codec)
+ {
+ return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Extracts all text subtitles.
+ /// </summary>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var locks = new List<AsyncKeyedLockReleaser<string>>();
+ var extractableStreams = new List<MediaStream>();
+
+ try
+ {
+ var subtitleStreams = mediaSource.MediaStreams
+ .Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream);
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
+
+ var @lock = _semaphoreLocks.GetOrAdd(outputPath);
+ await @lock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ if (File.Exists(outputPath))
+ {
+ @lock.Dispose();
+ continue;
+ }
+
+ locks.Add(@lock);
+ extractableStreams.Add(subtitleStream);
+ }
+
+ if (extractableStreams.Count > 0)
+ {
+ await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
+ }
+ finally
+ {
+ foreach (var @lock in locks)
+ {
+ @lock.Dispose();
+ }
+ }
+ }
+
+ private async Task ExtractAllTextSubtitlesInternal(
+ MediaSourceInfo mediaSource,
+ List<MediaStream> subtitleStreams,
+ CancellationToken cancellationToken)
+ {
+ var inputPath = mediaSource.Path;
+ var outputPaths = new List<string>();
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-i \"{0}\" -copyts",
+ inputPath);
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
+ var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
+ var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
+ if (streamIndex == -1)
+ {
+ _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
+ continue;
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
+
+ outputPaths.Add(outputPath);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0} -an -vn -c:s {1} \"{2}\"",
+ streamIndex,
+ outputCodec,
+ outputPath);
+ }
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
+ {
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting ffmpeg");
+
+ throw;
+ }
+
+ try
+ {
+ await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
+ }
+ }
+
+ var failed = false;
+
+ if (exitCode == -1)
+ {
+ failed = true;
+
+ foreach (var outputPath in outputPaths)
+ {
+ try
+ {
+ _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
+ _fileSystem.DeleteFile(outputPath);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
+ }
+ }
+ }
+ else
+ {
+ foreach (var outputPath in outputPaths)
+ {
+ if (!File.Exists(outputPath))
+ {
+ _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
+ failed = true;
+ continue;
+ }
+
+ if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
+ {
+ await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
+ }
+
+ _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
+ }
+ }
+
+ if (failed)
+ {
+ throw new FfmpegException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath));
+ }
+ }
+
/// <summary>
/// Extracts the text subtitle.
/// </summary>
@@ -484,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)
@@ -509,10 +663,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractTextSubtitleInternal(
@@ -530,7 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
+ "-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath,
subtitleStreamIndex,
outputCodec,
@@ -728,6 +878,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
+ /// <inheritdoc />
+ 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 d79e4441a..8bace15c6 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<TranscodingJob> _activeTranscodingJobs = new();
- private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new();
+ private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
/// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
@@ -224,11 +230,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
-
job.Stop();
if (delete(job.Path!))
@@ -404,7 +405,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw new ArgumentException("User does not have access to video transcoding.");
}
@@ -416,7 +417,12 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
- if (state.VideoType != VideoType.Dvd)
+ if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
+ {
+ var concatPath = Path.Join(_serverConfigurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
+ await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ else
{
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
@@ -451,7 +457,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
EnableRaisingEvents = true
};
- var transcodingJob = this.OnTranscodeBeginning(
+ var transcodingJob = OnTranscodeBeginning(
outputPath,
state.Request.PlaySessionId,
state.MediaSource.LiveStreamId,
@@ -506,7 +512,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
catch (Exception ex)
{
_logger.LogError(ex, "Error starting FFmpeg");
- this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
+ OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw;
}
@@ -625,11 +631,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 +706,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)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@@ -742,10 +728,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>An <see cref="IDisposable"/>.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
+ {
+ return _transcodingLocks.LockAsync(outputPath, cancellationToken);
+ }
+
/// <inheritdoc />
public void Dispose()
{
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
_sessionManager.PlaybackStart -= OnPlaybackProgress;
+ _transcodingLocks.Dispose();
}
}