aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.MediaEncoding
diff options
context:
space:
mode:
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.cs298
-rw-r--r--MediaBrowser.MediaEncoding/FfmpegException.cs39
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj16
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs142
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs23
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs9
9 files changed, 282 insertions, 336 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 eed50fe0b..c41ed20cd 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;
@@ -94,6 +95,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string ProbePath => _ffprobePath;
+ 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.
@@ -120,9 +129,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)
@@ -140,7 +149,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);
@@ -154,6 +186,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);
@@ -188,7 +230,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
// 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);
@@ -298,11 +339,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- public Version GetMediaEncoderVersion()
- {
- return _ffmpegVersion;
- }
-
public bool CanEncodeToAudioCodec(string codec)
{
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@@ -476,17 +512,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(
@@ -498,42 +534,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)
{
@@ -545,49 +555,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.
@@ -595,18 +611,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)
{
@@ -670,11 +687,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;
@@ -690,118 +705,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 30cfb904e..b60ccd2ca 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -11,6 +11,10 @@
<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.2" />
- <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/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 26f629a31..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;
}
@@ -1443,7 +1485,7 @@ 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;
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 022cdbe9d..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)
@@ -635,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))
@@ -679,7 +677,6 @@ 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, FileOptions.Asynchronous))
using (var writer = new StreamWriter(fileStream, encoding))
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index 6d56dda91..38ef57dee 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -19,12 +19,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
writer.WriteLine("WEBVTT");
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("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
writer.WriteLine();
foreach (var trackEvent in info.TrackEvents)
{
@@ -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;