diff --git a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioConverter.cs b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioConverter.cs index a7a39cbd..5cd18a07 100644 --- a/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioConverter.cs +++ b/VideoNodes/FfmpegBuilderNodes/Audio/FfmpegBuilderAudioConverter.cs @@ -27,17 +27,64 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode { new () { Label = "AAC", Value = "aac"}, new () { Label = "AC3", Value = "ac3"}, + new () { Label = "DTS", Value = "dts" }, new () { Label = "EAC3", Value = "eac3" }, + new () { Label = "FLAC", Value ="flac" }, new () { Label = "MP3", Value = "mp3"}, + new () { Label = "PCM", Value ="pcm" }, new () { Label = "OPUS", Value = "opus"}, + new () { Label = "Vorbis", Value ="libvorbis" }, }; } return _CodecOptions; } } + + [DefaultValue("pcm_s16le")] + [Select(nameof(PcmFormats), 2)] + [ConditionEquals(nameof(Codec), "pcm")] + public string PcmFormat { get; set; } + + private static List _PcmFormats; + public static List PcmFormats + { + get + { + if (_PcmFormats == null) + { + _PcmFormats = new List + { + new () { Label = "Common", Value = "###GROUP###" }, + new () { Label = "Signed 16-bit Little Endian", Value = "pcm_s16le" }, + new () { Label = "Signed 24-bit Little Endian", Value = "pcm_s24le"}, + + new () { Label = "Signed", Value = "###GROUP###" }, + new () { Label = "8-bit", Value = "pcm_s8"}, + new () { Label = "16-bit Little Endian", Value = "pcm_s16le" }, + new () { Label = "16-bit Big Endian", Value = "pcm_s16be"}, + new () { Label = "24-bit Little Endian", Value = "pcm_s24le"}, + new () { Label = "24-bit Big Endian", Value = "pcm_s24be"}, + new () { Label = "32-bit Little Endian", Value = "pcm_s32le"}, + new () { Label = "32-bit Big Endian", Value = "pcm_s32be"}, + new () { Label = "64-bit Little Endian", Value = "pcm_s64le"}, + new () { Label = "64-bit Big Endian", Value = "pcm_s64be"}, + new () { Label = "Floating-point", Value = "###GROUP###" }, + new () { Label = "32-bit Little Endian", Value = "pcm_f32le"}, + new () { Label = "32-bit Big Endian", Value = "pcm_f32be"}, + new () { Label = "16-bit Little Endian", Value = "pcm_f16le"}, + new () { Label = "24-bit Little Endian", Value = "pcm_f24le"}, + new () { Label = "64-bit Little Endian", Value = "pcm_f64le"}, + new () { Label = "64-bit Big Endian", Value = "pcm_f64be"}, + }; + + } + return _PcmFormats; + } + } + [DefaultValue(0)] - [Select(nameof(ChannelsOptions), 2)] + [Select(nameof(ChannelsOptions), 3)] public float Channels { get; set; } private static List _ChannelsOptions; @@ -60,7 +107,7 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode } } - [Select(nameof(BitrateOptions), 3)] + [Select(nameof(BitrateOptions), 4)] public int Bitrate { get; set; } private static List _BitrateOptions; @@ -85,13 +132,13 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode } - [TextVariable(4)] + [TextVariable(5)] public string Pattern { get; set; } - [Boolean(5)] + [Boolean(6)] public bool NotMatching { get; set; } - [Boolean(6)] + [Boolean(7)] public bool UseLanguageCode { get; set; } public override int Execute(NodeParameters args) @@ -157,7 +204,11 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode /// if the stream had to be converted or not private bool ConvertTrack(NodeParameters args, FfmpegAudioStream stream) { - bool codecSame = stream.Stream.Codec?.ToLower() == Codec?.ToLower(); + string codec = Codec?.ToLowerInvariant() ?? string.Empty; + if (codec == "pcm") + codec = PcmFormat; + + bool codecSame = stream.Stream.Codec?.ToLowerInvariant() == codec; bool channelsSame = Channels == 0 || Math.Abs(Channels - stream.Stream.Channels) < 0.05f; bool bitrateSame = Bitrate < 2 || stream.Stream.Bitrate == 0 || Math.Abs(stream.Stream.Bitrate - Bitrate) < 0.05f; @@ -170,7 +221,7 @@ public class FfmpegBuilderAudioConverter : FfmpegBuilderNode stream.Codec = Codec.ToLowerInvariant(); - stream.EncodingParameters.AddRange(FfmpegBuilderAudioAddTrack.GetNewAudioTrackParameters(args, stream, Codec, Channels, Bitrate, 0)); + stream.EncodingParameters.AddRange(FfmpegBuilderAudioAddTrack.GetNewAudioTrackParameters(args, stream, codec, Channels, Bitrate, 0)); return true; } } diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs index ef7cb64f..7eefc501 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs @@ -145,7 +145,13 @@ public class FfmpegBuilderTrackSorter : FfmpegBuilderNode if (changed) { streams[i].ForcedChange = true; - orderedStreams[i].ForcedChange = true; + args.Logger?.ILog("Stream has change[1]: " + streams[i]); + + if (streams[i] != orderedStreams[i]) + { + orderedStreams[i].ForcedChange = true; + args.Logger?.ILog("Stream has change[2]: " + orderedStreams[i]); + } } changes |= changed; streams[i] = orderedStreams[i]; diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs index 611a9bb4..65d28cf4 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs @@ -98,8 +98,8 @@ /// the string representation of stream public override string ToString() { - if (Stream != null) - return Stream.ToString() + (Deleted ? " / Deleted" : ""); + // if (Stream != null) + // return Stream.ToString() + (Deleted ? " / Deleted" : ""); // can be null in unit tests return string.Join(" / ", new string[] { @@ -108,7 +108,9 @@ Codec, Title, Channels > 0 ? Channels.ToString("0.0") : null, - Deleted ? "Deleted" : null + IsDefault ? "Default" : null, + Deleted ? "Deleted" : null, + HasChange ? "Changed" : null }.Where(x => string.IsNullOrWhiteSpace(x) == false)); } } diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs index ac79b2ac..13078f82 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegModel.cs @@ -97,6 +97,7 @@ Language = item.stream.Language, Stream = item.stream, Channels = item.stream.Channels, + IsDefault = item.stream.Default, Codec = item.stream.Codec }); } @@ -115,9 +116,15 @@ } if(info.FileName.ToLower().EndsWith(".mp4")) - model.Extension = info.FileName.Substring(info.FileName.LastIndexOf(".") + 1); - if (info.FileName.ToLower().EndsWith(".mkv")) - model.Extension = info.FileName.Substring(info.FileName.LastIndexOf(".") + 1); + model.Extension = info.FileName[(info.FileName.LastIndexOf(".", StringComparison.Ordinal) + 1)..]; + else if (info.FileName.ToLower().EndsWith(".mkv")) + model.Extension = info.FileName[(info.FileName.LastIndexOf(".", StringComparison.Ordinal) + 1)..]; + else if (info.FileName.ToLower().EndsWith(".mov")) + model.Extension = info.FileName[(info.FileName.LastIndexOf(".", StringComparison.Ordinal) + 1)..]; + else if (info.FileName.ToLower().EndsWith(".mxf")) + model.Extension = info.FileName[(info.FileName.LastIndexOf(".", StringComparison.Ordinal) + 1)..]; + else if (info.FileName.ToLower().EndsWith(".webm")) + model.Extension = info.FileName[(info.FileName.LastIndexOf(".", StringComparison.Ordinal) + 1)..]; return model; } diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs index 814b3aa2..4b4c22b5 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegSubtitleStream.cs @@ -91,8 +91,8 @@ public class FfmpegSubtitleStream : FfmpegStream /// the string representation of stream public override string ToString() { - if (Stream != null) - return Stream.ToString() + (Deleted ? " / Deleted" : ""); + // if (Stream != null) + // return Stream.ToString() + (Deleted ? " / Deleted" : ""); // can be null in unit tests return string.Join(" / ", new string[] @@ -101,7 +101,9 @@ public class FfmpegSubtitleStream : FfmpegStream Language, Codec, Title, - Deleted ? "Deleted" : null + IsDefault ? "Default" : null, + Deleted ? "Deleted" : null, + HasChange ? "Changed" : null }.Where(x => string.IsNullOrWhiteSpace(x) == false)); } } \ No newline at end of file diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs index c18d895c..5d08f8aa 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegVideoStream.cs @@ -120,8 +120,8 @@ public class FfmpegVideoStream : FfmpegStream /// the string representation of stream public override string ToString() { - if (Stream != null) - return Stream.ToString() + (Deleted ? " / Deleted" : ""); + // if (Stream != null) + // return Stream.ToString() + (Deleted ? " / Deleted" : ""); // can be null in unit tests return string.Join(" / ", new string[] @@ -129,7 +129,8 @@ public class FfmpegVideoStream : FfmpegStream Index.ToString(), Codec, Title, - Deleted ? "Deleted" : null + Deleted ? "Deleted" : null, + HasChange ? "Changed" : null }.Where(x => string.IsNullOrWhiteSpace(x) == false)); } } diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMxf.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMxf.cs new file mode 100644 index 00000000..f5f00408 --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderRemuxToMxf.cs @@ -0,0 +1,24 @@ +namespace FileFlows.VideoNodes.FfmpegBuilderNodes; + +/// +/// Remuxes a file to the MXF container +/// +public class FfmpegBuilderRemuxToMxf : FfmpegBuilderNode +{ + /// + /// Gets or sets the URL to the helper page + /// + public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/remux-to-mxf"; + + /// + /// Gets that this is an enterprise flow element + /// + public override bool Enterprise => true; + + /// + public override int Execute(NodeParameters args) + { + this.Model.Extension = "mxf"; + return 1; + } +} \ No newline at end of file diff --git a/VideoNodes/VideoInfo.cs b/VideoNodes/VideoInfo.cs index f9e8d41f..3633b204 100644 --- a/VideoNodes/VideoInfo.cs +++ b/VideoNodes/VideoInfo.cs @@ -176,6 +176,11 @@ public class AudioStream : VideoFileStream /// public int SampleRate { get; set; } + /// + /// If this is a the default audio track + /// + public bool Default { get; set; } + /// /// Converts the steam to a string /// @@ -187,7 +192,8 @@ public class AudioStream : VideoFileStream Language, Codec, Title, - Channels > 0 ? Channels.ToString("0.0") : null + Channels > 0 ? Channels.ToString("0.0") : null, + Default ? "Default" : null }.Where(x => string.IsNullOrWhiteSpace(x) == false)); } diff --git a/VideoNodes/VideoInfoHelper.cs b/VideoNodes/VideoInfoHelper.cs index 22daf52c..3c1a306b 100644 --- a/VideoNodes/VideoInfoHelper.cs +++ b/VideoNodes/VideoInfoHelper.cs @@ -315,6 +315,7 @@ public class VideoInfoHelper audio.Codec = audio.Codec[..^1].Trim(); audio.Language = GetLanguage(line); + audio.Default = info.Contains("(default)"); // if (info.IndexOf("0 channels", StringComparison.Ordinal) >= 0) // { // logger?.WLog("Stream contained '0 Channels'"); diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 88a10aef..0afeb2f1 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -175,6 +175,8 @@ "Fields": { "Channels": "Channels", "Channels-Help": "The number of channels this new audio track will be.\nIf you specify more channels than the source, FFMPEG will automatically upmix it.\nIf you specify fewer channels than the source, FFMPEG will automatically down mix it.", + "PcmFormat": "Format", + "PcmFormat-Help": "The PCM format to use for encoding PCM audio.", "Bitrate": "Bitrate", "Bitrate-Help": "Bitrate of the audio track", "Codec": "Codec", @@ -470,6 +472,8 @@ "1": "Video set to use prores" }, "Fields": { + "Encoder": "Encoder", + "Encoder-Help": "Which FFmpeg prores encoder to use.", "Quality": "Quality", "Quality-Help": "0 for maximum quality, 32 for minimum quality. 9 to 13 are good values to use.", "Profile": "Profile", @@ -490,6 +494,13 @@ "1": "FFMPEG Builder set to remux to MKV" } }, + "FfmpegBuilderRemuxToMxf": { + "Label": "FFMPEG Builder: Remux to MXF", + "Description": "Remuxes a video file into a MXF container.", + "Outputs": { + "1": "FFMPEG Builder set to remux to MXF" + } + }, "FfmpegBuilderRemuxToMP4": { "Label": "FFMPEG Builder: Remux to MP4", "Description": "Remuxes a video file into a MP4 container.", diff --git a/VideoNodes/VideoNodes/EncodingNode.cs b/VideoNodes/VideoNodes/EncodingNode.cs index 11e2f66c..9ca3b06d 100644 --- a/VideoNodes/VideoNodes/EncodingNode.cs +++ b/VideoNodes/VideoNodes/EncodingNode.cs @@ -90,11 +90,48 @@ namespace FileFlows.VideoNodes var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger).Read(outputFile); SetVideoInfo(args, videoInfo, this.Variables ?? new Dictionary()); } + else if (success.successs == false) + { + // look for known error messages + var lines = success.output.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries) + .ToArray(); + + if (lines.Count() >= 50) { + lines = lines.TakeLast(50).ToArray(); + } + + string line; + if (HasLine(lines, "is not supported by the bitstream filter", out line)) + { + // get that line, more efficiently that twice + int codecIndex = line.IndexOf("Codec", StringComparison.InvariantCulture); + if (codecIndex > 0) + line = line[codecIndex..]; + int periodIndex = line.IndexOf(".", StringComparison.InvariantCulture); + if (periodIndex > 0) + line = line[..periodIndex]; + args.FailureReason = line; + } + else if (HasLine(lines, "codec not currently supported in container", out line)) + { + args.FailureReason = "Codec not currently supported in container"; + } + else if (HasLine(lines, "encoding with ProRes Proxy/LT/422/422 HQ (apco, apcs, apcn, ap4h) profile, need YUV422P10 input", out line)) + { + args.FailureReason = "Encoding with ProRes Proxy/LT/422/422 HQ (apco, apcs, apcn, ap4h) profile, need YUV422P10 input"; + } + } Encoder.AtTime -= AtTimeEvent; Encoder.OnStatChange -= EncoderOnOnStatChange; Encoder = null; output = success.output; return success.successs; + + bool HasLine(string[] lines, string text, out string line) + { + line = lines.FirstOrDefault(x => x.Contains(text)); + return line != null; + } } public override Task Cancel()