#nullable enable using System; using System.Threading; namespace T3.Core.Video; /// The video codec for a render-export; the video assembly maps each to an FFmpeg encoder + container. public enum VideoExportCodec { /// H.264 in MP4 — hardware-encoded where available, else an LGPL software fallback. The default. H264 = 0, /// Apple ProRes 422 in MOV — an all-intra editing codec (larger files); always available (LGPL). ProRes = 1, /// VP9 in MP4 — an efficient delivery codec, software-encoded (libvpx, slower than H.264). VP9 = 2, /// AV1 in MP4 — the most efficient delivery codec, software-encoded (SVT-AV1). AV1 = 3, /// FFV1 in MKV — a lossless intra archival codec (very large files). FFV1 = 4, /// HAP (DXT1, RGB) in MOV — a GPU-friendly intra codec for realtime/VJ playback; no alpha. (LGPL.) Hap = 5, /// HAP Alpha (DXT5) in MOV — HAP carrying an alpha channel. (LGPL.) HapAlpha = 6, /// HAP Q (scaled DXT5-YCoCg) in MOV — higher-quality HAP; larger than standard HAP. (LGPL.) HapQ = 7, /// HEVC (H.265) in MP4 — hardware-encoded where available, else software (libkvazaar — BSD, no GPL). /// More efficient than H.264; heavier patent licensing (orthogonal to the software licence). Hevc = 8, } /// Container/extension helpers for . public static class VideoExportCodecExtensions { /// The output-file extension (which selects the container) for a codec. public static string GetFileExtension(this VideoExportCodec codec) => codec switch { VideoExportCodec.ProRes => ".mov", VideoExportCodec.Hap => ".mov", VideoExportCodec.HapAlpha => ".mov", VideoExportCodec.HapQ => ".mov", VideoExportCodec.FFV1 => ".mkv", _ => ".mp4", // H264, VP9, AV1 }; /// The size a codec's frame dimensions must be a multiple of. HAP packs 4×4 DXT blocks, so it needs /// 4; the others have no constraint the export path enforces (1). public static int GetEncoderBlockSize(this VideoExportCodec codec) => codec switch { VideoExportCodec.Hap => 4, VideoExportCodec.HapAlpha => 4, VideoExportCodec.HapQ => 4, _ => 1, }; /// Rounds a render resolution down to the codec's block size — the dimensions the encoder actually /// outputs (the export path crops the few excess pixels). A no-op for codecs with no block constraint. public static (int width, int height) RoundToEncoderBlock(this VideoExportCodec codec, int width, int height) { var block = codec.GetEncoderBlockSize(); if (block <= 1) return (width, height); return (Math.Max(block, width / block * block), Math.Max(block, height / block * block)); } } /// /// FFmpeg-free description of a render-export target. The editor's render-export builds this; the video /// assembly maps it onto its own FFmpeg settings. Kept in Core so the editor (which must not depend on the /// FFmpeg/operator assembly) can describe an encode. /// public readonly record struct VideoExportSettings { public required string FilePath { get; init; } public required int Width { get; init; } public required int Height { get; init; } /// Frames per second (rounded to an integer rate to match the previous Media Foundation writer). public required double FrameRate { get; init; } public long BitRate { get; init; } public bool ExportAudio { get; init; } public int AudioChannels { get; init; } public int AudioSampleRate { get; init; } /// Which codec/container to encode. Default . public VideoExportCodec Codec { get; init; } } /// /// Writes one render-export file. The editor reads back each output frame to CPU RGBA8 bytes (it owns the GPU /// readback) and hands them here; the implementation (in the video assembly) encodes and muxes them. /// public interface IVideoFileWriter : IDisposable { /// Encodes one frame. is RGBA8, bytes /// per row (≥ Width*4); read only during the call. void AddVideoFrame(ReadOnlySpan rgbaPixels, int rowStride); /// Buffers interleaved 32-bit-float PCM for the audio track. No-op when audio is disabled. void AddAudioSamples(ReadOnlySpan interleavedFloatPcm); /// Flushes the encoders and writes the container trailer. also calls it. void Finish(); } /// How a will be encoded on this machine — for inline UI before export. public enum VideoEncoderKind { /// FFmpeg / the video package isn't available, so the codec can't be encoded at all. Unavailable, /// A built-in software encoder serves it in-process (ProRes/VP9/AV1/FFV1/HAP, or H.264 via OpenH264). Software, /// A GPU hardware encoder (NVENC/Quick Sync/AMF) initialised successfully (H.264). Preferred, silent. Hardware, } /// How the requested codec will be served, for the render window's inline availability indicator. public readonly record struct VideoEncoderAvailability { public required VideoEncoderKind Kind { get; init; } /// A human-readable label for the encoder that will run (e.g. "NVIDIA NVENC", "MPEG-4"). May be null. public string? EncoderName { get; init; } } /// Whether a video source benefits from a seek-proxy — decided from its measured keyframe spacing. public enum VideoProxyRecommendation { /// Already seek-cheap (all-intra or short-GOP); a proxy would waste disk and time. NotNeeded, /// Long-GOP inter-frame video; a proxy makes scrubbing/seeking cheap. Recommended, /// Couldn't tell (file unreadable, no video stream, no usable timestamps). Unknown, } /// The proxy advice for one source: the recommendation, the measured keyframe interval, and a /// human-readable reason for UI tooltips. public readonly record struct VideoProxyAdvice(VideoProxyRecommendation Recommendation, double KeyframeIntervalSeconds, string Reason); /// Creates s; implemented by the video assembly. public interface IVideoEncoderFactory { /// Returns null and an message when no usable encoder can be created. IVideoFileWriter? TryCreateWriter(VideoExportSettings settings, out string? error); /// Reports how will be encoded here. The hardware probe behind it is a /// one-shot GPU-encoder open (cached); callers on a UI thread should run this off-thread on first use. VideoEncoderAvailability GetAvailability(VideoExportCodec codec); /// Transcodes a source video to an all-intra **proxy** file (decode → re-encode at /// of the source resolution) for fast seeking. Blocking and CPU-only — the editor /// calls it on a background thread. Returns null on success, otherwise an error message. string? GenerateProxy(string sourcePath, string proxyPath, VideoExportCodec proxyCodec, double scale, IProgress? progress, CancellationToken cancel); /// Reports whether is worth proxying, from its measured keyframe /// spacing (demux-only — no decode). Used to decide auto-generation and to hint the manual trigger. VideoProxyAdvice EvaluateProxyNeed(string sourcePath); /// Reads 's duration in seconds (demux-only — no decode). Returns false /// (and 0) when the file can't be read or carries no usable duration. Used to size a freshly dropped video /// timeline clip to its real length. bool TryProbeDurationSeconds(string sourcePath, out double seconds); } /// /// Holds the process-wide video-encode factory. The video assembly sets it when its operator package loads /// (it is the only assembly that can depend on FFmpeg); the editor's render-export reads it. Null until the /// video assembly has registered — render-export surfaces a clear message in that case. Mirrors /// ; Core is loaded once and shared across the editor and operator load contexts, /// so this single static bridges them. /// public static class VideoExport { public static IVideoEncoderFactory? Factory { get; set; } }