aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding
diff options
context:
space:
mode:
authoroledfish <88390729+oledfish@users.noreply.github.com>2022-01-16 21:33:18 -0300
committerGitHub <noreply@github.com>2022-01-16 21:33:18 -0300
commit3b075a58027be4a2a3bdf662c70934f6cafafe87 (patch)
treeb4c226f25f843c3f2685c92e1edc3b3999716d34 /MediaBrowser.MediaEncoding
parent86a5e72a65df638df2cde349ccd2ad8c5d40f88c (diff)
parentef0708d876434a99ec647473c37295fab45a35fb (diff)
Merge branch 'jellyfin:master' into additional-episode-orders
Diffstat (limited to 'MediaBrowser.MediaEncoding')
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs9
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs80
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs318
-rw-r--r--MediaBrowser.MediaEncoding/FfmpegException.cs39
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj18
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs150
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs53
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs13
11 files changed, 321 insertions, 371 deletions
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index a524aeaa9..3fd4cd731 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
/// <inheritdoc />
- public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
+ public async Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
{
if (item == null)
{
@@ -223,11 +223,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (failed)
{
- var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}";
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- _logger.LogError(msg);
-
- throw new InvalidOperationException(msg);
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index e86e518be..409379c35 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -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);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 60a2d39e5..fe3069934 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -16,6 +16,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
"h264",
"hevc",
+ "vp8",
+ "libvpx",
+ "vp9",
+ "libvpx-vp9",
+ "av1",
+ "libdav1d",
"mpeg2video",
"mpeg4",
"msmpeg4",
@@ -30,6 +36,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"vc1_qsv",
"vp8_qsv",
"vp9_qsv",
+ "av1_qsv",
"h264_cuvid",
"hevc_cuvid",
"mpeg2_cuvid",
@@ -37,16 +44,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg4_cuvid",
"vp8_cuvid",
"vp9_cuvid",
+ "av1_cuvid",
"h264_mmal",
"mpeg2_mmal",
"mpeg4_mmal",
"vc1_mmal",
- "h264_mediacodec",
- "hevc_mediacodec",
- "mpeg2_mediacodec",
- "mpeg4_mediacodec",
- "vp8_mediacodec",
- "vp9_mediacodec",
"h264_opencl",
"hevc_opencl",
"mpeg2_opencl",
@@ -89,20 +91,39 @@ namespace MediaBrowser.MediaEncoding.Encoder
private static readonly string[] _requiredFilters = new[]
{
+ // sw
+ "alphasrc",
+ "zscale",
+ // qsv
+ "scale_qsv",
+ "vpp_qsv",
+ "deinterlace_qsv",
+ "overlay_qsv",
+ // cuda
"scale_cuda",
"yadif_cuda",
- "hwupload_cuda",
- "overlay_cuda",
"tonemap_cuda",
+ "overlay_cuda",
+ "hwupload_cuda",
+ // opencl
+ "scale_opencl",
"tonemap_opencl",
+ "overlay_opencl",
+ // vaapi
+ "scale_vaapi",
+ "deinterlace_vaapi",
"tonemap_vaapi",
+ "overlay_vaapi",
+ "hwupload_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" } }
+ { 2, new string[] { "tonemap_opencl", "bt2390" } },
+ { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } },
+ { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
@@ -144,7 +165,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-version");
+ output = GetProcessOutput(_encoderPath, "-version", false);
}
catch (Exception ex)
{
@@ -225,7 +246,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-version");
+ output = GetProcessOutput(_encoderPath, "-version", false);
}
catch (Exception ex)
{
@@ -318,12 +339,36 @@ namespace MediaBrowser.MediaEncoding.Encoder
return map;
}
+ public bool CheckVaapiDeviceByDriverName(string driverName, string renderNodePath)
+ {
+ if (!OperatingSystem.IsLinux())
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(driverName) || string.IsNullOrEmpty(renderNodePath))
+ {
+ return false;
+ }
+
+ try
+ {
+ var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true);
+ return output.Contains(driverName, StringComparison.Ordinal);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting the given vaapi render node path");
+ return false;
+ }
+ }
+
private IEnumerable<string> GetHwaccelTypes()
{
string? output = null;
try
{
- output = GetProcessOutput(_encoderPath, "-hwaccels");
+ output = GetProcessOutput(_encoderPath, "-hwaccels", false);
}
catch (Exception ex)
{
@@ -351,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-h filter=" + filter);
+ output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false);
}
catch (Exception ex)
{
@@ -375,7 +420,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-" + codecstr);
+ output = GetProcessOutput(_encoderPath, "-" + codecstr, false);
}
catch (Exception ex)
{
@@ -406,7 +451,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
string output;
try
{
- output = GetProcessOutput(_encoderPath, "-filters");
+ output = GetProcessOutput(_encoderPath, "-filters", false);
}
catch (Exception ex)
{
@@ -444,7 +489,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return dict;
}
- private string GetProcessOutput(string path, string arguments)
+ private string GetProcessOutput(string path, string arguments, bool readStdErr)
{
using (var process = new Process()
{
@@ -455,7 +500,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
RedirectStandardOutput = true,
- // ffmpeg uses stderr to log info, don't show this
RedirectStandardError = true
}
})
@@ -464,7 +508,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
process.Start();
- return process.StandardOutput.ReadToEnd();
+ return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a7bcaf544..e1643ea43 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -12,12 +12,14 @@ 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;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -43,11 +45,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;
@@ -68,6 +65,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _filters = new List<string>();
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
+ private bool _isVaapiDeviceAmd = false;
+ private bool _isVaapiDeviceInteliHD = false;
+ private bool _isVaapiDeviceInteli965 = false;
+
private Version _ffmpegVersion = null;
private string _ffmpegPath = string.Empty;
private string _ffprobePath;
@@ -91,6 +92,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string EncoderPath => _ffmpegPath;
+ public Version EncoderVersion => _ffmpegVersion;
+
+ public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
+
+ public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
+
+ public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
+
/// <summary>
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
@@ -117,9 +126,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// 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;
- _configurationManager.SaveConfiguration("encoding", config);
+ var options = _configurationManager.GetEncodingOptions();
+ options.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
+ _configurationManager.SaveConfiguration("encoding", options);
// Only if mpeg path is set, try and set path to probe
if (_ffmpegPath != null)
@@ -137,7 +146,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableHwaccels(validator.GetHwaccels());
SetMediaEncoderVersion(validator);
- _threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
+ _threads = EncodingHelper.GetNumberOfThreads(null, options, null);
+
+ // Check the Vaapi device vendor
+ if (OperatingSystem.IsLinux()
+ && SupportsHwaccel("vaapi")
+ && !string.IsNullOrEmpty(options.VaapiDevice)
+ && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
+ _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
+ _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
+ if (_isVaapiDeviceAmd)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice);
+ }
+ else if (_isVaapiDeviceInteliHD)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (iHD)", options.VaapiDevice);
+ }
+ else if (_isVaapiDeviceInteli965)
+ {
+ _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
+ }
+ }
}
_logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
@@ -151,6 +183,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);
@@ -165,19 +207,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
// User had cleared the custom path in UI
newPath = string.Empty;
}
- else if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
else
{
- newPath = path;
+ 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);
@@ -287,11 +336,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- public Version GetMediaEncoderVersion()
- {
- return _ffmpegVersion;
- }
-
public bool CanEncodeToAudioCodec(string codec)
{
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@@ -465,17 +509,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, ImageFormat.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, ImageFormat.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, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
- return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken);
+ return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
}
private async Task<string> ExtractImage(
@@ -487,42 +531,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool isAudio,
Video3DFormat? threedFormat,
TimeSpan? offset,
+ ImageFormat? targetFormat,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
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);
- }
- catch (ArgumentException)
- {
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "I-frame or HDR image extraction failed, will attempt with I-frame extraction disabled. Input: {Arguments}", inputArgument);
- }
-
- try
- {
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false);
- }
- catch (ArgumentException)
- {
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "HDR image extraction failed, will fallback to SDR image extraction. Input: {Arguments}", inputArgument);
- }
-
- 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, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -534,49 +552,55 @@ 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, targetFormat, 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, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
}
- var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
+ var outputExtension = targetFormat switch
+ {
+ ImageFormat.Bmp => ".bmp",
+ ImageFormat.Gif => ".gif",
+ ImageFormat.Jpg => ".jpg",
+ ImageFormat.Png => ".png",
+ ImageFormat.Webp => ".webp",
+ _ => ".jpg"
+ };
+
+ var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
+ // deint -> scale -> thumbnail -> tonemap.
+ // put the SW tonemap right after the thumbnail to do it only once to reduce cpu usage.
+ var filters = new List<string>();
+
+ // deinterlace using bwdif algorithm for video stream.
+ if (videoStream != null && videoStream.IsInterlaced)
+ {
+ filters.Add("bwdif=0:-1:0");
+ }
+
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
// This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar
- var vf = threedFormat switch
+ 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 => "-vf 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 => "-vf 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 heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made
- Video3DFormat.HalfTopAndBottom => "-vf 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 heigt in half, set the display aspect,crop out any black bars we may have made
- Video3DFormat.FullTopAndBottom => "-vf 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",
- _ => string.Empty
+ 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"
};
- 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)
- {
- string tonemapFilters = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p";
- if (vf.Length == 0)
- {
- vf = "-vf " + tonemapFilters;
- }
- else
- {
- vf += "," + tonemapFilters;
- }
- }
+ filters.Add(scaler);
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
// mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed.
@@ -584,18 +608,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (enableThumbnail)
{
var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase);
- var batchSize = useLargerBatchSize ? "50" : "24";
- if (string.IsNullOrEmpty(vf))
- {
- vf = "-vf thumbnail=" + batchSize;
- }
- else
- {
- vf += ",thumbnail=" + batchSize;
- }
+ filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
+ }
+
+ // Use SW tonemap on HDR video stream only when the zscale filter is available.
+ var enableHdrExtraction = string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) && SupportsFilter("zscale");
+ if (enableHdrExtraction)
+ {
+ filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p");
}
- 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 vf = string.Join(',', filters);
+ var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads);
if (offset.HasValue)
{
@@ -659,11 +684,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1 || !file.Exists || file.Length == 0)
{
- var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath);
+ _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath);
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath));
}
return tempExtractPath;
@@ -679,118 +702,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetTimeParameter(TimeSpan time)
{
- return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
- }
-
- public async Task ExtractVideoImagesOnInterval(
- string inputFile,
- string container,
- MediaStream videoStream,
- MediaSourceInfo mediaSource,
- Video3DFormat? threedFormat,
- TimeSpan interval,
- string targetDirectory,
- string filenamePrefix,
- int? maxWidth,
- CancellationToken cancellationToken)
- {
- var inputArgument = GetInputArgument(inputFile, mediaSource);
-
- var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
-
- if (maxWidth.HasValue)
- {
- var maxWidthParam = maxWidth.Value.ToString(_usCulture);
-
- vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
- }
-
- 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);
-
- if (!string.IsNullOrWhiteSpace(container))
- {
- var inputFormat = EncodingHelper.GetInputFormat(container);
- if (!string.IsNullOrWhiteSpace(inputFormat))
- {
- args = "-f " + inputFormat + " " + args;
- }
- }
-
- var processStartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- UseShellExecute = false,
- FileName = _ffmpegPath,
- Arguments = args,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- };
-
- _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments);
-
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- bool ranToCompletion = false;
-
- var process = new Process
- {
- StartInfo = processStartInfo,
- EnableRaisingEvents = true
- };
- using (var processWrapper = new ProcessWrapper(process, this))
- {
- try
- {
- StartProcess(processWrapper);
-
- // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
- // but we still need to detect if the process hangs.
- // Making the assumption that as long as new jpegs are showing up, everything is good.
-
- bool isResponsive = true;
- int lastCount = 0;
-
- while (isResponsive)
- {
- if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false))
- {
- ranToCompletion = true;
- break;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- var jpegCount = _fileSystem.GetFilePaths(targetDirectory)
- .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase));
-
- isResponsive = jpegCount > lastCount;
- lastCount = jpegCount;
- }
-
- if (!ranToCompletion)
- {
- StopProcess(processWrapper, 1000);
- }
- }
- finally
- {
- _thumbnailResourcePool.Release();
- }
-
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
-
- if (exitCode == -1)
- {
- var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument);
-
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
- }
- }
+ return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
}
private void StartProcess(ProcessWrapper process)
diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.MediaEncoding/FfmpegException.cs
deleted file mode 100644
index 1697fd33a..000000000
--- a/MediaBrowser.MediaEncoding/FfmpegException.cs
+++ /dev/null
@@ -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)
- {
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6da9886a4..b60ccd2ca 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,11 +6,15 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
+ </PropertyGroup>
+
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
@@ -22,17 +26,17 @@
</ItemGroup>
<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.4.0" />
+ <PackageReference Include="BDInfo" Version="0.7.6.2" />
+ <PackageReference Include="libse" Version="3.6.4" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
+ <PackageReference Include="UTF.Unknown" Version="2.5.0" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 9196fe139..a9e753726 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -63,10 +63,10 @@ namespace MediaBrowser.MediaEncoding.Probing
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
{
if (tags.TryGetValue(key, out var val)
- && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var dateTime)
- || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out dateTime)))
+ && (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 dateTime.ToUniversalTime();
+ return dateTime;
}
return null;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 2516aad1c..750fd44eb 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -28,9 +28,8 @@ namespace MediaBrowser.MediaEncoding.Probing
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
- private static readonly Regex _performerPattern = new (@"(?<name>.*) \((?<instrument>.*)\)");
+ private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)");
- private readonly CultureInfo _usCulture = new ("en-US");
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -46,11 +45,13 @@ namespace MediaBrowser.MediaEncoding.Probing
{
"AC/DC",
"Au/Ra",
+ "Bremer/McCoy",
"이달의 소녀 1/3",
"LOONA 1/3",
"LOONA / yyxy",
"LOONA / ODD EYE CIRCLE",
- "K/DA"
+ "K/DA",
+ "22/7"
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
@@ -83,7 +84,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(data.Format.BitRate))
{
- if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
info.Bitrate = value;
}
@@ -191,7 +192,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration))
{
- info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks;
+ info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, CultureInfo.InvariantCulture)).Ticks;
}
FetchWtvInfo(info, data);
@@ -582,7 +583,8 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaAttachments.</returns>
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
{
- if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
+ && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
{
return null;
}
@@ -647,11 +649,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.IsAVC = false;
}
- if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase))
- {
- stream.IsInterlaced = true;
- }
-
// Filter out junk
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
{
@@ -673,7 +670,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.SampleRate))
{
- if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
stream.SampleRate = value;
}
@@ -689,6 +686,16 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.BitDepth = streamInfo.BitsPerRawSample;
}
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
{
@@ -697,18 +704,43 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedForced = _localization.GetLocalizedString("Forced");
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SubtitleHandler", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
{
- stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
- ? MediaStreamType.EmbeddedImage
- : MediaStreamType.Video;
-
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
- if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
+ // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
+ // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
+ // frame rate (both rounded to the nearest integer) to determine if the file is interlaced
+ int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0);
+ int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0);
+
+ bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
+ bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
+ && string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ && roundedTimeBaseFPS == roundedDoubleFrameRate;
+
+ if (videoInterlaced || h264MbaffCoded)
+ {
+ stream.IsInterlaced = true;
+ }
+
+ if (isAudio
+ || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.EmbeddedImage;
}
@@ -745,18 +777,23 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!stream.BitDepth.HasValue)
{
- if (!string.IsNullOrEmpty(streamInfo.PixelFormat)
- && streamInfo.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(streamInfo.PixelFormat))
{
- stream.BitDepth = 10;
- }
-
- if (!string.IsNullOrEmpty(streamInfo.Profile)
- && (streamInfo.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
- || streamInfo.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
- || streamInfo.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase)))
- {
- stream.BitDepth = 10;
+ if (string.Equals(streamInfo.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(streamInfo.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.BitDepth = 8;
+ }
+ else if (string.Equals(streamInfo.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(streamInfo.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.BitDepth = 10;
+ }
+ else if (string.Equals(streamInfo.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(streamInfo.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.BitDepth = 12;
+ }
}
}
@@ -802,7 +839,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.BitRate))
{
- if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -815,7 +852,7 @@ namespace MediaBrowser.MediaEncoding.Probing
&& (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
{
// If the stream info doesn't have a bitrate get the value from the media format info
- if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -921,8 +958,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
- int.TryParse(parts[0], NumberStyles.Any, _usCulture, out var width) &&
- int.TryParse(parts[1], NumberStyles.Any, _usCulture, out var height) &&
+ int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) &&
+ int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) &&
width > 0 &&
height > 0))
{
@@ -995,27 +1032,32 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.Nullable{System.Single}.</returns>
- private float? GetFrameRate(string value)
+ internal static float? GetFrameRate(ReadOnlySpan<char> value)
{
- if (string.IsNullOrEmpty(value))
+ if (value.IsEmpty)
{
return null;
}
- var parts = value.Split('/');
-
- float result;
-
- if (parts.Length == 2)
+ int index = value.IndexOf('/');
+ if (index == -1)
{
- result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
+ // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?)
+ if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+
+ return null;
}
- else
+
+ if (!float.TryParse(value[..index], NumberStyles.Integer, CultureInfo.InvariantCulture, out var dividend)
+ || !float.TryParse(value[(index + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var divisor))
{
- result = float.Parse(parts[0], _usCulture);
+ return null;
}
- return float.IsNaN(result) ? null : result;
+ return divisor == 0f ? null : dividend / divisor;
}
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
@@ -1039,7 +1081,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// If we got something, parse it
if (!string.IsNullOrEmpty(duration))
{
- data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
+ data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, CultureInfo.InvariantCulture)).Ticks;
}
}
@@ -1101,7 +1143,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return;
}
- info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, _usCulture);
+ info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture);
}
private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags)
@@ -1144,7 +1186,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
Name = match.Groups["name"].Value,
Type = PersonType.Actor,
- Role = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
+ Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
});
}
}
@@ -1325,8 +1367,8 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// Don't add artist/album artist name to studios, even if it's listed there
- if (info.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase)
- || info.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
+ if (info.Artists.Contains(studio, StringComparison.OrdinalIgnoreCase)
+ || info.AlbumArtists.Contains(studio, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -1378,7 +1420,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var disc = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.Split('/')[0], out var discNum))
+ if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
{
return discNum;
}
@@ -1443,16 +1485,16 @@ namespace MediaBrowser.MediaEncoding.Probing
.ToArray();
}
- if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, _usCulture, out var parsedYear))
+ if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
{
video.ProductionYear = parsedYear;
}
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
- if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.None, out var parsedDate))
+ if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate))
{
- video.PremiereDate = parsedDate.ToUniversalTime();
+ video.PremiereDate = parsedDate;
}
var description = tags.GetValueOrDefault("WM/SubTitleDescription");
@@ -1468,7 +1510,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (string.IsNullOrWhiteSpace(subTitle)
&& !string.IsNullOrWhiteSpace(description)
- && description.AsSpan()[0..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ && description.AsSpan()[..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].Contains(':')) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] descriptionParts = description.Split(':');
if (descriptionParts.Length > 0)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 24ceb1b57..52c1b6467 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -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();
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 6f6178af2..5b1ec8041 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -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;
@@ -138,28 +139,28 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
- var inputFormat = subtitle.format;
+ var inputFormat = subtitle.Format;
// Return the original if we don't have any way of converting it
if (!TryGetWriter(outputFormat, out var writer))
{
- return subtitle.stream;
+ return subtitle.Stream;
}
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
{
- return subtitle.stream;
+ return subtitle.Stream;
}
- using (var stream = subtitle.stream)
+ using (var stream = subtitle.Stream)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
}
- private async Task<(Stream stream, string format)> GetSubtitleStream(
+ private async Task<(Stream Stream, string Format)> GetSubtitleStream(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -195,7 +196,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
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)
@@ -639,17 +636,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (failed)
{
- var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}";
+ _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
+ throw new FfmpegException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
- var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}";
-
- _logger.LogInformation(msg);
+ _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
@@ -683,8 +677,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, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ 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);
@@ -756,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- private struct SubtitleInfo
+ internal readonly struct SubtitleInfo
{
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
{
@@ -766,13 +759,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; }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index ad32cb794..38ef57dee 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -18,14 +18,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
- writer.WriteLine(string.Empty);
- 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();
+ writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
+ writer.WriteLine();
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -39,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
}
- writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
+ writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime);
var text = trackEvent.Text;