using System; using System.Threading; using Sdcb.FFmpeg.Codecs; using Sdcb.FFmpeg.Raw; using T3.Core.Logging; using T3.Core.Video; namespace T3.VideoServices; /// /// The FFmpeg implementation of . Registered into the Core /// holder so the editor's render-export can reach it across the operator /// load-context boundary (the editor cannot depend on this assembly directly). /// public sealed class FfmpegVideoEncoderFactory : IVideoEncoderFactory { /// /// Publishes the factory to Core. Idempotent and cheap (no native FFmpeg load — that is deferred to /// ). Called eagerly when the operator package loads so export works even /// when the rendered graph never used a video operator. /// public static void Register() => VideoExport.Factory ??= new FfmpegVideoEncoderFactory(); public IVideoFileWriter? TryCreateWriter(VideoExportSettings settings, out string? error) { error = null; try { if (!FfmpegLibrary.EnsureInitialized()) { error = FfmpegLibrary.StatusError ?? "FFmpeg is not available"; return null; } return new FfmpegVideoFileWriter(BuildEncoderSettings(settings)); } catch (Exception e) { error = "Failed to create the FFmpeg video encoder: " + e.Message; return null; } } public string? GenerateProxy(string sourcePath, string proxyPath, VideoExportCodec proxyCodec, double scale, IProgress? progress, CancellationToken cancel) => ProxyTranscoder.Generate(sourcePath, proxyPath, proxyCodec, scale, progress, cancel); public VideoProxyAdvice EvaluateProxyNeed(string sourcePath) { var result = ProxyEligibility.Evaluate(sourcePath); var recommendation = result.Recommendation switch { ProxyEligibility.Recommendation.Recommended => VideoProxyRecommendation.Recommended, ProxyEligibility.Recommendation.NotNeeded => VideoProxyRecommendation.NotNeeded, _ => VideoProxyRecommendation.Unknown, }; return new VideoProxyAdvice(recommendation, result.KeyframeIntervalSeconds, result.Reason); } public bool TryProbeDurationSeconds(string sourcePath, out double seconds) => VideoMetadata.TryProbeDurationSeconds(sourcePath, out seconds); public VideoEncoderAvailability GetAvailability(VideoExportCodec codec) { if (!FfmpegLibrary.EnsureInitialized()) return new VideoEncoderAvailability { Kind = VideoEncoderKind.Unavailable }; if (codec == VideoExportCodec.H264) { // H.264: a GPU encoder when available, otherwise in-process software OpenH264 (no GPL). Either way // it encodes — there's no unavailable case (MPEG-4 backs up OpenH264 in the encode path). var hardwareEncoder = HardwareEncoderProbe.H264HardwareEncoder; if (hardwareEncoder != null) return new VideoEncoderAvailability { Kind = VideoEncoderKind.Hardware, EncoderName = FriendlyHardwareName(hardwareEncoder) }; var softwareName = SoftwareH264EncoderName() != null ? "OpenH264" : "MPEG-4"; return new VideoEncoderAvailability { Kind = VideoEncoderKind.Software, EncoderName = softwareName }; } if (codec == VideoExportCodec.Hevc) { // HEVC: a GPU encoder when available, otherwise software libkvazaar (BSD, no GPL). No universal // last-resort fallback like H.264's MPEG-4, so it's Unavailable if neither is present. var hardwareEncoder = HardwareEncoderProbe.HevcHardwareEncoder; if (hardwareEncoder != null) return new VideoEncoderAvailability { Kind = VideoEncoderKind.Hardware, EncoderName = FriendlyHardwareName(hardwareEncoder) }; return Codec.FindEncoderByName("libkvazaar") != null ? new VideoEncoderAvailability { Kind = VideoEncoderKind.Software, EncoderName = "kvazaar" } : new VideoEncoderAvailability { Kind = VideoEncoderKind.Unavailable }; } // The rest are LGPL software encoders — but only if this build actually ships them. Probe before // claiming availability rather than failing at export time (encoder sets differ between FFmpeg builds). return SoftwareEncoderIsPresent(codec) ? new VideoEncoderAvailability { Kind = VideoEncoderKind.Software, EncoderName = SoftwareEncoderLabel(codec) } : new VideoEncoderAvailability { Kind = VideoEncoderKind.Unavailable, EncoderName = SoftwareEncoderLabel(codec) }; } // OpenH264 (libopenh264 — Cisco's BSD-licensed software H.264) when the build ships it; null falls the // encode path back to MPEG-4. Far better than MPEG-4, and no GPL (libx264) needed. private static string? SoftwareH264EncoderName() => Codec.FindEncoderByName("libopenh264") != null ? "libopenh264" : null; // Whether the bundled FFmpeg build includes the encoder a given software codec maps to. private static bool SoftwareEncoderIsPresent(VideoExportCodec codec) { if (codec == VideoExportCodec.ProRes) { try { Codec.FindEncoderById(AVCodecID.Prores); return true; } catch { return false; } } var encoderName = codec switch { VideoExportCodec.VP9 => "libvpx-vp9", VideoExportCodec.AV1 => "libsvtav1", VideoExportCodec.FFV1 => "ffv1", VideoExportCodec.Hap or VideoExportCodec.HapAlpha or VideoExportCodec.HapQ => "hap", _ => null, }; return encoderName != null && Codec.FindEncoderByName(encoderName) != null; } private static string FriendlyHardwareName(string encoderName) => encoderName switch { "h264_nvenc" or "hevc_nvenc" => "NVIDIA NVENC", "h264_qsv" or "hevc_qsv" => "Intel Quick Sync", "h264_amf" or "hevc_amf" => "AMD AMF", _ => encoderName, }; private static string SoftwareEncoderLabel(VideoExportCodec codec) => codec switch { VideoExportCodec.ProRes => "ProRes 422", VideoExportCodec.VP9 => "VP9 (libvpx)", VideoExportCodec.AV1 => "AV1 (SVT-AV1)", VideoExportCodec.FFV1 => "FFV1", VideoExportCodec.Hap => "HAP", VideoExportCodec.HapAlpha => "HAP Alpha", VideoExportCodec.HapQ => "HAP Q", _ => codec.ToString(), }; private static VideoEncoderSettings BuildEncoderSettings(VideoExportSettings s) { var fps = Math.Max(1, (int)Math.Round(s.FrameRate)); // integer rate, matching the previous MF writer var common = new VideoEncoderSettings { FilePath = s.FilePath, Width = s.Width, Height = s.Height, FrameRate = new AVRational(fps, 1), BitRate = s.BitRate, SourceFormat = AVPixelFormat.Rgba, SourceBytesPerPixel = 4, EncodeAudio = s.ExportAudio, AudioSampleRate = s.AudioSampleRate, AudioChannels = s.AudioChannels, AudioBitRate = 192_000, }; switch (s.Codec) { case VideoExportCodec.ProRes: { // ProRes 422 is an all-intra LGPL editing codec; it needs 10-bit 4:2:2 and sets its own rate. return common with { VideoCodecId = AVCodecID.Prores, EncoderPixelFormat = AVPixelFormat.Yuv422p10le, }; } case VideoExportCodec.VP9: { // VP9 (libvpx) in MP4 — efficient but software-encoded. Default libvpx is single-threaded and // painfully slow; row-mt + a faster cpu-used make it usable (with ThreadCount=0 in the encoder). return common with { VideoEncoderName = "libvpx-vp9", EncoderPixelFormat = AVPixelFormat.Yuv420p, VideoCodecOptions = new[] { new KeyValuePair("deadline", "good"), new KeyValuePair("cpu-used", "4"), new KeyValuePair("row-mt", "1"), }, }; } case VideoExportCodec.AV1: { // AV1 via SVT-AV1 in MP4 — the most efficient delivery codec. `preset` trades speed for size // (0 slowest … 13 fastest); 8 is a usable export default rather than SVT's slow default. return common with { VideoEncoderName = "libsvtav1", EncoderPixelFormat = AVPixelFormat.Yuv420p, VideoCodecOptions = new[] { new KeyValuePair("preset", "8") }, }; } case VideoExportCodec.FFV1: { // FFV1 in MKV — a lossless intra archival codec; large files, ignores the target bitrate. return common with { VideoEncoderName = "ffv1", EncoderPixelFormat = AVPixelFormat.Yuv420p, }; } case VideoExportCodec.Hevc: { // Hardware HEVC when the GPU supports it; otherwise software libkvazaar (BSD, not GPL libx265). var hardwareEncoder = HardwareEncoderProbe.HevcHardwareEncoder; return common with { VideoEncoderName = hardwareEncoder ?? "libkvazaar", EncoderPixelFormat = HardwareEncoderProbe.EncoderInputFormat(hardwareEncoder), }; } case VideoExportCodec.Hap: return BuildHapSettings(common, "hap"); case VideoExportCodec.HapAlpha: return BuildHapSettings(common, "hap_alpha"); case VideoExportCodec.HapQ: return BuildHapSettings(common, "hap_q"); case VideoExportCodec.H264: default: { // Hardware H.264 when the GPU supports it; otherwise in-process OpenH264 (libopenh264 — BSD, ships // in the LGPL build, no GPL). MPEG-4 Part 2 is only a last resort if OpenH264 is somehow absent. // (libx264 is GPL and intentionally not bundled.) var hardwareEncoder = HardwareEncoderProbe.H264HardwareEncoder; var encoderName = hardwareEncoder ?? SoftwareH264EncoderName(); if (encoderName == null) Log.Warning("No hardware or OpenH264 encoder available - exporting with MPEG-4 (lower quality)."); return common with { VideoEncoderName = encoderName, // null => fall back to VideoCodecId (MPEG-4) VideoCodecId = AVCodecID.Mpeg4, // QSV needs nv12; NVENC/OpenH264/MPEG-4 take yuv420p. Must match what the probe opened with. EncoderPixelFormat = HardwareEncoderProbe.EncoderInputFormat(hardwareEncoder), }; } } } private static VideoEncoderSettings BuildHapSettings(VideoEncoderSettings common, string hapFormat) { // HAP feeds RGBA straight in and snappy-compresses DXT blocks itself, so it ignores the target bitrate. // DXT works on 4×4 blocks, so the frame size must be a multiple of 4 — round down (crops ≤ 3 px) rather // than let the encoder reject odd sizes. var (width, height) = VideoExportCodec.Hap.RoundToEncoderBlock(common.Width, common.Height); return common with { VideoEncoderName = "hap", EncoderPixelFormat = AVPixelFormat.Rgba, Width = width, Height = height, VideoCodecOptions = new[] { new KeyValuePair("format", hapFormat) }, }; } } /// Thin Core-facing wrapper over . internal sealed class FfmpegVideoFileWriter : IVideoFileWriter { public FfmpegVideoFileWriter(in VideoEncoderSettings settings) => _encoder = new VideoFileEncoder(settings); public void AddVideoFrame(ReadOnlySpan rgbaPixels, int rowStride) => _encoder.WriteVideoFrame(rgbaPixels, rowStride); public void AddAudioSamples(ReadOnlySpan interleavedFloatPcm) => _encoder.WriteAudioSamples(interleavedFloatPcm); public void Finish() => _encoder.Finish(); public void Dispose() => _encoder.Dispose(); private readonly VideoFileEncoder _encoder; }