mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Merge branch 'master' into trickplay
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
public sealed class AttachmentExtractor : IAttachmentExtractor
|
||||
{
|
||||
private readonly ILogger<AttachmentExtractor> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
@@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
IApplicationPaths appPaths,
|
||||
@@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
process.Start();
|
||||
|
||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
process.Start();
|
||||
|
||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var prefix = filename.Substring(0, 1);
|
||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
var prefix = filename.AsSpan(0, 1);
|
||||
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo;
|
||||
/// </summary>
|
||||
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
|
||||
{
|
||||
private FileSystemMetadata _impl;
|
||||
private readonly FileSystemMetadata _impl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
public class EncoderValidator
|
||||
public partial class EncoderValidator
|
||||
{
|
||||
private static readonly string[] _requiredDecoders = new[]
|
||||
{
|
||||
@@ -165,6 +165,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
public static Version? MaxVersion { get; } = null;
|
||||
|
||||
[GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")]
|
||||
private static partial Regex FfmpegVersionRegex();
|
||||
|
||||
[GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)]
|
||||
private static partial Regex LibraryRegex();
|
||||
|
||||
public bool ValidateVersion()
|
||||
{
|
||||
string output;
|
||||
@@ -283,7 +289,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
internal Version? GetFFmpegVersionInternal(string output)
|
||||
{
|
||||
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
|
||||
var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
|
||||
var match = FfmpegVersionRegex().Match(output);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
@@ -331,10 +337,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
var map = new Dictionary<string, Version>();
|
||||
|
||||
foreach (Match match in Regex.Matches(
|
||||
output,
|
||||
@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
|
||||
RegexOptions.Multiline))
|
||||
foreach (Match match in LibraryRegex().Matches(output))
|
||||
{
|
||||
var version = new Version(
|
||||
int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
|
||||
@@ -496,8 +499,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
|
||||
|
||||
var found = Regex
|
||||
.Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline)
|
||||
var found = CodecRegex()
|
||||
.Matches(output)
|
||||
.Select(x => x.Groups["codec"].Value)
|
||||
.Where(x => required.Contains(x));
|
||||
|
||||
@@ -524,8 +527,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
var found = Regex
|
||||
.Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
|
||||
var found = FilterRegex()
|
||||
.Matches(output)
|
||||
.Select(x => x.Groups["filter"].Value)
|
||||
.Where(x => _requiredFilters.Contains(x));
|
||||
|
||||
@@ -550,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
||||
{
|
||||
using (var process = new Process()
|
||||
var redirectStandardIn = !string.IsNullOrEmpty(testKey);
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo(path, arguments)
|
||||
{
|
||||
@@ -558,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
UseShellExecute = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
RedirectStandardInput = !string.IsNullOrEmpty(testKey),
|
||||
RedirectStandardInput = redirectStandardIn,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
@@ -568,13 +572,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
process.Start();
|
||||
|
||||
if (!string.IsNullOrEmpty(testKey))
|
||||
if (redirectStandardIn)
|
||||
{
|
||||
process.StandardInput.Write(testKey);
|
||||
using var writer = process.StandardInput;
|
||||
writer.Write(testKey);
|
||||
}
|
||||
|
||||
return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
|
||||
using var reader = readStdErr ? process.StandardError : process.StandardOutput;
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex CodecRegex();
|
||||
|
||||
[GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex FilterRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// <summary>
|
||||
/// Class MediaEncoder.
|
||||
/// </summary>
|
||||
public class MediaEncoder : IMediaEncoder, IDisposable
|
||||
public partial class MediaEncoder : IMediaEncoder, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The default SDR image extraction timeout in milliseconds.
|
||||
@@ -79,12 +79,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private bool _isVaapiDeviceAmd = false;
|
||||
private bool _isVaapiDeviceInteliHD = false;
|
||||
private bool _isVaapiDeviceInteli965 = false;
|
||||
private bool _isVaapiDeviceSupportVulkanFmtModifier = false;
|
||||
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
|
||||
|
||||
private static string[] _vulkanFmtModifierExts =
|
||||
private static string[] _vulkanExternalMemoryDmaBufExts =
|
||||
{
|
||||
"VK_KHR_sampler_ycbcr_conversion",
|
||||
"VK_EXT_image_drm_format_modifier",
|
||||
"VK_KHR_external_memory_fd",
|
||||
"VK_EXT_external_memory_dma_buf",
|
||||
"VK_KHR_external_semaphore_fd",
|
||||
@@ -143,7 +141,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
|
||||
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
|
||||
|
||||
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
|
||||
private static partial Regex FfprobePathRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Run at startup or if the user removes a Custom path from transcode page.
|
||||
@@ -179,7 +180,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
if (_ffmpegPath is not null)
|
||||
{
|
||||
// Determine a probe path from the mpeg path
|
||||
_ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
|
||||
_ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
|
||||
|
||||
// Interrogate to understand what coders are supported
|
||||
var validator = new EncoderValidator(_logger, _ffmpegPath);
|
||||
@@ -204,7 +205,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
|
||||
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
|
||||
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
|
||||
_isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts);
|
||||
_isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts);
|
||||
|
||||
if (_isVaapiDeviceAmd)
|
||||
{
|
||||
@@ -219,9 +220,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
|
||||
}
|
||||
|
||||
if (_isVaapiDeviceSupportVulkanFmtModifier)
|
||||
if (_isVaapiDeviceSupportVulkanDrmInterop)
|
||||
{
|
||||
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice);
|
||||
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,10 +319,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
var files = _fileSystem.GetFilePaths(path, recursive);
|
||||
|
||||
var excludeExtensions = new[] { ".c" };
|
||||
|
||||
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -419,24 +418,36 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||
string analyzeDuration = string.Empty;
|
||||
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
var analyzeDuration = string.Empty;
|
||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
|
||||
var extraArgs = string.Empty;
|
||||
|
||||
if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
|
||||
analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDuration))
|
||||
{
|
||||
extraArgs = analyzeDuration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
extraArgs += " -probesize " + ffmpegProbeSize;
|
||||
}
|
||||
|
||||
return GetMediaInfoInternal(
|
||||
GetInputArgument(request.MediaSource.Path, request.MediaSource),
|
||||
request.MediaSource.Path,
|
||||
request.MediaSource.Protocol,
|
||||
extractChapters,
|
||||
analyzeDuration,
|
||||
extraArgs,
|
||||
request.MediaType == DlnaProfileType.Audio,
|
||||
request.MediaSource.VideoType,
|
||||
cancellationToken);
|
||||
@@ -513,7 +524,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||
using var reader = process.StandardOutput;
|
||||
await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
InternalMediaInfoResult result;
|
||||
try
|
||||
@@ -614,9 +626,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private string GetImageResolutionParameter()
|
||||
{
|
||||
string imageResolutionParameter;
|
||||
|
||||
imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
|
||||
var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
|
||||
{
|
||||
ImageResolution.P144 => "256x144",
|
||||
ImageResolution.P240 => "426x240",
|
||||
@@ -641,15 +651,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
var outputExtension = targetFormat switch
|
||||
{
|
||||
ImageFormat.Bmp => ".bmp",
|
||||
ImageFormat.Gif => ".gif",
|
||||
ImageFormat.Jpg => ".jpg",
|
||||
ImageFormat.Png => ".png",
|
||||
ImageFormat.Webp => ".webp",
|
||||
_ => ".jpg"
|
||||
};
|
||||
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
|
||||
|
||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||
@@ -669,13 +671,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var scaler = threedFormat switch
|
||||
{
|
||||
// hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
|
||||
Video3DFormat.HalfSideBySide => "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
|
||||
Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made
|
||||
Video3DFormat.FullSideBySide => "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
|
||||
Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
// htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made
|
||||
Video3DFormat.HalfTopAndBottom => "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
|
||||
Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
// ftab crop height in half, set the display aspect,crop out any black bars we may have made
|
||||
Video3DFormat.FullTopAndBottom => "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
|
||||
Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
|
||||
_ => "scale=trunc(iw*sar):ih"
|
||||
};
|
||||
|
||||
@@ -751,11 +753,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
StopProcess(processWrapper, 1000);
|
||||
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||
ranToCompletion = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
ranToCompletion = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -1024,7 +1030,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
// https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
|
||||
// We need to double escape
|
||||
|
||||
return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", "'\\\\\\''", StringComparison.Ordinal);
|
||||
return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1167,7 +1173,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return true;
|
||||
}
|
||||
|
||||
private class ProcessWrapper : IDisposable
|
||||
private sealed class ProcessWrapper : IDisposable
|
||||
{
|
||||
private readonly MediaEncoder _mediaEncoder;
|
||||
|
||||
@@ -1210,13 +1216,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_mediaEncoder._runningProcesses.Remove(this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -20,7 +19,10 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
public class ProbeResultNormalizer
|
||||
/// <summary>
|
||||
/// Class responsible for normalizing FFprobe output.
|
||||
/// </summary>
|
||||
public partial class ProbeResultNormalizer
|
||||
{
|
||||
// When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
|
||||
private const int MaxSubtitleDescriptionExtractionLength = 100;
|
||||
@@ -29,13 +31,16 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
|
||||
|
||||
private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)");
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
private string[] _splitWhiteList;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProbeResultNormalizer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger{ProbeResultNormalizer}"/> for use with the <see cref="ProbeResultNormalizer"/> instance.</param>
|
||||
/// <param name="localization">The <see cref="ILocalizationManager"/> for use with the <see cref="ProbeResultNormalizer"/> instance.</param>
|
||||
public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -71,8 +76,18 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
"She/Her/Hers",
|
||||
"5/8erl in Ehr'n",
|
||||
"Smith/Kotzen",
|
||||
"We;Na",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a FFprobe response into its <see cref="MediaInfo"/> equivalent.
|
||||
/// </summary>
|
||||
/// <param name="data">The <see cref="InternalMediaInfoResult"/>.</param>
|
||||
/// <param name="videoType">The <see cref="VideoType"/>.</param>
|
||||
/// <param name="isAudio">A boolean indicating whether the media is audio.</param>
|
||||
/// <param name="path">Path to media file.</param>
|
||||
/// <param name="protocol">Path media protocol.</param>
|
||||
/// <returns>The <see cref="MediaInfo"/>.</returns>
|
||||
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
|
||||
{
|
||||
var info = new MediaInfo
|
||||
@@ -252,25 +267,30 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle MPEG-1 container
|
||||
if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase))
|
||||
// Input can be a list of multiple, comma-delimited formats - each of them needs to be checked
|
||||
var splitFormat = format.Split(',');
|
||||
for (var i = 0; i < splitFormat.Length; i++)
|
||||
{
|
||||
return "mpeg";
|
||||
// Handle MPEG-1 container
|
||||
if (string.Equals(splitFormat[i], "mpegvideo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
splitFormat[i] = "mpeg";
|
||||
}
|
||||
|
||||
// Handle MPEG-2 container
|
||||
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
splitFormat[i] = "ts";
|
||||
}
|
||||
|
||||
// Handle matroska container
|
||||
else if (string.Equals(splitFormat[i], "matroska", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
splitFormat[i] = "mkv";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MPEG-2 container
|
||||
if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "ts";
|
||||
}
|
||||
|
||||
// Handle matroska container
|
||||
if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "mkv";
|
||||
}
|
||||
|
||||
return format;
|
||||
return string.Join(',', splitFormat);
|
||||
}
|
||||
|
||||
private int? GetEstimatedAudioBitrate(string codec, int? channels)
|
||||
@@ -741,9 +761,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isAudio
|
||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
|
||||
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
stream.Type = MediaStreamType.EmbeddedImage;
|
||||
}
|
||||
@@ -1191,7 +1213,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
foreach (var person in Split(performer, false))
|
||||
{
|
||||
Match match = _performerPattern.Match(person);
|
||||
Match match = PerformerRegex().Match(person);
|
||||
|
||||
// If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it.
|
||||
if (match.Success)
|
||||
@@ -1630,5 +1652,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
return TransportStreamTimestamp.Valid;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")]
|
||||
private static partial Regex PerformerRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// ASS subtitle writer.
|
||||
/// </summary>
|
||||
public class AssWriter : ISubtitleWriter
|
||||
public partial class AssWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||
var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
|
||||
@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// SRT subtitle writer.
|
||||
/// </summary>
|
||||
public class SrtWriter : ISubtitleWriter
|
||||
public partial class SrtWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineEscapedRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -35,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var text = trackEvent.Text;
|
||||
|
||||
// TODO: Not sure how to handle these
|
||||
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
|
||||
text = NewLineEscapedRegex().Replace(text, " ");
|
||||
|
||||
writer.WriteLine(text);
|
||||
writer.WriteLine();
|
||||
|
||||
@@ -11,8 +11,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// SSA subtitle writer.
|
||||
/// </summary>
|
||||
public class SsaWriter : ISubtitleWriter
|
||||
public partial class SsaWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -40,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||
var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
|
||||
@@ -293,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new VttWriter();
|
||||
return true;
|
||||
@@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg subtitle conversion process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle conversion process");
|
||||
}
|
||||
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg subtitle extraction process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
|
||||
@@ -9,8 +9,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// TTML subtitle writer.
|
||||
/// </summary>
|
||||
public class TtmlWriter : ISubtitleWriter
|
||||
public partial class TtmlWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineEscapeRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -38,7 +41,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
var text = trackEvent.Text;
|
||||
|
||||
text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase);
|
||||
text = NewLineEscapeRegex().Replace(text, "<br/>");
|
||||
|
||||
writer.WriteLine(
|
||||
"<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
|
||||
|
||||
@@ -10,8 +10,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// Subtitle writer for the WebVTT format.
|
||||
/// </summary>
|
||||
public class VttWriter : ISubtitleWriter
|
||||
public partial class VttWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewlineEscapeRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -39,7 +42,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var text = trackEvent.Text;
|
||||
|
||||
// TODO: Not sure how to handle these
|
||||
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
|
||||
text = NewlineEscapeRegex().Replace(text, " ");
|
||||
|
||||
writer.WriteLine(text);
|
||||
writer.WriteLine();
|
||||
|
||||
Reference in New Issue
Block a user