diff --git a/VideoNodes/FFMpegEncoder.cs b/VideoNodes/FFMpegEncoder.cs index 5f886d95..32dfe02c 100644 --- a/VideoNodes/FFMpegEncoder.cs +++ b/VideoNodes/FFMpegEncoder.cs @@ -26,10 +26,13 @@ namespace FileFlows.VideoNodes this.Logger = logger; } - public bool Encode(string input, string output, string arguments) + public bool Encode(string input, string output, string arguments, bool dontAddInputFile = false) { // -y means it will overwrite a file if output already exists - arguments = $"-i \"{input}\" -y {arguments} \"{output}\""; + if(dontAddInputFile == false) + arguments = $"-i \"{input}\" -y {arguments} \"{output}\""; + else + arguments = $"{arguments} \"{output}\""; Logger.ILog(new string('=', ("FFMpeg.Arguments: " + arguments).Length)); Logger.ILog("FFMpeg.Arguments: " + arguments); diff --git a/VideoNodes/Tests/VideoInfoHelperTests.cs b/VideoNodes/Tests/VideoInfoHelperTests.cs index 7b9e2a57..11befab7 100644 --- a/VideoNodes/Tests/VideoInfoHelperTests.cs +++ b/VideoNodes/Tests/VideoInfoHelperTests.cs @@ -26,7 +26,7 @@ namespace VideoNodes.Tests { const string file = @"D:\videos\unprocessed\Bourne.mkv"; var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger()); - vi.Read(@"D:\videos\unprocessed\Bourne.mkv"); + var vii = vi.Read(@"D:\videos\unprocessed\Masters of the Universe (1987) Bluray-1080p.mkv.skip"); SubtitleRemover remover = new SubtitleRemover(); remover.SubtitlesToRemove = new List @@ -267,6 +267,29 @@ namespace VideoNodes.Tests Assert.AreEqual("ac3", reordered[7].Codec); Assert.AreEqual("fre", reordered[7].Language); } + + + [TestMethod] + public void ComskipTest() + { + const string file = @"D:\videos\unprocessed\The IT Crowd - 2x04 - The Dinner Party - No English.mkv"; + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\videos\temp"; + + + var vi = new VideoInfoHelper(@"C:\utils\ffmpeg\ffmpeg.exe", new TestLogger()); + var vii = vi.Read(file); + args.SetParameter("VideoInfo", vii); + //args.Process = new FileFlows.Plugin.ProcessHelper(args.Logger); + + var node = new ComskipRemoveAds(); + int output = node.Execute(args); + Assert.AreEqual(1, output); + } + } } diff --git a/VideoNodes/VideoNodes.csproj b/VideoNodes/VideoNodes.csproj index df927490..3546651d 100644 Binary files a/VideoNodes/VideoNodes.csproj and b/VideoNodes/VideoNodes.csproj differ diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 0b15e6ef..e3d57e5b 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -48,6 +48,13 @@ "Language-Help": "The ISO 639-2 language code to use. \nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes" } }, + "ComskipRemoveAds": { + "Description": "Uses a comskip EDL file and will remove commericals using that file.", + "Outputs": { + "1": "Commericals removed, saved to temporary file", + "2": "No commericals detected" + } + }, "VideoFile": { "Description": "An input video file that has had its VideoInformation read and can be processed", "Outputs": { diff --git a/VideoNodes/VideoNodes/ComskipRemoveAds.cs b/VideoNodes/VideoNodes/ComskipRemoveAds.cs new file mode 100644 index 00000000..dea74a62 --- /dev/null +++ b/VideoNodes/VideoNodes/ComskipRemoveAds.cs @@ -0,0 +1,128 @@ +namespace FileFlows.VideoNodes +{ + using FileFlows.Plugin; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + + public class ComskipRemoveAds: EncodingNode + { + public override int Outputs => 2; + + public override int Execute(NodeParameters args) + { + string ffmpegExe = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpegExe)) + return -1; + VideoInfo videoInfo = GetVideoInfo(args); + if (videoInfo == null) + return -1; + float totalTime = (float)videoInfo.VideoStreams[0].Duration.TotalSeconds; + + + string edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl"; + if(File.Exists(edlFile) == false) + edlFile = args.WorkingFile.Substring(0, args.WorkingFile.LastIndexOf(".") + 1) + "edl"; + if (File.Exists(edlFile) == false) + { + args.Logger?.ILog("No EDL file found for file"); + return 2; + } + + string text = File.ReadAllText(edlFile) ?? string.Empty; + float last = -1; + List breakPoints = new List(); + foreach(string line in text.Split(new string[] { "\r\n", "\n", "\r"}, StringSplitOptions.RemoveEmptyEntries)) + { + // 93526.47 93650.13 0 + string[] parts = line.Split(" "); + if (parts.Length < 2) + continue; + float start = 0; + float end = 0; + if (float.TryParse(parts[0], out start) == false || float.TryParse(parts[1], out end) == false) + continue; + + if (start < last) + continue; + + BreakPoint bp = new BreakPoint(); + bp.Start = start; + bp.End = end; + breakPoints.Add(bp); + } + + if(breakPoints.Any() == false) + { + args.Logger?.ILog("No break points detected in file"); + return 2; + } + + List segments = new List(); + + float segStart = 0; + string extension = args.WorkingFile.Substring(args.WorkingFile.LastIndexOf(".") + 1); + string segmentPrefix = Path.Combine(args.TempPath, Guid.NewGuid().ToString())+"_"; + int count = 0; + foreach (BreakPoint bp in breakPoints) + { + if(EncodeSegment(segStart, bp.Duration) == false) + { + args.Logger?.ELog("Failed to create segment: " + count); + return 2; + } + segStart = bp.End; + } + // add the end + if (EncodeSegment(segStart, totalTime - segStart) == false) + { + args.Logger?.ELog("Failed to create segment: " + count); + return 2; + } + + + // stitch file back together + string concatList = segmentPrefix + "concatlist.txt"; + File.WriteAllLines(concatList, segments.Select(x => $"file '{x}'")); + + bool concatResult = Encode(args, ffmpegExe, $"-f concat -safe 0 -i \"{concatList}\" -c copy", dontAddInputFile: true, extension: extension); + + foreach(string segment in segments.Union(new[] { concatList })) + { + try + { + File.Delete(segment); + } + catch (Exception) { } + } + + if (concatResult) + return 1; + args.Logger?.ELog("Failed to stitch file back together"); + return 2; + + bool EncodeSegment(float start, float duration) + { + string segment = segmentPrefix + (++count).ToString("D2") + "." + extension; + if (Encode(args, ffmpegExe, $"-ss {start} -t {duration} -c copy", outputFile: segment, updateWorkingFile: false)) + { + segments.Add(segment); + return true; + } + return false; + } + } + + private class BreakPoint + { + public float Start { get; set; } + public float End { get; set; } + + public float Duration => End - Start; + } + } +} diff --git a/VideoNodes/VideoNodes/EncodingNode.cs b/VideoNodes/VideoNodes/EncodingNode.cs index f100b7d7..7444a5b7 100644 --- a/VideoNodes/VideoNodes/EncodingNode.cs +++ b/VideoNodes/VideoNodes/EncodingNode.cs @@ -18,7 +18,7 @@ namespace FileFlows.VideoNodes private FFMpegEncoder Encoder; - protected bool Encode(NodeParameters args, string ffmpegExe, string ffmpegParameters, string extension = "mkv", string outputFile = "") + protected bool Encode(NodeParameters args, string ffmpegExe, string ffmpegParameters, string extension = "mkv", string outputFile = "", bool updateWorkingFile = true, bool dontAddInputFile = false) { if (string.IsNullOrEmpty(extension)) extension = "mkv"; @@ -30,9 +30,9 @@ namespace FileFlows.VideoNodes if (string.IsNullOrEmpty(outputFile)) outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + extension); - bool success = Encoder.Encode(args.WorkingFile, outputFile, ffmpegParameters); + bool success = Encoder.Encode(args.WorkingFile, outputFile, ffmpegParameters, dontAddInputFile: dontAddInputFile); args.Logger.ILog("Encoding succesful: " + success); - if (success) + if (success && updateWorkingFile) { args.SetWorkingFile(outputFile);