mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-22 10:04:44 +01:00
Merge remote-tracking branch 'upstream/master' into mbaff-interlace-detection
This commit is contained in:
@@ -89,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
|
||||
return File.OpenRead(attachmentPath);
|
||||
return AsyncFile.OpenRead(attachmentPath);
|
||||
}
|
||||
|
||||
private async Task<string> GetReadableFile(
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
x => new BdInfoFileInfo(x));
|
||||
}
|
||||
|
||||
public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path)
|
||||
public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
|
||||
{
|
||||
return new BdInfoDirectoryInfo(fs, path);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
|
||||
public bool IsDir => _impl.IsDirectory;
|
||||
|
||||
public System.IO.Stream OpenRead()
|
||||
public Stream OpenRead()
|
||||
{
|
||||
return new FileStream(
|
||||
FullName,
|
||||
@@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
FileShare.Read);
|
||||
}
|
||||
|
||||
public System.IO.StreamReader OpenText()
|
||||
public StreamReader OpenText()
|
||||
{
|
||||
return new System.IO.StreamReader(OpenRead());
|
||||
return new StreamReader(OpenRead());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
public class EncoderValidator
|
||||
{
|
||||
private const string DefaultEncoderPath = "ffmpeg";
|
||||
|
||||
private static readonly string[] _requiredDecoders = new[]
|
||||
{
|
||||
"h264",
|
||||
@@ -89,6 +87,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"hevc_videotoolbox"
|
||||
};
|
||||
|
||||
private static readonly string[] _requiredFilters = new[]
|
||||
{
|
||||
"scale_cuda",
|
||||
"yadif_cuda",
|
||||
"hwupload_cuda",
|
||||
"overlay_cuda",
|
||||
"tonemap_cuda",
|
||||
"tonemap_opencl",
|
||||
"tonemap_vaapi",
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
|
||||
{
|
||||
{ 0, new string[] { "scale_cuda", "Output format (default \"same\")" } },
|
||||
{ 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
|
||||
{ 2, new string[] { "tonemap_opencl", "bt2390" } }
|
||||
};
|
||||
|
||||
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
|
||||
private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
|
||||
{
|
||||
@@ -106,7 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private readonly string _encoderPath;
|
||||
|
||||
public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath)
|
||||
public EncoderValidator(ILogger logger, string encoderPath)
|
||||
{
|
||||
_logger = logger;
|
||||
_encoderPath = encoderPath;
|
||||
@@ -156,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
|
||||
// Work out what the version under test is
|
||||
var version = GetFFmpegVersion(versionOutput);
|
||||
var version = GetFFmpegVersionInternal(versionOutput);
|
||||
|
||||
_logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown");
|
||||
|
||||
@@ -200,6 +216,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
|
||||
|
||||
public IEnumerable<string> GetFilters() => GetFFmpegFilters();
|
||||
|
||||
public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
|
||||
|
||||
public Version? GetFFmpegVersion()
|
||||
{
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating encoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: The process returned no result");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("ffmpeg output: {Output}", output);
|
||||
|
||||
return GetFFmpegVersionInternal(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
|
||||
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
|
||||
@@ -208,7 +252,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// </summary>
|
||||
/// <param name="output">The output from "ffmpeg -version".</param>
|
||||
/// <returns>The FFmpeg version.</returns>
|
||||
internal Version? GetFFmpegVersion(string output)
|
||||
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]+\.?)+)");
|
||||
@@ -297,9 +341,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return found;
|
||||
}
|
||||
|
||||
public bool CheckFilter(string filter, string option)
|
||||
public bool CheckFilterWithOption(string filter, string option)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -317,11 +361,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrEmpty(option))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return output.Contains(option, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -362,6 +401,49 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return found;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetFFmpegFilters()
|
||||
{
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-filters");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting available filters");
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
var found = Regex
|
||||
.Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
|
||||
.Cast<Match>()
|
||||
.Select(x => x.Groups["filter"].Value)
|
||||
.Where(x => _requiredFilters.Contains(x));
|
||||
|
||||
_logger.LogInformation("Available filters: {Filters}", found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private IDictionary<int, bool> GetFFmpegFiltersWithOption()
|
||||
{
|
||||
IDictionary<int, bool> dict = new Dictionary<int, bool>();
|
||||
for (int i = 0; i < _filterOptionsDict.Count; i++)
|
||||
{
|
||||
if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
|
||||
{
|
||||
dict.Add(i, CheckFilterWithOption(val[0], val[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
private string GetProcessOutput(string path, string arguments)
|
||||
{
|
||||
using (var process = new Process()
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -23,7 +24,6 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -44,11 +44,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// </summary>
|
||||
internal const int DefaultHdrImageExtractionTimeout = 20000;
|
||||
|
||||
/// <summary>
|
||||
/// The us culture.
|
||||
/// </summary>
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly ILogger<MediaEncoder> _logger;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@@ -66,10 +61,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private List<string> _encoders = new List<string>();
|
||||
private List<string> _decoders = new List<string>();
|
||||
private List<string> _hwaccels = new List<string>();
|
||||
private List<string> _filters = new List<string>();
|
||||
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
|
||||
|
||||
private Version _ffmpegVersion = null;
|
||||
private string _ffmpegPath = string.Empty;
|
||||
private string _ffprobePath;
|
||||
private int threads;
|
||||
private int _threads;
|
||||
|
||||
public MediaEncoder(
|
||||
ILogger<MediaEncoder> logger,
|
||||
@@ -89,9 +87,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// <inheritdoc />
|
||||
public string EncoderPath => _ffmpegPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FFmpegLocation EncoderLocation { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run at startup or if the user removes a Custom path from transcode page.
|
||||
/// Sets global variables FFmpegPath.
|
||||
@@ -100,20 +95,23 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public void SetFFmpegPath()
|
||||
{
|
||||
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
|
||||
if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom))
|
||||
var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
|
||||
if (string.IsNullOrEmpty(ffmpegPath))
|
||||
{
|
||||
// 2) Check if the --ffmpeg CLI switch has been given
|
||||
if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
|
||||
ffmpegPath = _startupOptionFFmpegPath;
|
||||
if (string.IsNullOrEmpty(ffmpegPath))
|
||||
{
|
||||
// 3) Search system $PATH environment variable for valid FFmpeg
|
||||
if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
|
||||
{
|
||||
EncoderLocation = FFmpegLocation.NotFound;
|
||||
_ffmpegPath = null;
|
||||
}
|
||||
// 3) Check "ffmpeg"
|
||||
ffmpegPath = "ffmpeg";
|
||||
}
|
||||
}
|
||||
|
||||
if (!ValidatePath(ffmpegPath))
|
||||
{
|
||||
_ffmpegPath = null;
|
||||
}
|
||||
|
||||
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
|
||||
var config = _configurationManager.GetEncodingOptions();
|
||||
config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
|
||||
@@ -130,11 +128,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
SetAvailableDecoders(validator.GetDecoders());
|
||||
SetAvailableEncoders(validator.GetEncoders());
|
||||
SetAvailableFilters(validator.GetFilters());
|
||||
SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
|
||||
SetAvailableHwaccels(validator.GetHwaccels());
|
||||
threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
|
||||
SetMediaEncoderVersion(validator);
|
||||
|
||||
_threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
|
||||
}
|
||||
|
||||
_logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
|
||||
_logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,6 +147,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// <param name="pathType">The path type.</param>
|
||||
public void UpdateEncoderPath(string path, string pathType)
|
||||
{
|
||||
var config = _configurationManager.GetEncodingOptions();
|
||||
|
||||
// Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file?
|
||||
if (string.IsNullOrEmpty(config.EncoderAppPath)
|
||||
&& string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay));
|
||||
return;
|
||||
}
|
||||
|
||||
string newPath;
|
||||
|
||||
_logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty);
|
||||
@@ -153,28 +165,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
throw new ArgumentException("Unexpected pathType value");
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(path))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
// User had cleared the custom path in UI
|
||||
newPath = string.Empty;
|
||||
}
|
||||
else if (File.Exists(path))
|
||||
{
|
||||
newPath = path;
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
// Given path is directory, so resolve down to filename
|
||||
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
// Given path is directory, so resolve down to filename
|
||||
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
|
||||
}
|
||||
else
|
||||
{
|
||||
newPath = path;
|
||||
}
|
||||
|
||||
if (!new EncoderValidator(_logger, newPath).ValidateVersion())
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
||||
// This ensures its not lost on next startup
|
||||
var config = _configurationManager.GetEncodingOptions();
|
||||
config.EncoderAppPath = newPath;
|
||||
_configurationManager.SaveConfiguration("encoding", config);
|
||||
|
||||
@@ -184,37 +200,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
/// <summary>
|
||||
/// Validates the supplied FQPN to ensure it is a ffmpeg utility.
|
||||
/// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
|
||||
/// If checks pass, global variable FFmpegPath is updated.
|
||||
/// </summary>
|
||||
/// <param name="path">FQPN to test.</param>
|
||||
/// <param name="location">Location (External, Custom, System) of tool.</param>
|
||||
/// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
|
||||
private bool ValidatePath(string path, FFmpegLocation location)
|
||||
private bool ValidatePath(string path)
|
||||
{
|
||||
bool rc = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
rc = new EncoderValidator(_logger, path).ValidateVersion();
|
||||
|
||||
if (!rc)
|
||||
{
|
||||
_logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path);
|
||||
}
|
||||
|
||||
_ffmpegPath = path;
|
||||
EncoderLocation = location;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return rc;
|
||||
bool rc = new EncoderValidator(_logger, path).ValidateVersion();
|
||||
if (!rc)
|
||||
{
|
||||
_logger.LogWarning("FFmpeg: Failed version check: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
_ffmpegPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
|
||||
@@ -235,34 +240,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search the system $PATH environment variable looking for given filename.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The filename.</param>
|
||||
/// <returns>The full path to the file.</returns>
|
||||
private string ExistsOnSystemPath(string fileName)
|
||||
{
|
||||
var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true);
|
||||
if (!string.IsNullOrEmpty(inJellyfinPath))
|
||||
{
|
||||
return inJellyfinPath;
|
||||
}
|
||||
|
||||
var values = Environment.GetEnvironmentVariable("PATH");
|
||||
|
||||
foreach (var path in values.Split(Path.PathSeparator))
|
||||
{
|
||||
var candidatePath = GetEncoderPathFromDirectory(path, fileName);
|
||||
|
||||
if (!string.IsNullOrEmpty(candidatePath))
|
||||
{
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetAvailableEncoders(IEnumerable<string> list)
|
||||
{
|
||||
_encoders = list.ToList();
|
||||
@@ -278,6 +255,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_hwaccels = list.ToList();
|
||||
}
|
||||
|
||||
public void SetAvailableFilters(IEnumerable<string> list)
|
||||
{
|
||||
_filters = list.ToList();
|
||||
}
|
||||
|
||||
public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
|
||||
{
|
||||
_filtersWithOption = dict;
|
||||
}
|
||||
|
||||
public void SetMediaEncoderVersion(EncoderValidator validator)
|
||||
{
|
||||
_ffmpegVersion = validator.GetFFmpegVersion();
|
||||
}
|
||||
|
||||
public bool SupportsEncoder(string encoder)
|
||||
{
|
||||
return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -293,17 +285,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool SupportsFilter(string filter, string option)
|
||||
public bool SupportsFilter(string filter)
|
||||
{
|
||||
if (_ffmpegPath != null)
|
||||
return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool SupportsFilterWithOption(FilterOptionType option)
|
||||
{
|
||||
if (_filtersWithOption.TryGetValue((int)option, out var val))
|
||||
{
|
||||
var validator = new EncoderValidator(_logger, _ffmpegPath);
|
||||
return validator.CheckFilter(filter, option);
|
||||
return val;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Version GetMediaEncoderVersion()
|
||||
{
|
||||
return _ffmpegVersion;
|
||||
}
|
||||
|
||||
public bool CanEncodeToAudioCodec(string codec)
|
||||
{
|
||||
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -394,7 +395,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var args = extractChapters
|
||||
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
|
||||
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
|
||||
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
|
||||
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
@@ -477,17 +478,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
Protocol = MediaProtocol.File
|
||||
};
|
||||
|
||||
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken);
|
||||
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
|
||||
{
|
||||
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken);
|
||||
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken)
|
||||
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken)
|
||||
{
|
||||
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken);
|
||||
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string> ExtractImage(
|
||||
@@ -499,24 +500,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
bool isAudio,
|
||||
Video3DFormat? threedFormat,
|
||||
TimeSpan? offset,
|
||||
string outputExtension,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputArgument = GetInputArgument(inputFile, mediaSource);
|
||||
|
||||
if (isAudio)
|
||||
{
|
||||
if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0)
|
||||
{
|
||||
// It seems for audio files we need to subtract 1 (for the audio stream??)
|
||||
imageStreamIndex = imageStreamIndex.Value - 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!isAudio)
|
||||
{
|
||||
// The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
|
||||
try
|
||||
{
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -529,7 +523,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
try
|
||||
{
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -542,7 +536,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
try
|
||||
{
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -554,17 +548,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false);
|
||||
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken)
|
||||
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inputPath));
|
||||
}
|
||||
|
||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
|
||||
if (string.IsNullOrEmpty(outputExtension))
|
||||
{
|
||||
outputExtension = ".jpg";
|
||||
}
|
||||
else if (outputExtension[0] != '.')
|
||||
{
|
||||
outputExtension = "." + outputExtension;
|
||||
}
|
||||
|
||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||
|
||||
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
|
||||
@@ -582,7 +585,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
|
||||
var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
|
||||
|
||||
var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
|
||||
if (enableHdrExtraction)
|
||||
@@ -615,7 +618,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads);
|
||||
|
||||
if (offset.HasValue)
|
||||
{
|
||||
@@ -699,7 +702,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
public string GetTimeParameter(TimeSpan time)
|
||||
{
|
||||
return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
|
||||
return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public async Task ExtractVideoImagesOnInterval(
|
||||
@@ -716,11 +719,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
var inputArgument = GetInputArgument(inputFile, mediaSource);
|
||||
|
||||
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
|
||||
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
if (maxWidth.HasValue)
|
||||
{
|
||||
var maxWidthParam = maxWidth.Value.ToString(_usCulture);
|
||||
var maxWidthParam = maxWidth.Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
|
||||
}
|
||||
@@ -728,7 +731,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
|
||||
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
|
||||
var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, _threads);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container))
|
||||
{
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occur during interaction with FFmpeg.
|
||||
/// </summary>
|
||||
public class FfmpegException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FfmpegException"/> class.
|
||||
/// </summary>
|
||||
public FfmpegException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public FfmpegException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message and a
|
||||
/// reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
|
||||
/// no inner exception is specified.
|
||||
/// </param>
|
||||
public FfmpegException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -27,10 +23,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BDInfo" Version="0.7.6.1" />
|
||||
<PackageReference Include="libse" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
|
||||
<PackageReference Include="UTF.Unknown" Version="2.3.0" />
|
||||
<PackageReference Include="libse" Version="3.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="UTF.Unknown" Version="2.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -22,7 +20,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
if (result.Format != null && result.Format.Tags != null)
|
||||
if (result.Format?.Tags != null)
|
||||
{
|
||||
result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags);
|
||||
}
|
||||
@@ -40,39 +38,17 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
tags.TryGetValue(key, out var val);
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an int from an FFProbeResult tags dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
|
||||
public static int? GetDictionaryNumericValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
if (tags.TryGetValue(key, out var val) && int.TryParse(val, out var i))
|
||||
{
|
||||
if (int.TryParse(val, out var i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -84,18 +60,13 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{DateTime}.</returns>
|
||||
public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
|
||||
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
if (tags.TryGetValue(key, out var val)
|
||||
&& (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)
|
||||
|| DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var i))
|
||||
{
|
||||
return i.ToUniversalTime();
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core;
|
||||
using Nikse.SubtitleEdit.Core.Common;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
subRip.LoadSubtitle(subtitle, lines, "untitled");
|
||||
if (subRip.ErrorCount > 0)
|
||||
{
|
||||
_logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
|
||||
_logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
|
||||
}
|
||||
|
||||
var trackInfo = new SubtitleTrackInfo();
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
@@ -192,10 +193,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
|
||||
return File.OpenRead(fileInfo.Path);
|
||||
return AsyncFile.OpenRead(fileInfo.Path);
|
||||
}
|
||||
|
||||
private async Task<SubtitleInfo> GetReadableFile(
|
||||
internal async Task<SubtitleInfo> GetReadableFile(
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream subtitleStream,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -205,9 +206,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
string outputFormat;
|
||||
string outputCodec;
|
||||
|
||||
if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
|
||||
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";
|
||||
@@ -238,7 +239,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
|
||||
.TrimStart('.');
|
||||
|
||||
if (TryGetReader(currentFormat, out _))
|
||||
if (!TryGetReader(currentFormat, out _))
|
||||
{
|
||||
// Convert
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
|
||||
@@ -248,12 +249,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
|
||||
}
|
||||
|
||||
if (subtitleStream.IsExternal)
|
||||
{
|
||||
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
|
||||
}
|
||||
|
||||
return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
|
||||
// It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
|
||||
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
|
||||
}
|
||||
|
||||
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
|
||||
@@ -671,7 +668,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
string text;
|
||||
Encoding encoding;
|
||||
|
||||
using (var fileStream = File.OpenRead(file))
|
||||
using (var fileStream = AsyncFile.OpenRead(file))
|
||||
using (var reader = new StreamReader(fileStream, true))
|
||||
{
|
||||
encoding = reader.CurrentEncoding;
|
||||
@@ -683,8 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
if (!string.Equals(text, newText, StringComparison.Ordinal))
|
||||
{
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
|
||||
using (var writer = new StreamWriter(fileStream, encoding))
|
||||
{
|
||||
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
|
||||
@@ -750,13 +746,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
|
||||
case MediaProtocol.File:
|
||||
return File.OpenRead(path);
|
||||
return AsyncFile.OpenRead(path);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(protocol));
|
||||
}
|
||||
}
|
||||
|
||||
private struct SubtitleInfo
|
||||
internal readonly struct SubtitleInfo
|
||||
{
|
||||
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
|
||||
{
|
||||
@@ -766,13 +762,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
IsExternal = isExternal;
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public string Path { get; }
|
||||
|
||||
public MediaProtocol Protocol { get; set; }
|
||||
public MediaProtocol Protocol { get; }
|
||||
|
||||
public string Format { get; set; }
|
||||
public string Format { get; }
|
||||
|
||||
public bool IsExternal { get; set; }
|
||||
public bool IsExternal { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
writer.WriteLine("WEBVTT");
|
||||
writer.WriteLine(string.Empty);
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("REGION");
|
||||
writer.WriteLine("id:subtitle");
|
||||
writer.WriteLine("width:80%");
|
||||
writer.WriteLine("lines:3");
|
||||
writer.WriteLine("regionanchor:50%,100%");
|
||||
writer.WriteLine("viewportanchor:50%,90%");
|
||||
writer.WriteLine(string.Empty);
|
||||
writer.WriteLine();
|
||||
foreach (var trackEvent in info.TrackEvents)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Reference in New Issue
Block a user