aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding/Encoder
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder')
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs23
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs17
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs114
3 files changed, 112 insertions, 42 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index c8670c67cc..99c08eb9a1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
+using System.Threading.Tasks;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
@@ -185,16 +186,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ "libavdevice", new Version(58, 13) },
{ "libavfilter", new Version(7, 110) },
{ "libswscale", new Version(5, 9) },
- { "libswresample", new Version(3, 9) },
- { "libpostproc", new Version(55, 9) }
+ { "libswresample", new Version(3, 9) }
};
private readonly ILogger _logger;
private readonly string _encoderPath;
- private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
-
public EncoderValidator(ILogger logger, string encoderPath)
{
_logger = logger;
@@ -552,9 +550,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- // With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
- var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
- output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
+ // Start a dummy encode of 1x1@1fps. Send '?' to stdin to get the help/keybind text, followed by 'q' to stop the job immediately
+ // As a safeguard in case 'q' doesn't stop the job, the dummy input has a max duration of 5 (realtime) seconds
+ output = GetProcessOutput(_encoderPath, $"-hide_banner -re -f lavfi -i nullsrc=s=1x1:r=1:d=5 -f null -", true, "?q");
}
catch (Exception ex)
{
@@ -663,8 +661,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
writer.Write(testKey);
}
- using var reader = readStdErr ? process.StandardError : process.StandardOutput;
- return reader.ReadToEnd();
+ // Drain both streams concurrently to prevent pipe hanging, see #17429
+ using var standardOutput = process.StandardOutput;
+ using var standardError = process.StandardError;
+ var standardOutputTask = standardOutput.ReadToEndAsync();
+ var standardErrorTask = standardError.ReadToEndAsync();
+ process.WaitForExit();
+ Task.WaitAll(standardOutputTask, standardErrorTask);
+
+ return (readStdErr ? standardErrorTask : standardOutputTask).GetAwaiter().GetResult();
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index 2daeac7343..a525dcfa62 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -42,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
- var files = string.Join('|', inputFiles.Select(NormalizePath));
+ var files = string.Join('|', inputFiles.Select(f => f.EscapeProcessArgument()));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
@@ -64,21 +65,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path);
}
- // Quotes are valid path characters in linux and they need to be escaped here with a leading \
- path = NormalizePath(path);
+ path = path.EscapeProcessArgument();
return string.Format(CultureInfo.InvariantCulture, "{1}:\"{0}\"", path, inputPrefix);
}
-
- /// <summary>
- /// Normalizes the path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>System.String.</returns>
- public static string NormalizePath(string path)
- {
- // Quotes are valid path characters in linux and they need to be escaped here with a leading \
- return path.Replace("\"", "\\\"", StringComparison.Ordinal);
- }
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 1199fd7d70..fa43d756a4 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -922,12 +922,40 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-hwaccel_flags +low_priority " + inputArg;
}
+ // Force the video stream, otherwise ffmpeg may pick a cover image.
+ var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, imageStream);
+ if (streamIndex < 0)
+ {
+ throw new InvalidOperationException($"Unable to locate requested stream {imageStream.Title}");
+ }
+
+ inputArg += " -map 0:" + streamIndex;
+
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
if (string.IsNullOrWhiteSpace(filterParam))
{
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
+ // Normalize invalid PTS from containers for non keyframe only mode
+ if (!enableKeyFrameOnlyExtraction)
+ {
+ var fpsFilterIndex = filterParam.IndexOf("fps=", StringComparison.Ordinal);
+ if (fpsFilterIndex >= 0)
+ {
+ var inputFrameRate = (imageStream.ReferenceFrameRate.HasValue && imageStream.ReferenceFrameRate > 0)
+ ? imageStream.ReferenceFrameRate.Value : 30;
+
+ var setPtsFilter = string.Create(CultureInfo.InvariantCulture, $"setpts=N/{inputFrameRate:F3}/TB,");
+
+ filterParam = filterParam.Insert(fpsFilterIndex, setPtsFilter);
+ }
+ else
+ {
+ throw new InvalidOperationException("EncodingHelper returned invalid filter parameters.");
+ }
+ }
+
try
{
return await ExtractVideoImagesOnIntervalInternal(
@@ -1133,6 +1161,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
+ catch (InvalidOperationException)
+ {
+ // The process finished before its priority could be lowered. That says nothing
+ // about whether the platform allows it, so keep the capability for the next one.
+ }
catch (Exception ex)
{
_canSetProcessPriority = false;
@@ -1342,12 +1375,20 @@ namespace MediaBrowser.MediaEncoding.Encoder
return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
}
- private sealed class ProcessWrapper : IDisposable
+ internal sealed class ProcessWrapper : IDisposable
{
private readonly MediaEncoder _mediaEncoder;
+ // The exit event is raised on the thread pool, so it writes the state below while the
+ // caller that started the process is reading it.
+ private readonly Lock _exitLock = new();
+
private bool _disposed = false;
+ private bool _hasExited;
+
+ private int? _exitCode;
+
public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
{
Process = process;
@@ -1357,49 +1398,84 @@ namespace MediaBrowser.MediaEncoding.Encoder
public Process Process { get; }
- public bool HasExited { get; private set; }
+ // The exit event can lag behind the wait that returned, so ask the process rather than
+ // report one that has exited as still running.
+ public bool HasExited => ReadExitState().HasExited;
+
+ // As above: rather than report no exit code for a process that has one.
+ public int? ExitCode => ReadExitState().ExitCode;
+
+ private (bool HasExited, int? ExitCode) ReadExitState()
+ {
+ lock (_exitLock)
+ {
+ if (!_hasExited && !_disposed)
+ {
+ try
+ {
+ if (Process.HasExited)
+ {
+ _hasExited = true;
+ _exitCode = Process.ExitCode;
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ // No process is associated with this object, or it was disposed from
+ // under us - ObjectDisposedException derives from this one.
+ }
+ }
- public int? ExitCode { get; private set; }
+ return (_hasExited, _exitCode);
+ }
+ }
private void OnProcessExited(object sender, EventArgs e)
{
var process = (Process)sender;
- HasExited = true;
-
- try
- {
- ExitCode = process.ExitCode;
- }
- catch
+ lock (_exitLock)
{
+ _hasExited = true;
+
+ try
+ {
+ _exitCode = process.ExitCode;
+ }
+ catch
+ {
+ }
}
- DisposeProcess(process);
+ // Only stop tracking it. The caller that started the process still holds it to read
+ // its output and its exit code, so disposing it here handed whoever was quickest to
+ // exit - an ffprobe on a file it rejects outright - an ObjectDisposedException.
+ Untrack();
}
- private void DisposeProcess(Process process)
+ private void Untrack()
{
lock (_mediaEncoder._runningProcessesLock)
{
_mediaEncoder._runningProcesses.Remove(this);
}
-
- process.Dispose();
}
public void Dispose()
{
- if (!_disposed)
+ lock (_exitLock)
{
- if (Process is not null)
+ if (_disposed)
{
- Process.Exited -= OnProcessExited;
- DisposeProcess(Process);
+ return;
}
+
+ _disposed = true;
}
- _disposed = true;
+ Process.Exited -= OnProcessExited;
+ Untrack();
+ Process.Dispose();
}
}
}