mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-27 22:57:09 -04:00
New: MediaInfo -> FFProbe
* New: MediaInfo -> FFProbe * Detect HDR format * Fix migration for users that tested early ffmpeg
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
{
|
||||
public enum HdrFormat
|
||||
{
|
||||
None,
|
||||
Pq10,
|
||||
Hdr10,
|
||||
Hdr10Plus,
|
||||
Hlg10,
|
||||
DolbyVision
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -12,9 +11,9 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
{
|
||||
public static class MediaInfoFormatter
|
||||
{
|
||||
private const string ValidHdrColourPrimaries = "BT.2020";
|
||||
private const string VideoDynamicRangeHdr = "HDR";
|
||||
private static readonly string[] ValidHdrTransferFunctions = { "PQ", "HLG" };
|
||||
|
||||
private static readonly Regex PositionRegex = new Regex(@"(?<position>^\d\.\d)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter));
|
||||
|
||||
@@ -24,101 +23,95 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
|
||||
if (audioChannels == null || audioChannels == 0.0m)
|
||||
{
|
||||
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
|
||||
audioChannels = mediaInfo.AudioChannels;
|
||||
}
|
||||
|
||||
if (audioChannels == null || audioChannels == 0.0m)
|
||||
{
|
||||
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
|
||||
}
|
||||
|
||||
return audioChannels ?? 0;
|
||||
return audioChannels.Value;
|
||||
}
|
||||
|
||||
public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName)
|
||||
{
|
||||
if (mediaInfo.AudioCodecID == null)
|
||||
if (mediaInfo.AudioFormat == null)
|
||||
{
|
||||
return FormatAudioCodecLegacy(mediaInfo, sceneName);
|
||||
return null;
|
||||
}
|
||||
|
||||
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var audioFormat = mediaInfo.AudioFormat;
|
||||
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
|
||||
var audioProfile = mediaInfo.AudioProfile ?? string.Empty;
|
||||
var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty;
|
||||
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (audioFormat.Empty())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Atmos"))
|
||||
// see definitions here https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
|
||||
if (audioCodecID == "thd+")
|
||||
{
|
||||
return "TrueHD Atmos";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
|
||||
{
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
|
||||
{
|
||||
return "TrueHD Atmos";
|
||||
}
|
||||
|
||||
return "TrueHD";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("TrueHD"))
|
||||
if (audioFormat == "truehd")
|
||||
{
|
||||
return "TrueHD";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("FLAC"))
|
||||
if (audioFormat == "flac")
|
||||
{
|
||||
return "FLAC";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("DTS"))
|
||||
if (audioFormat == "dts")
|
||||
{
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
|
||||
if (audioProfile == "DTS:X")
|
||||
{
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
|
||||
{
|
||||
return "DTS-X";
|
||||
}
|
||||
return "DTS-X";
|
||||
}
|
||||
|
||||
if (audioProfile == "DTS-HD MA")
|
||||
{
|
||||
return "DTS-HD MA";
|
||||
}
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
|
||||
if (audioProfile == "DTS-ES")
|
||||
{
|
||||
return "DTS-ES";
|
||||
}
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
|
||||
if (audioProfile == "DTS-HD HRA")
|
||||
{
|
||||
return "DTS-HD HRA";
|
||||
}
|
||||
|
||||
if (audioProfile == "DTS Express")
|
||||
{
|
||||
return "DTS Express";
|
||||
}
|
||||
|
||||
if (audioProfile == "DTS 96/24")
|
||||
{
|
||||
return "DTS 96/24";
|
||||
}
|
||||
|
||||
return "DTS";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
|
||||
if (audioCodecID == "ec+3")
|
||||
{
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
|
||||
{
|
||||
return "EAC3 Atmos";
|
||||
}
|
||||
return "EAC3 Atmos";
|
||||
}
|
||||
|
||||
if (audioFormat == "eac3")
|
||||
{
|
||||
return "EAC3";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("AC-3"))
|
||||
if (audioFormat == "ac3")
|
||||
{
|
||||
return "AC3";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("AAC"))
|
||||
if (audioFormat == "aac")
|
||||
{
|
||||
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
|
||||
{
|
||||
@@ -128,461 +121,166 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
return "AAC";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("mp3"))
|
||||
if (audioFormat == "mp3")
|
||||
{
|
||||
return "MP3";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
|
||||
if (audioFormat == "mp2")
|
||||
{
|
||||
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
|
||||
{
|
||||
return "MP3";
|
||||
}
|
||||
|
||||
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
|
||||
{
|
||||
return "MP2";
|
||||
}
|
||||
return "MP2";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Opus"))
|
||||
if (audioFormat == "opus")
|
||||
{
|
||||
return "Opus";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("PCM"))
|
||||
if (audioFormat.StartsWith("pcm_") || audioFormat.StartsWith("adpcm_"))
|
||||
{
|
||||
return "PCM";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("ADPCM"))
|
||||
{
|
||||
return "PCM";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Vorbis"))
|
||||
if (audioFormat == "vorbis")
|
||||
{
|
||||
return "Vorbis";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("WMA"))
|
||||
if (audioFormat == "wmav1" ||
|
||||
audioFormat == "wmav2")
|
||||
{
|
||||
return "WMA";
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("A_QUICKTIME"))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
Logger.Debug()
|
||||
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.AudioFormat, audioCodecID, audioProfile, audioCodecLibrary, mediaInfo.AudioAdditionalFeatures), sceneName)
|
||||
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
|
||||
.Message("Unknown audio format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
|
||||
.WriteSentryWarn("UnknownAudioFormatFFProbe", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
|
||||
.Write();
|
||||
|
||||
return mediaInfo.AudioFormat;
|
||||
}
|
||||
|
||||
public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
|
||||
{
|
||||
var audioFormat = mediaInfo.AudioFormat;
|
||||
|
||||
if (audioFormat.IsNullOrWhiteSpace())
|
||||
{
|
||||
return audioFormat;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("AC-3"))
|
||||
{
|
||||
return "AC3";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
|
||||
{
|
||||
return "EAC3";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("AAC"))
|
||||
{
|
||||
return "AAC";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
|
||||
{
|
||||
return "MP3";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("DTS"))
|
||||
{
|
||||
return "DTS";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("TrueHD"))
|
||||
{
|
||||
return "TrueHD";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("FLAC"))
|
||||
{
|
||||
return "FLAC";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("Vorbis"))
|
||||
{
|
||||
return "Vorbis";
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("Opus"))
|
||||
{
|
||||
return "Opus";
|
||||
}
|
||||
|
||||
return audioFormat;
|
||||
}
|
||||
|
||||
public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName)
|
||||
{
|
||||
if (mediaInfo.VideoFormat == null)
|
||||
{
|
||||
return FormatVideoCodecLegacy(mediaInfo, sceneName);
|
||||
return null;
|
||||
}
|
||||
|
||||
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var videoFormat = mediaInfo.VideoFormat;
|
||||
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
|
||||
var videoProfile = mediaInfo.VideoProfile ?? string.Empty;
|
||||
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
|
||||
|
||||
var result = mediaInfo.VideoFormat.Trim();
|
||||
var result = videoFormat.Trim();
|
||||
|
||||
if (videoFormat.Empty())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("x264"))
|
||||
// see definitions here: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
|
||||
if (videoCodecID == "x264")
|
||||
{
|
||||
return "x264";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
|
||||
if (videoFormat == "h264")
|
||||
{
|
||||
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
|
||||
{
|
||||
return "x264";
|
||||
}
|
||||
|
||||
return GetSceneNameMatch(sceneName, "AVC", "x264", "h264");
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
|
||||
if (videoCodecID == "x265")
|
||||
{
|
||||
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
|
||||
{
|
||||
return "x265";
|
||||
}
|
||||
return "x265";
|
||||
}
|
||||
|
||||
if (videoFormat == "hevc")
|
||||
{
|
||||
return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265");
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
|
||||
{
|
||||
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
|
||||
{
|
||||
return "MPEG2";
|
||||
}
|
||||
|
||||
if (videoCodecID.IsNullOrWhiteSpace())
|
||||
{
|
||||
return "MPEG";
|
||||
}
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
|
||||
if (videoFormat == "mpeg2video")
|
||||
{
|
||||
return "MPEG2";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
|
||||
if (videoFormat == "mpeg1video")
|
||||
{
|
||||
if (videoCodecID.ContainsIgnoreCase("XVID") ||
|
||||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
|
||||
return "MPEG";
|
||||
}
|
||||
|
||||
if (videoFormat == "mpeg4")
|
||||
{
|
||||
if (videoCodecID == "XVID")
|
||||
{
|
||||
return "XviD";
|
||||
}
|
||||
|
||||
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
|
||||
videoCodecID.ContainsIgnoreCase("DIVX") ||
|
||||
videoCodecID.ContainsIgnoreCase("DX50") ||
|
||||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
|
||||
if (videoCodecID == "DIV3" ||
|
||||
videoCodecID == "DIVX" ||
|
||||
videoCodecID == "DX50")
|
||||
{
|
||||
return "DivX";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
|
||||
{
|
||||
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
|
||||
if (result.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (videoCodecLibrary.Contains("Lavc"))
|
||||
{
|
||||
return ""; // libavcodec mpeg-4
|
||||
}
|
||||
|
||||
if (videoCodecLibrary.Contains("em4v"))
|
||||
{
|
||||
return ""; // NeroDigital
|
||||
}
|
||||
|
||||
if (videoCodecLibrary.Contains("Intel(R) IPP"))
|
||||
{
|
||||
return ""; // Intel(R) IPP
|
||||
}
|
||||
|
||||
if (videoCodecLibrary.Contains("ZJMedia") ||
|
||||
videoCodecLibrary.Contains("DigiArty"))
|
||||
{
|
||||
return ""; // Other
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(videoCodecLibrary))
|
||||
{
|
||||
return ""; // Unknown mp4v
|
||||
}
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("VC-1"))
|
||||
if (videoFormat == "vc1")
|
||||
{
|
||||
return "VC1";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("AV1"))
|
||||
if (videoFormat == "av1")
|
||||
{
|
||||
return "AV1";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
|
||||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
|
||||
if (videoFormat == "vp6" ||
|
||||
videoFormat == "vp7" ||
|
||||
videoFormat == "vp8" ||
|
||||
videoFormat == "vp9")
|
||||
{
|
||||
return videoFormat.First().ToUpperInvariant();
|
||||
return videoFormat.ToUpperInvariant();
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
|
||||
if (videoFormat == "wmv1" ||
|
||||
videoFormat == "wmv2")
|
||||
{
|
||||
return "WMV";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
|
||||
if (videoFormat == "qtrle" ||
|
||||
videoFormat == "rpza" ||
|
||||
videoFormat == "rv10" ||
|
||||
videoFormat == "rv20" ||
|
||||
videoFormat == "rv30" ||
|
||||
videoFormat == "rv40")
|
||||
{
|
||||
return "DivX";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("XviD"))
|
||||
{
|
||||
return "XviD";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
|
||||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("mp42") ||
|
||||
videoFormat.ContainsIgnoreCase("mp43"))
|
||||
{
|
||||
// MS old DivX competitor
|
||||
return "";
|
||||
}
|
||||
|
||||
Logger.Debug()
|
||||
.Message("Unknown video format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.VideoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName)
|
||||
.WriteSentryWarn("UnknownVideoFormat", mediaInfo.ContainerFormat, mediaInfo.VideoFormat, videoCodecID)
|
||||
.Message("Unknown video format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
|
||||
.WriteSentryWarn("UnknownVideoFormatFFProbe", mediaInfo.ContainerFormat, videoFormat, videoCodecID)
|
||||
.Write();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
|
||||
{
|
||||
var videoCodec = mediaInfo.VideoCodec;
|
||||
|
||||
if (videoCodec.IsNullOrWhiteSpace())
|
||||
{
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
if (videoCodec == "AVC")
|
||||
{
|
||||
return GetSceneNameMatch(sceneName, "AVC", "h264", "x264");
|
||||
}
|
||||
|
||||
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
|
||||
{
|
||||
return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265");
|
||||
}
|
||||
|
||||
if (videoCodec == "MPEG-2 Video")
|
||||
{
|
||||
return "MPEG2";
|
||||
}
|
||||
|
||||
if (videoCodec == "MPEG-4 Visual")
|
||||
{
|
||||
return GetSceneNameMatch(sceneName, "DivX", "XviD");
|
||||
}
|
||||
|
||||
if (videoCodec.StartsWithIgnoreCase("XviD"))
|
||||
{
|
||||
return "XviD";
|
||||
}
|
||||
|
||||
if (videoCodec.StartsWithIgnoreCase("DivX"))
|
||||
{
|
||||
return "DivX";
|
||||
}
|
||||
|
||||
if (videoCodec.EqualsIgnoreCase("VC-1"))
|
||||
{
|
||||
return "VC1";
|
||||
}
|
||||
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoModel mediaInfo)
|
||||
{
|
||||
var audioChannelPositions = mediaInfo.AudioChannelPositions;
|
||||
var audioFormat = mediaInfo.AudioFormat;
|
||||
|
||||
if (audioChannelPositions.IsNullOrWhiteSpace())
|
||||
if (mediaInfo.AudioChannelPositions == null)
|
||||
{
|
||||
return null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
var match = PositionRegex.Match(mediaInfo.AudioChannelPositions);
|
||||
if (match.Success)
|
||||
{
|
||||
if (audioChannelPositions.Contains("+"))
|
||||
{
|
||||
return audioChannelPositions.Split('+')
|
||||
.Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (audioChannelPositions.Contains("/"))
|
||||
{
|
||||
var channelStringList = Regex.Replace(audioChannelPositions,
|
||||
@"^\d+\sobjects",
|
||||
"",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
.Replace("Object Based / ", "")
|
||||
.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault()
|
||||
?.Split('/');
|
||||
|
||||
var positions = default(decimal);
|
||||
|
||||
if (channelStringList == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var channel in channelStringList)
|
||||
{
|
||||
var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None);
|
||||
|
||||
if (channelSplit.Length == 3)
|
||||
{
|
||||
positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
positions += decimal.Parse(channel, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn()
|
||||
.Message("Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}' and '{1}'. Error {2}", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer, ex.Message)
|
||||
.WriteSentryWarn("UnknownAudioChannelFormat", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer)
|
||||
.Write();
|
||||
return decimal.Parse(match.Groups["position"].Value, NumberStyles.Number, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel mediaInfo)
|
||||
{
|
||||
var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer;
|
||||
var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream;
|
||||
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
|
||||
var audioChannelsStream = mediaInfo.AudioChannelsStream;
|
||||
|
||||
//Skip if the positions texts give us nothing
|
||||
if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") &&
|
||||
(audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (audioChannelsStream > 0)
|
||||
{
|
||||
return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream;
|
||||
}
|
||||
|
||||
return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText' or 'AudioChannelPositionsTextStream', with value of: '{0}' and '{1}", audioChannelPositionsTextContainer, audioChannelPositionsTextStream);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel mediaInfo)
|
||||
{
|
||||
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
|
||||
var audioChannelsStream = mediaInfo.AudioChannelsStream;
|
||||
|
||||
var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio
|
||||
if (audioFormat.ContainsIgnoreCase("DTS") &&
|
||||
splitAdditionalFeatures.ContainsIgnoreCase("XLL") &&
|
||||
splitAdditionalFeatures.ContainsIgnoreCase("X") &&
|
||||
audioChannelsContainer > 0)
|
||||
{
|
||||
return audioChannelsContainer - 1 + 0.1m;
|
||||
}
|
||||
|
||||
// FLAC 6 channels is likely 5.1
|
||||
if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6)
|
||||
{
|
||||
return 5.1m;
|
||||
}
|
||||
|
||||
if (mediaInfo.SchemaRevision > 5)
|
||||
{
|
||||
return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer;
|
||||
}
|
||||
|
||||
if (mediaInfo.SchemaRevision >= 3)
|
||||
{
|
||||
return audioChannelsContainer;
|
||||
}
|
||||
|
||||
return null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
|
||||
@@ -603,23 +301,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
|
||||
public static string FormatVideoDynamicRange(MediaInfoModel mediaInfo)
|
||||
{
|
||||
if (mediaInfo.VideoHdrFormat.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return VideoDynamicRangeHdr;
|
||||
}
|
||||
|
||||
if (mediaInfo.VideoBitDepth >= 10 &&
|
||||
mediaInfo.VideoColourPrimaries.IsNotNullOrWhiteSpace() &&
|
||||
mediaInfo.VideoTransferCharacteristics.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
if (mediaInfo.VideoColourPrimaries.EqualsIgnoreCase(ValidHdrColourPrimaries) &&
|
||||
ValidHdrTransferFunctions.Any(mediaInfo.VideoTransferCharacteristics.Contains))
|
||||
{
|
||||
return VideoDynamicRangeHdr;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
return mediaInfo.VideoHdrFormat != HdrFormat.None ? VideoDynamicRangeHdr : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
{
|
||||
[Flags]
|
||||
public enum BufferStatus
|
||||
{
|
||||
Accepted = 1,
|
||||
Filled = 2,
|
||||
Updated = 4,
|
||||
Finalized = 8
|
||||
}
|
||||
|
||||
public enum StreamKind
|
||||
{
|
||||
General,
|
||||
Video,
|
||||
Audio,
|
||||
Text,
|
||||
Other,
|
||||
Image,
|
||||
Menu
|
||||
}
|
||||
|
||||
public enum InfoKind
|
||||
{
|
||||
Name,
|
||||
Text,
|
||||
Measure,
|
||||
Options,
|
||||
NameText,
|
||||
MeasureText,
|
||||
Info,
|
||||
HowTo
|
||||
}
|
||||
|
||||
public enum InfoOptions
|
||||
{
|
||||
ShowInInform,
|
||||
Support,
|
||||
ShowInSupported,
|
||||
TypeOfValue
|
||||
}
|
||||
|
||||
public enum InfoFileOptions
|
||||
{
|
||||
FileOption_Nothing = 0x00,
|
||||
FileOption_NoRecursive = 0x01,
|
||||
FileOption_CloseAll = 0x02,
|
||||
FileOption_Max = 0x04
|
||||
}
|
||||
|
||||
public class MediaInfo : IDisposable
|
||||
{
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfo));
|
||||
private IntPtr _handle;
|
||||
|
||||
public bool MustUseAnsi { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
|
||||
public MediaInfo()
|
||||
{
|
||||
_handle = MediaInfo_New();
|
||||
|
||||
InitializeEncoding();
|
||||
}
|
||||
|
||||
~MediaInfo()
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
MediaInfo_Delete(_handle);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
MediaInfo_Delete(_handle);
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void InitializeEncoding()
|
||||
{
|
||||
if (Environment.OSVersion.ToString().IndexOf("Windows") != -1)
|
||||
{
|
||||
// Windows guaranteed UCS-2
|
||||
MustUseAnsi = false;
|
||||
Encoding = Encoding.Unicode;
|
||||
}
|
||||
else
|
||||
{
|
||||
var responses = new List<string>();
|
||||
|
||||
// Linux normally UCS-4. As fallback we try UCS-2 and plain Ansi.
|
||||
MustUseAnsi = false;
|
||||
Encoding = Encoding.UTF32;
|
||||
|
||||
var version = Option("Info_Version", "");
|
||||
responses.Add(version);
|
||||
if (version.StartsWith("MediaInfoLib"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Encoding = Encoding.Unicode;
|
||||
|
||||
version = Option("Info_Version", "");
|
||||
responses.Add(version);
|
||||
if (version.StartsWith("MediaInfoLib"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MustUseAnsi = true;
|
||||
Encoding = Encoding.Default;
|
||||
|
||||
version = Option("Info_Version", "");
|
||||
responses.Add(version);
|
||||
if (version.StartsWith("MediaInfoLib"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotSupportedException("Unsupported MediaInfoLib encoding, version check responses (may be gibberish, show it to the Radarr devs): " + responses.Join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr MakeStringParameter(string value)
|
||||
{
|
||||
var buffer = Encoding.GetBytes(value);
|
||||
|
||||
Array.Resize(ref buffer, buffer.Length + 4);
|
||||
|
||||
var buf = Marshal.AllocHGlobal(buffer.Length);
|
||||
Marshal.Copy(buffer, 0, buf, buffer.Length);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private string MakeStringResult(IntPtr value)
|
||||
{
|
||||
if (Encoding == Encoding.Unicode)
|
||||
{
|
||||
return Marshal.PtrToStringUni(value);
|
||||
}
|
||||
else if (Encoding == Encoding.UTF32)
|
||||
{
|
||||
int i = 0;
|
||||
for (; i < 1024; i += 4)
|
||||
{
|
||||
var data = Marshal.ReadInt32(value, i);
|
||||
if (data == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var buffer = new byte[i];
|
||||
Marshal.Copy(value, buffer, 0, i);
|
||||
|
||||
return Encoding.GetString(buffer, 0, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Marshal.PtrToStringAnsi(value);
|
||||
}
|
||||
}
|
||||
|
||||
public int Open(string fileName)
|
||||
{
|
||||
var pFileName = MakeStringParameter(fileName);
|
||||
try
|
||||
{
|
||||
if (MustUseAnsi)
|
||||
{
|
||||
return (int)MediaInfoA_Open(_handle, pFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (int)MediaInfo_Open(_handle, pFileName);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public int Open(Stream stream)
|
||||
{
|
||||
if (stream.Length < 1024)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var isValid = (int)MediaInfo_Open_Buffer_Init(_handle, stream.Length, 0);
|
||||
if (isValid == 1)
|
||||
{
|
||||
var buffer = new byte[16 * 1024];
|
||||
long seekStart = 0;
|
||||
long totalRead = 0;
|
||||
int bufferRead;
|
||||
|
||||
do
|
||||
{
|
||||
bufferRead = stream.Read(buffer, 0, buffer.Length);
|
||||
totalRead += bufferRead;
|
||||
|
||||
var status = (BufferStatus)MediaInfo_Open_Buffer_Continue(_handle, buffer, (IntPtr)bufferRead);
|
||||
|
||||
if (status.HasFlag(BufferStatus.Finalized) || status <= 0 || bufferRead == 0)
|
||||
{
|
||||
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
|
||||
break;
|
||||
}
|
||||
|
||||
var seekPos = MediaInfo_Open_Buffer_Continue_GoTo_Get(_handle);
|
||||
if (seekPos != -1)
|
||||
{
|
||||
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
|
||||
seekPos = stream.Seek(seekPos, SeekOrigin.Begin);
|
||||
seekStart = seekPos;
|
||||
MediaInfo_Open_Buffer_Init(_handle, stream.Length, seekPos);
|
||||
}
|
||||
}
|
||||
while (bufferRead > 0);
|
||||
|
||||
MediaInfo_Open_Buffer_Finalize(_handle);
|
||||
|
||||
Logger.Trace("Read a total of {0} bytes ({1:0.0}%)", totalRead, totalRead * 100.0 / stream.Length);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
MediaInfo_Close(_handle);
|
||||
}
|
||||
|
||||
public string Get(StreamKind streamKind, int streamNumber, string parameter, InfoKind infoKind = InfoKind.Text, InfoKind searchKind = InfoKind.Name)
|
||||
{
|
||||
var pParameter = MakeStringParameter(parameter);
|
||||
try
|
||||
{
|
||||
if (MustUseAnsi)
|
||||
{
|
||||
return MakeStringResult(MediaInfoA_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MakeStringResult(MediaInfo_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pParameter);
|
||||
}
|
||||
}
|
||||
|
||||
public string Get(StreamKind streamKind, int streamNumber, int parameter, InfoKind infoKind)
|
||||
{
|
||||
if (MustUseAnsi)
|
||||
{
|
||||
return MakeStringResult(MediaInfoA_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MakeStringResult(MediaInfo_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
|
||||
}
|
||||
}
|
||||
|
||||
public string Option(string option, string value)
|
||||
{
|
||||
var pOption = MakeStringParameter(option.ToLowerInvariant());
|
||||
var pValue = MakeStringParameter(value);
|
||||
try
|
||||
{
|
||||
if (MustUseAnsi)
|
||||
{
|
||||
return MakeStringResult(MediaInfoA_Option(_handle, pOption, pValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MakeStringResult(MediaInfo_Option(_handle, pOption, pValue));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pOption);
|
||||
Marshal.FreeHGlobal(pValue);
|
||||
}
|
||||
}
|
||||
|
||||
public int State_Get()
|
||||
{
|
||||
return (int)MediaInfo_State_Get(_handle);
|
||||
}
|
||||
|
||||
public int Count_Get(StreamKind streamKind, int streamNumber = -1)
|
||||
{
|
||||
return (int)MediaInfo_Count_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber);
|
||||
}
|
||||
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_New();
|
||||
[DllImport("mediainfo")]
|
||||
private static extern void MediaInfo_Delete(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Open(IntPtr handle, IntPtr fileName);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern long MediaInfo_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Open_Buffer_Finalize(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern void MediaInfo_Close(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Option(IntPtr handle, IntPtr option, IntPtr value);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_State_Get(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfo_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
|
||||
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_New();
|
||||
[DllImport("mediainfo")]
|
||||
private static extern void MediaInfoA_Delete(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Open(IntPtr handle, IntPtr fileName);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern long MediaInfoA_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Open_Buffer_Finalize(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern void MediaInfoA_Close(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Option(IntPtr handle, IntPtr option, IntPtr value);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_State_Get(IntPtr handle);
|
||||
[DllImport("mediainfo")]
|
||||
private static extern IntPtr MediaInfoA_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using FFMpegCore;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
{
|
||||
public class MediaInfoModel : IEmbeddedDocument
|
||||
{
|
||||
public string ContainerFormat { get; set; }
|
||||
|
||||
// Deprecated according to MediaInfo
|
||||
public string VideoCodec { get; set; }
|
||||
public string VideoFormat { get; set; }
|
||||
public string VideoCodecID { get; set; }
|
||||
public string VideoProfile { get; set; }
|
||||
public string VideoCodecLibrary { get; set; }
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBitDepth { get; set; }
|
||||
public int VideoMultiViewCount { get; set; }
|
||||
public string VideoColourPrimaries { get; set; }
|
||||
public string VideoTransferCharacteristics { get; set; }
|
||||
public string VideoHdrFormat { get; set; }
|
||||
public string VideoHdrFormatCompatibility { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string AudioFormat { get; set; }
|
||||
public string AudioCodecID { get; set; }
|
||||
public string AudioCodecLibrary { get; set; }
|
||||
public string AudioAdditionalFeatures { get; set; }
|
||||
public int AudioBitrate { get; set; }
|
||||
public TimeSpan RunTime { get; set; }
|
||||
public int AudioStreamCount { get; set; }
|
||||
public int AudioChannelsContainer { get; set; }
|
||||
public int AudioChannelsStream { get; set; }
|
||||
public string AudioChannelPositions { get; set; }
|
||||
public string AudioChannelPositionsTextContainer { get; set; }
|
||||
public string AudioChannelPositionsTextStream { get; set; }
|
||||
public string AudioProfile { get; set; }
|
||||
public decimal VideoFps { get; set; }
|
||||
public string AudioLanguages { get; set; }
|
||||
public string Subtitles { get; set; }
|
||||
public string ScanType { get; set; }
|
||||
public string RawStreamData { get; set; }
|
||||
public string RawFrameData { get; set; }
|
||||
public int SchemaRevision { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IMediaAnalysis Analysis => FFProbe.Analyse(RawStreamData);
|
||||
|
||||
[JsonIgnore]
|
||||
public IMediaAnalysis Frames => FFProbe.Analyse(RawFrameData);
|
||||
|
||||
public string ContainerFormat { get; set; }
|
||||
public string VideoFormat { get; set; }
|
||||
|
||||
public string VideoCodecID { get; set; }
|
||||
|
||||
public string VideoProfile { get; set; }
|
||||
|
||||
public int VideoBitrate { get; set; }
|
||||
|
||||
public int VideoBitDepth { get; set; }
|
||||
|
||||
public int VideoMultiViewCount { get; set; }
|
||||
|
||||
public string VideoColourPrimaries { get; set; }
|
||||
|
||||
public string VideoTransferCharacteristics { get; set; }
|
||||
|
||||
public DoviConfigurationRecordSideData DoviConfigurationRecord { get; set; }
|
||||
|
||||
public HdrFormat VideoHdrFormat { get; set; }
|
||||
|
||||
public int Height { get; set; }
|
||||
|
||||
public int Width { get; set; }
|
||||
|
||||
public string AudioFormat { get; set; }
|
||||
|
||||
public string AudioCodecID { get; set; }
|
||||
|
||||
public string AudioProfile { get; set; }
|
||||
|
||||
public int AudioBitrate { get; set; }
|
||||
|
||||
public TimeSpan RunTime { get; set; }
|
||||
|
||||
public int AudioStreamCount { get; set; }
|
||||
|
||||
public int AudioChannels { get; set; }
|
||||
|
||||
public string AudioChannelPositions { get; set; }
|
||||
|
||||
public decimal VideoFps { get; set; }
|
||||
|
||||
public List<string> AudioLanguages { get; set; }
|
||||
|
||||
public List<string> Subtitles { get; set; }
|
||||
|
||||
public string ScanType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FFMpegCore;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -17,14 +19,33 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
private readonly List<FFProbePixelFormat> _pixelFormats;
|
||||
|
||||
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 4;
|
||||
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 7;
|
||||
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8;
|
||||
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 8;
|
||||
|
||||
private static readonly string[] ValidHdrColourPrimaries = { "bt2020" };
|
||||
private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" };
|
||||
private static readonly string[] PqTransferFunctions = { "smpte2084" };
|
||||
private static readonly string[] ValidHdrTransferFunctions = HlgTransferFunctions.Concat(PqTransferFunctions).ToArray();
|
||||
|
||||
public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
|
||||
// We bundle ffprobe for all platforms
|
||||
GlobalFFOptions.Configure(options => options.BinaryFolder = AppDomain.CurrentDomain.BaseDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
_pixelFormats = FFProbe.GetPixelFormats();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to get supported pixel formats from ffprobe");
|
||||
_pixelFormats = new List<FFProbePixelFormat>();
|
||||
}
|
||||
}
|
||||
|
||||
public MediaInfoModel GetMediaInfo(string filename)
|
||||
@@ -34,181 +55,77 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
throw new FileNotFoundException("Media file does not exist: " + filename);
|
||||
}
|
||||
|
||||
MediaInfo mediaInfo = null;
|
||||
|
||||
// TODO: Cache media info by path, mtime and length so we don't need to read files multiple times
|
||||
try
|
||||
{
|
||||
mediaInfo = new MediaInfo();
|
||||
_logger.Debug("Getting media info from {0}", filename);
|
||||
var ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 50000000" });
|
||||
|
||||
if (filename.ToLower().EndsWith(".ts"))
|
||||
var analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
|
||||
|
||||
if (analysis.PrimaryAudioStream.ChannelLayout.IsNullOrWhiteSpace())
|
||||
{
|
||||
// For .ts files we often have to scan more of the file to get all the info we need
|
||||
mediaInfo.Option("ParseSpeed", "0.3");
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaInfo.Option("ParseSpeed", "0.0");
|
||||
ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 150000000 -analyzeduration 150000000" });
|
||||
analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
|
||||
}
|
||||
|
||||
int open;
|
||||
|
||||
using (var stream = _diskProvider.OpenReadStream(filename))
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
open = mediaInfo.Open(stream);
|
||||
ContainerFormat = analysis.Format.FormatName,
|
||||
VideoFormat = analysis.PrimaryVideoStream?.CodecName,
|
||||
VideoCodecID = analysis.PrimaryVideoStream?.CodecTagString,
|
||||
VideoProfile = analysis.PrimaryVideoStream?.Profile,
|
||||
VideoBitrate = analysis.PrimaryVideoStream?.BitRate ?? 0,
|
||||
VideoMultiViewCount = 1,
|
||||
VideoBitDepth = GetPixelFormat(analysis.PrimaryVideoStream?.PixelFormat).Components.Min(x => x.BitDepth),
|
||||
VideoColourPrimaries = analysis.PrimaryVideoStream?.ColorPrimaries,
|
||||
VideoTransferCharacteristics = analysis.PrimaryVideoStream?.ColorTransfer,
|
||||
DoviConfigurationRecord = analysis.PrimaryVideoStream?.SideDataList?.Find(x => x.GetType().Name == nameof(DoviConfigurationRecordSideData)) as DoviConfigurationRecordSideData,
|
||||
Height = analysis.PrimaryVideoStream?.Height ?? 0,
|
||||
Width = analysis.PrimaryVideoStream?.Width ?? 0,
|
||||
AudioFormat = analysis.PrimaryAudioStream?.CodecName,
|
||||
AudioCodecID = analysis.PrimaryAudioStream?.CodecTagString,
|
||||
AudioProfile = analysis.PrimaryAudioStream?.Profile,
|
||||
AudioBitrate = analysis.PrimaryAudioStream?.BitRate ?? 0,
|
||||
RunTime = GetBestRuntime(analysis.PrimaryAudioStream?.Duration, analysis.PrimaryVideoStream.Duration, analysis.Format.Duration),
|
||||
AudioStreamCount = analysis.AudioStreams.Count,
|
||||
AudioChannels = analysis.PrimaryAudioStream?.Channels ?? 0,
|
||||
AudioChannelPositions = analysis.PrimaryAudioStream?.ChannelLayout,
|
||||
VideoFps = analysis.PrimaryVideoStream?.FrameRate ?? 0,
|
||||
AudioLanguages = analysis.AudioStreams?.Select(x => x.Language)
|
||||
.Where(l => l.IsNotNullOrWhiteSpace())
|
||||
.ToList(),
|
||||
Subtitles = analysis.SubtitleStreams?.Select(x => x.Language)
|
||||
.Where(l => l.IsNotNullOrWhiteSpace())
|
||||
.ToList(),
|
||||
ScanType = "Progressive",
|
||||
RawStreamData = ffprobeOutput,
|
||||
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
|
||||
};
|
||||
|
||||
FFProbeFrames frames = null;
|
||||
|
||||
// if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is
|
||||
if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics))
|
||||
{
|
||||
var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = "-read_intervals \"%+#1\" -select_streams v" });
|
||||
mediaInfoModel.RawFrameData = frameOutput;
|
||||
|
||||
frames = FFProbe.AnalyseFrameJson(frameOutput);
|
||||
}
|
||||
|
||||
if (open != 0)
|
||||
{
|
||||
int audioRuntime;
|
||||
int videoRuntime;
|
||||
int generalRuntime;
|
||||
var streamSideData = analysis.PrimaryVideoStream?.SideDataList ?? new ();
|
||||
var framesSideData = frames?.Frames[0]?.SideDataList ?? new ();
|
||||
|
||||
// Runtime
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
|
||||
var sideData = streamSideData.Concat(framesSideData).ToList();
|
||||
mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData);
|
||||
|
||||
// Audio Channels
|
||||
var audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
|
||||
int.TryParse(audioChannelsStr, out var audioChannels);
|
||||
|
||||
if (audioRuntime == 0 && videoRuntime == 0 && generalRuntime == 0)
|
||||
{
|
||||
// No runtime, ask mediainfo to scan the whole file
|
||||
_logger.Trace("No runtime value found, rescanning at 1.0 scan depth");
|
||||
mediaInfo.Option("ParseSpeed", "1.0");
|
||||
|
||||
using (var stream = _diskProvider.OpenReadStream(filename))
|
||||
{
|
||||
open = mediaInfo.Open(stream);
|
||||
}
|
||||
}
|
||||
else if (audioChannels > 2 && audioChannelPositions.IsNullOrWhiteSpace())
|
||||
{
|
||||
// Some files with DTS don't have ChannelPositions unless more of the file is scanned
|
||||
_logger.Trace("DTS audio without expected channel information, rescanning at 0.3 scan depth");
|
||||
mediaInfo.Option("ParseSpeed", "0.3");
|
||||
|
||||
using (var stream = _diskProvider.OpenReadStream(filename))
|
||||
{
|
||||
open = mediaInfo.Open(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open != 0)
|
||||
{
|
||||
int width;
|
||||
int height;
|
||||
int videoBitRate;
|
||||
int audioBitRate;
|
||||
int audioRuntime;
|
||||
int videoRuntime;
|
||||
int generalRuntime;
|
||||
int streamCount;
|
||||
int audioChannels;
|
||||
int audioChannelsOrig;
|
||||
int videoBitDepth;
|
||||
decimal videoFrameRate;
|
||||
int videoMultiViewCount;
|
||||
|
||||
string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List");
|
||||
string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType");
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate);
|
||||
if (videoBitRate <= 0)
|
||||
{
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate_Nominal"), out videoBitRate);
|
||||
}
|
||||
|
||||
decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "MultiView_Count"), out videoMultiViewCount);
|
||||
|
||||
//Runtime
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
|
||||
|
||||
string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
|
||||
int.TryParse(aBitRate, out audioBitRate);
|
||||
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount);
|
||||
|
||||
string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
int.TryParse(audioChannelsStr, out audioChannels);
|
||||
|
||||
string audioChannelsStrOrig = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)_Original").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
int.TryParse(audioChannelsStrOrig, out audioChannelsOrig);
|
||||
|
||||
var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions");
|
||||
var audioChannelPositionsTextOrig = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions_Original");
|
||||
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
|
||||
|
||||
string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List");
|
||||
|
||||
string videoProfile = mediaInfo.Get(StreamKind.Video, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
|
||||
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
ContainerFormat = mediaInfo.Get(StreamKind.General, 0, "Format"),
|
||||
VideoFormat = mediaInfo.Get(StreamKind.Video, 0, "Format"),
|
||||
VideoCodecID = mediaInfo.Get(StreamKind.Video, 0, "CodecID"),
|
||||
VideoProfile = videoProfile,
|
||||
VideoCodecLibrary = mediaInfo.Get(StreamKind.Video, 0, "Encoded_Library"),
|
||||
VideoBitrate = videoBitRate,
|
||||
VideoBitDepth = videoBitDepth,
|
||||
VideoMultiViewCount = videoMultiViewCount,
|
||||
VideoColourPrimaries = mediaInfo.Get(StreamKind.Video, 0, "colour_primaries"),
|
||||
VideoTransferCharacteristics = mediaInfo.Get(StreamKind.Video, 0, "transfer_characteristics"),
|
||||
VideoHdrFormat = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format"),
|
||||
VideoHdrFormatCompatibility = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format_Compatibility"),
|
||||
Height = height,
|
||||
Width = width,
|
||||
AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"),
|
||||
AudioCodecID = mediaInfo.Get(StreamKind.Audio, 0, "CodecID"),
|
||||
AudioProfile = audioProfile,
|
||||
AudioCodecLibrary = mediaInfo.Get(StreamKind.Audio, 0, "Encoded_Library"),
|
||||
AudioAdditionalFeatures = mediaInfo.Get(StreamKind.Audio, 0, "Format_AdditionalFeatures"),
|
||||
AudioBitrate = audioBitRate,
|
||||
RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime),
|
||||
AudioStreamCount = streamCount,
|
||||
AudioChannelsContainer = audioChannels,
|
||||
AudioChannelsStream = audioChannelsOrig,
|
||||
AudioChannelPositions = audioChannelPositions,
|
||||
AudioChannelPositionsTextContainer = audioChannelPositionsText,
|
||||
AudioChannelPositionsTextStream = audioChannelPositionsTextOrig,
|
||||
VideoFps = videoFrameRate,
|
||||
AudioLanguages = audioLanguages,
|
||||
Subtitles = subtitles,
|
||||
ScanType = scanType,
|
||||
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
|
||||
};
|
||||
|
||||
return mediaInfoModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Unable to open media info from file: " + filename);
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException ex)
|
||||
{
|
||||
_logger.Error(ex, "mediainfo is required but was not found");
|
||||
return mediaInfoModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to parse media info from file: {0}", filename);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mediaInfo?.Close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -220,19 +137,70 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
|
||||
return info?.RunTime;
|
||||
}
|
||||
|
||||
private TimeSpan GetBestRuntime(int audio, int video, int general)
|
||||
private static TimeSpan GetBestRuntime(TimeSpan? audio, TimeSpan? video, TimeSpan general)
|
||||
{
|
||||
if (video == 0)
|
||||
if (!video.HasValue || video.Value.TotalMilliseconds == 0)
|
||||
{
|
||||
if (audio == 0)
|
||||
if (!audio.HasValue || audio.Value.TotalMilliseconds == 0)
|
||||
{
|
||||
return TimeSpan.FromMilliseconds(general);
|
||||
return general;
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(audio);
|
||||
return audio.Value;
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(video);
|
||||
return video.Value;
|
||||
}
|
||||
|
||||
private FFProbePixelFormat GetPixelFormat(string format)
|
||||
{
|
||||
return _pixelFormats.Find(x => x.Name == format);
|
||||
}
|
||||
|
||||
public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List<SideData> sideData)
|
||||
{
|
||||
if (bitDepth < 10)
|
||||
{
|
||||
return HdrFormat.None;
|
||||
}
|
||||
|
||||
if (TryFindSideData(sideData, nameof(DoviConfigurationRecordSideData)))
|
||||
{
|
||||
return HdrFormat.DolbyVision;
|
||||
}
|
||||
|
||||
if (!ValidHdrColourPrimaries.Contains(colorPrimaries) || !ValidHdrTransferFunctions.Contains(transferFunction))
|
||||
{
|
||||
return HdrFormat.None;
|
||||
}
|
||||
|
||||
if (HlgTransferFunctions.Contains(transferFunction))
|
||||
{
|
||||
return HdrFormat.Hlg10;
|
||||
}
|
||||
|
||||
if (PqTransferFunctions.Contains(transferFunction))
|
||||
{
|
||||
if (TryFindSideData(sideData, nameof(HdrDynamicMetadataSpmte2094)))
|
||||
{
|
||||
return HdrFormat.Hdr10Plus;
|
||||
}
|
||||
|
||||
if (TryFindSideData(sideData, nameof(MasteringDisplayMetadata)) ||
|
||||
TryFindSideData(sideData, nameof(ContentLightLevelMetadata)))
|
||||
{
|
||||
return HdrFormat.Hdr10;
|
||||
}
|
||||
|
||||
return HdrFormat.Pq10;
|
||||
}
|
||||
|
||||
return HdrFormat.None;
|
||||
}
|
||||
|
||||
private static bool TryFindSideData(List<SideData> list, string typeName)
|
||||
{
|
||||
return list?.Find(x => x.GetType().Name == typeName) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Aggregation.Aggregators.Augmenter
|
||||
return null;
|
||||
}
|
||||
|
||||
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Split('/').Select(l => l.Trim()).Distinct().ToList();
|
||||
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Distinct().ToList();
|
||||
|
||||
var languages = new List<Languages.Language>();
|
||||
|
||||
foreach (var audioLanguage in audioLanguages)
|
||||
{
|
||||
var language = IsoLanguages.FindByName(audioLanguage)?.Language;
|
||||
var language = IsoLanguages.Find(audioLanguage)?.Language;
|
||||
languages.AddIfNotNull(language);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user