Files
FileFlowsPlugins/VideoLegacyNodes/VideoInfoHelper.cs
2022-06-28 10:26:15 +02:00

322 lines
14 KiB
C#

namespace FileFlows.VideoNodes
{
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using FileFlows.Plugin;
public class VideoInfoHelper
{
private string ffMpegExe;
private ILogger Logger;
static Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
static Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxDuration2 = new Regex(@"(?<=((^[\s]+Duration:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
static Regex rgxAudioSampleRate = new Regex(@"(?<=((,|\s)))[\d]+(?=([\s]?hz))", RegexOptions.IgnoreCase);
static int _ProbeSize = 25;
internal static int ProbeSize
{
get => _ProbeSize;
set
{
if (value < 5)
_ProbeSize = 5;
else if (value > 1000)
_ProbeSize = 1000;
else
_ProbeSize = value;
}
}
public VideoInfoHelper(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public VideoInfo Read(string filename)
{
var vi = new VideoInfo();
vi.FileName = filename;
if (File.Exists(filename) == false)
{
Logger.ELog("File not found: " + filename);
return vi;
}
if (string.IsNullOrEmpty(ffMpegExe) || File.Exists(ffMpegExe) == false)
{
Logger.ELog("FFMpeg not found: " + (ffMpegExe ?? "not passed in"));
return vi;
}
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffMpegExe;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
foreach (var arg in new[]
{
"-hide_banner",
"-probesize", ProbeSize + "M",
"-i",
filename,
})
{
process.StartInfo.ArgumentList.Add(arg);
}
process.Start();
string output = process.StandardError.ReadToEnd();
output = output.Replace("At least one output file must be specified", string.Empty).Trim();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified")
{
Logger.ELog("Failed reading ffmpeg info: " + error);
return vi;
}
Logger.ILog("Video Information:" + Environment.NewLine + output);
vi = ParseOutput(Logger, output);
}
}
catch (Exception ex)
{
Logger.ELog(ex.Message, ex.StackTrace.ToString());
}
return vi;
}
public static VideoInfo ParseOutput(ILogger logger, string output)
{
var vi = new VideoInfo();
var rgxStreams = new Regex(@"Stream\s#[\d]+:[\d]+(.*?)(?=(Stream\s#[\d]|$))", RegexOptions.Singleline);
var streamMatches = rgxStreams.Matches(output);
int streamIndex = 0;
// get a rough estimate, bitrate: 346 kb/s
var rgxBitrate = new Regex(@"(?<=(bitrate: ))[\d\.]+(?!=( kb/s))");
var brMatch = rgxBitrate.Match(output);
if (brMatch.Success)
{
vi.Bitrate = float.Parse(brMatch.Value) * 1_000; // to convert to b/s
}
vi.Chapters = ParseChapters(output);
int subtitleIndex = 0;
int videoIndex = 0;
int audioIndex = 0;
foreach (Match sm in streamMatches)
{
if (sm.Value.Contains(" Video: "))
{
var vs = ParseVideoStream(logger, sm.Value, output);
if (vs != null)
{
vs.Index = streamIndex;
vs.TypeIndex = videoIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
vs.IndexString = match.Value;
vi.VideoStreams.Add(vs);
}
++videoIndex;
}
else if (sm.Value.Contains(" Audio: "))
{
var audio = ParseAudioStream(sm.Value);
if (audio != null)
{
audio.TypeIndex = audioIndex;
audio.Index = streamIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
audio.IndexString = match.Value;
vi.AudioStreams.Add(audio);
}
++audioIndex;
}
else if (sm.Value.Contains(" Subtitle: "))
{
var sub = ParseSubtitleStream(sm.Value);
if (sub != null)
{
sub.Index = streamIndex;
sub.TypeIndex = subtitleIndex;
var match = Regex.Match(sm.Value, @"(?<=(Stream #))[\d]+:[\d]+");
if (match.Success)
sub.IndexString = match.Value;
vi.SubtitleStreams.Add(sub);
}
++subtitleIndex;
}
++streamIndex;
}
return vi;
}
private static List<Chapter> ParseChapters(string output)
{
try
{
var rgxChatpers = new Regex("(?<=(Chapters:))(.*?)(?=(Stream))", RegexOptions.Singleline);
string strChapters;
if (rgxChatpers.TryMatch(output, out Match matchChapters))
strChapters = matchChapters.Value.Trim();
else
return new List<Chapter>();
var rgxChapter = new Regex("Chapter #(.*?)(?=(Chapter #|$))", RegexOptions.Singleline);
var chapters = new List<Chapter>();
var rgxTitle = new Regex(@"title[\s]*:[\s]*(.*?)$");
var rgxStart = new Regex(@"(?<=(start[\s]))[\d]+\.[\d]+");
var rgxEnd = new Regex(@"(?<=(end[\s]))[\d]+\.[\d]+");
foreach (Match match in rgxChapter.Matches(strChapters))
{
try
{
Chapter chapter = new Chapter();
if (rgxTitle.TryMatch(match.Value.Trim(), out Match title))
chapter.Title = title.Groups[1].Value;
if (rgxStart.TryMatch(match.Value, out Match start))
{
double startSeconds = double.Parse(start.Value);
chapter.Start = TimeSpan.FromSeconds(startSeconds);
}
if (rgxEnd.TryMatch(match.Value, out Match end))
{
double endSeconds = double.Parse(end.Value);
chapter.End = TimeSpan.FromSeconds(endSeconds);
}
if (chapter.Start > TimeSpan.Zero || chapter.End > TimeSpan.Zero)
{
chapters.Add(chapter);
}
}
catch (Exception ) { }
}
return chapters;
}catch (Exception) { return new List<Chapter>(); }
}
public static VideoStream ParseVideoStream(ILogger logger, string info, string fullOutput)
{
// Stream #0:0(eng): Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
VideoStream vs = new VideoStream();
vs.Codec = line.Substring(line.IndexOf("Video: ") + "Video: ".Length).Replace(",", "").Trim().Split(' ').First().ToLower();
var dimensions = Regex.Match(line, @"([\d]{3,})x([\d]{3,})");
if (int.TryParse(dimensions.Groups[1].Value, out int width))
vs.Width = width;
if (int.TryParse(dimensions.Groups[2].Value, out int height))
vs.Height = height;
if (Regex.IsMatch(line, @"([\d]+(\.[\d]+)?)\sfps") && float.TryParse(Regex.Match(line, @"([\d]+(\.[\d]+)?)\sfps").Groups[1].Value, out float fps))
vs.FramesPerSecond = fps;
var rgxBps = new Regex(@"(?<=((BPS(\-[\w]+)?[\s]*:[\s])))([\d]+)");
if (rgxBps.IsMatch(info) && float.TryParse(rgxBps.Match(info).Value, out float bps))
vs.Bitrate = bps;
if (rgxDuration.IsMatch(info) && TimeSpan.TryParse(rgxDuration.Match(info).Value, out TimeSpan duration) && duration.TotalSeconds > 0)
{
vs.Duration = duration;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else if (rgxDuration2.IsMatch(fullOutput) && TimeSpan.TryParse(rgxDuration2.Match(fullOutput).Value, out TimeSpan duration2) && duration2.TotalSeconds > 0)
{
vs.Duration = duration2;
logger?.ILog("Video stream duration: " + vs.Duration);
}
else
{
logger?.ILog("Failed to read duration for VideoStream: " + info);
}
// As per https://video.stackexchange.com/a/33827
// "HDR is only the new transfer function" (PQ or HLG)
vs.HDR = info.Contains("arib-std-b67") || info.Contains("smpte2084");
return vs;
}
public static AudioStream ParseAudioStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
AudioStream audio = new AudioStream();
audio.Title = "";
// this isnt type index, this is overall index
audio.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value) - 1;
audio.Codec = parts[0].Substring(parts[0].IndexOf("Audio: ") + "Audio: ".Length).Trim().Split(' ').First().ToLower() ?? "";
audio.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
if (info.IndexOf("0 channels") >= 0)
{
audio.Channels = 0;
}
else
{
try
{
//Logger.ILog("codec: " + vs.Codec);
if (parts[2] == "stereo")
audio.Channels = 2;
else if (parts[2] == "mono")
audio.Channels = 1;
else if (Regex.IsMatch(parts[2], @"^[\d]+(\.[\d]+)?"))
{
audio.Channels = float.Parse(Regex.Match(parts[2], @"^[\d]+(\.[\d]+)?").Value);
}
}
catch (Exception) { }
}
var match = rgxAudioSampleRate.Match(info);
if (match.Success)
audio.SampleRate = int.Parse(match.Value);
if (rgxTitle.IsMatch(info))
audio.Title = rgxTitle.Match(info).Value.Trim();
if (rgxDuration.IsMatch(info))
audio.Duration = TimeSpan.Parse(rgxDuration.Match(info).Value);
return audio;
}
public static SubtitleStream ParseSubtitleStream(string info)
{
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, stereo, fltp, 1536 kb/s (default)
string line = info.Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).First();
var parts = line.Split(",").Select(x => x?.Trim() ?? "").ToArray();
SubtitleStream sub = new SubtitleStream();
sub.TypeIndex = int.Parse(Regex.Match(line, @"#([\d]+):([\d]+)").Groups[2].Value);
sub.Codec = line.Substring(line.IndexOf("Subtitle: ") + "Subtitle: ".Length).Trim().Split(' ').First().ToLower();
sub.Language = Regex.Match(line, @"(?<=(Stream\s#[\d]+:[\d]+)\()[^\)]+").Value?.ToLower() ?? "";
if (rgxTitle.IsMatch(info))
sub.Title = rgxTitle.Match(info).Value.Trim();
sub.Forced = info.ToLower().Contains("forced");
return sub;
}
}
}