#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; }
}