Files
2026-07-13 13:13:17 +08:00

193 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System;
using System.Threading;
namespace T3.Core.Video;
/// <summary>The video codec for a render-export; the video assembly maps each to an FFmpeg encoder + container.</summary>
public enum VideoExportCodec
{
/// <summary>H.264 in MP4 — hardware-encoded where available, else an LGPL software fallback. The default.</summary>
H264 = 0,
/// <summary>Apple ProRes 422 in MOV — an all-intra editing codec (larger files); always available (LGPL).</summary>
ProRes = 1,
/// <summary>VP9 in MP4 — an efficient delivery codec, software-encoded (libvpx, slower than H.264).</summary>
VP9 = 2,
/// <summary>AV1 in MP4 — the most efficient delivery codec, software-encoded (SVT-AV1).</summary>
AV1 = 3,
/// <summary>FFV1 in MKV — a lossless intra archival codec (very large files).</summary>
FFV1 = 4,
/// <summary>HAP (DXT1, RGB) in MOV — a GPU-friendly intra codec for realtime/VJ playback; no alpha. (LGPL.)</summary>
Hap = 5,
/// <summary>HAP Alpha (DXT5) in MOV — HAP carrying an alpha channel. (LGPL.)</summary>
HapAlpha = 6,
/// <summary>HAP Q (scaled DXT5-YCoCg) in MOV — higher-quality HAP; larger than standard HAP. (LGPL.)</summary>
HapQ = 7,
/// <summary>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).</summary>
Hevc = 8,
}
/// <summary>Container/extension helpers for <see cref="VideoExportCodec"/>.</summary>
public static class VideoExportCodecExtensions
{
/// <summary>The output-file extension (which selects the container) for a codec.</summary>
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
};
/// <summary>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).</summary>
public static int GetEncoderBlockSize(this VideoExportCodec codec) => codec switch
{
VideoExportCodec.Hap => 4,
VideoExportCodec.HapAlpha => 4,
VideoExportCodec.HapQ => 4,
_ => 1,
};
/// <summary>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.</summary>
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));
}
}
/// <summary>
/// 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.
/// </summary>
public readonly record struct VideoExportSettings
{
public required string FilePath { get; init; }
public required int Width { get; init; }
public required int Height { get; init; }
/// <summary>Frames per second (rounded to an integer rate to match the previous Media Foundation writer).</summary>
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; }
/// <summary>Which codec/container to encode. Default <see cref="VideoExportCodec.H264"/>.</summary>
public VideoExportCodec Codec { get; init; }
}
/// <summary>
/// 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.
/// </summary>
public interface IVideoFileWriter : IDisposable
{
/// <summary>Encodes one frame. <paramref name="rgbaPixels"/> is RGBA8, <paramref name="rowStride"/> bytes
/// per row (≥ Width*4); read only during the call.</summary>
void AddVideoFrame(ReadOnlySpan<byte> rgbaPixels, int rowStride);
/// <summary>Buffers interleaved 32-bit-float PCM for the audio track. No-op when audio is disabled.</summary>
void AddAudioSamples(ReadOnlySpan<byte> interleavedFloatPcm);
/// <summary>Flushes the encoders and writes the container trailer. <see cref="IDisposable.Dispose"/> also calls it.</summary>
void Finish();
}
/// <summary>How a <see cref="VideoExportCodec"/> will be encoded on this machine — for inline UI before export.</summary>
public enum VideoEncoderKind
{
/// <summary>FFmpeg / the video package isn't available, so the codec can't be encoded at all.</summary>
Unavailable,
/// <summary>A built-in software encoder serves it in-process (ProRes/VP9/AV1/FFV1/HAP, or H.264 via OpenH264).</summary>
Software,
/// <summary>A GPU hardware encoder (NVENC/Quick Sync/AMF) initialised successfully (H.264). Preferred, silent.</summary>
Hardware,
}
/// <summary>How the requested codec will be served, for the render window's inline availability indicator.</summary>
public readonly record struct VideoEncoderAvailability
{
public required VideoEncoderKind Kind { get; init; }
/// <summary>A human-readable label for the encoder that will run (e.g. "NVIDIA NVENC", "MPEG-4"). May be null.</summary>
public string? EncoderName { get; init; }
}
/// <summary>Whether a video source benefits from a seek-proxy — decided from its measured keyframe spacing.</summary>
public enum VideoProxyRecommendation
{
/// <summary>Already seek-cheap (all-intra or short-GOP); a proxy would waste disk and time.</summary>
NotNeeded,
/// <summary>Long-GOP inter-frame video; a proxy makes scrubbing/seeking cheap.</summary>
Recommended,
/// <summary>Couldn't tell (file unreadable, no video stream, no usable timestamps).</summary>
Unknown,
}
/// <summary>The proxy advice for one source: the recommendation, the measured keyframe interval, and a
/// human-readable reason for UI tooltips.</summary>
public readonly record struct VideoProxyAdvice(VideoProxyRecommendation Recommendation,
double KeyframeIntervalSeconds, string Reason);
/// <summary>Creates <see cref="IVideoFileWriter"/>s; implemented by the video assembly.</summary>
public interface IVideoEncoderFactory
{
/// <summary>Returns null and an <paramref name="error"/> message when no usable encoder can be created.</summary>
IVideoFileWriter? TryCreateWriter(VideoExportSettings settings, out string? error);
/// <summary>Reports how <paramref name="codec"/> 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.</summary>
VideoEncoderAvailability GetAvailability(VideoExportCodec codec);
/// <summary>Transcodes a source video to an all-intra **proxy** file (decode → re-encode at
/// <paramref name="scale"/> 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.</summary>
string? GenerateProxy(string sourcePath, string proxyPath, VideoExportCodec proxyCodec, double scale,
IProgress<double>? progress, CancellationToken cancel);
/// <summary>Reports whether <paramref name="sourcePath"/> is worth proxying, from its measured keyframe
/// spacing (demux-only — no decode). Used to decide auto-generation and to hint the manual trigger.</summary>
VideoProxyAdvice EvaluateProxyNeed(string sourcePath);
/// <summary>Reads <paramref name="sourcePath"/>'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.</summary>
bool TryProbeDurationSeconds(string sourcePath, out double seconds);
}
/// <summary>
/// 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
/// <see cref="VideoPlayback"/>; Core is loaded once and shared across the editor and operator load contexts,
/// so this single static bridges them.
/// </summary>
public static class VideoExport
{
public static IVideoEncoderFactory? Factory { get; set; }
}