GameOverlay.Capture/samples/GameOverlay.Capture.Cli/Program.cs
2026-07-29 20:03:02 -05:00

268 lines
8.8 KiB
C#

using System.Diagnostics;
using GameOverlay.Capture;
if (args.Length == 0)
{
PrintUsage();
return 0;
}
try
{
switch (args[0].ToLowerInvariant())
{
case "backends":
ListBackends();
return 0;
case "devices":
ListDevicesAndCapabilities();
return 0;
case "shot":
case "screenshot":
await RunScreenshotAsync(args);
return 0;
case "record":
await RunRecordAsync(args);
return 0;
case "metrics":
await RunMetricsAsync(args);
return 0;
#if WINDOWS
case "encode-selftest":
{
var outPath = OutputPath(args, "selftest.mp4");
int frames = GetSeconds(args, 90); // reuse --seconds as frame count
var full = await GameOverlay.Capture.Windows.WindowsDiagnostics.EncodeSyntheticClipAsync(outPath, frames, fps: 30);
Console.WriteLine($"Encoded {frames} synthetic frames to {full}");
return 0;
}
#endif
default:
Console.Error.WriteLine($"Unknown command: {args[0]}");
PrintUsage();
return 2;
}
}
catch (PlatformNotSupportedException ex)
{
Console.Error.WriteLine($"No capture backend available: {ex.Message}");
return 3;
}
catch (NotSupportedException ex)
{
Console.Error.WriteLine($"Not supported: {ex.Message}");
return 4;
}
catch (UnauthorizedAccessException ex)
{
Console.Error.WriteLine($"Insufficient privileges: {ex.Message}");
return 5;
}
static void PrintUsage()
{
Console.WriteLine(
"""
GameOverlay.Capture CLI
backends List registered capture backends
devices List audio devices + recording capabilities
shot --process <name> <out.png>
record --process <name> <out.mp4> [--seconds N] [--audio] [--mic]
metrics --process <name> [--seconds N] (requires admin)
--audio embed system/game audio in the video
--mic also record the microphone to a sidecar <out>.mic.m4a
--fps N target/cap frame rate (default 60)
--quality Q low|medium|high|veryhigh (default high)
--vbitrate K explicit video bitrate in kbps (overrides quality)
--abitrate K audio bitrate in kbps (default 128)
--hevc encode H.265/HEVC instead of H.264
--duplication use DXGI Desktop Duplication (monitor only) instead of WGC
--borderless request borderless capture (Windows 11)
--no-cursor exclude the mouse cursor
Targets: --process <name|pid> | --window <hwnd> | --monitor
""");
}
static void ListBackends()
{
var all = CaptureBackendRegistry.All;
if (all.Count == 0)
{
Console.WriteLine("No backends registered. Reference a platform package (e.g. GameOverlay.Capture.Windows).");
return;
}
foreach (var b in all)
Console.WriteLine($" {b.Name} - supported: {b.IsSupported}");
}
static void ListDevicesAndCapabilities()
{
Console.WriteLine("Audio output devices (system/loopback):");
foreach (var d in CaptureCapabilities.GetOutputDevices())
Console.WriteLine($" {(d.IsDefault ? "*" : " ")} {d.Name} [{d.Id}]");
Console.WriteLine("Audio input devices (microphone):");
foreach (var d in CaptureCapabilities.GetInputDevices())
Console.WriteLine($" {(d.IsDefault ? "*" : " ")} {d.Name} [{d.Id}]");
Console.WriteLine($"Frame rates: {string.Join(", ", CaptureCapabilities.CommonFrameRates)} (any positive value works)");
Console.WriteLine($"Video qualities: {string.Join(", ", CaptureCapabilities.VideoQualities)}");
Console.WriteLine($"Video codecs: {string.Join(", ", CaptureCapabilities.VideoCodecs)}");
Console.WriteLine($"Audio bitrates: {string.Join(", ", CaptureCapabilities.AudioBitratesKbps)} kbps");
}
static CaptureTarget ParseTarget(string[] args)
{
for (int i = 0; i < args.Length - 1; i++)
{
switch (args[i])
{
case "--process":
return int.TryParse(args[i + 1], out int pid)
? CaptureTarget.FromProcess(pid)
: CaptureTarget.FromProcess(args[i + 1]);
case "--window":
return CaptureTarget.FromWindow((nint)long.Parse(args[i + 1]));
case "--monitor":
return CaptureTarget.Interactive();
}
}
// No explicit target => interactive picker where supported.
return CaptureTarget.Interactive();
}
static void WarnIfExclusiveFullscreen()
{
#if WINDOWS
if (GameOverlay.Capture.Windows.WindowsDiagnostics.IsExclusiveFullscreenActive())
Console.Error.WriteLine("warning: an exclusive-fullscreen app is active. External capture (WGC/Duplication) may be black. Switch the game to borderless/windowed.");
#endif
}
static CaptureOptions BuildOptions(string[] args) => new()
{
UseDesktopDuplication = args.Contains("--duplication"),
HideCaptureBorder = args.Contains("--borderless"),
CaptureCursor = !args.Contains("--no-cursor"),
};
static int GetSeconds(string[] args, int fallback) => GetIntFlag(args, "--seconds") ?? fallback;
static int? GetIntFlag(string[] args, string name)
{
for (int i = 0; i < args.Length - 1; i++)
if (args[i] == name && int.TryParse(args[i + 1], out int v))
return v;
return null;
}
static string? GetStringFlag(string[] args, string name)
{
for (int i = 0; i < args.Length - 1; i++)
if (args[i] == name)
return args[i + 1];
return null;
}
static string OutputPath(string[] args, string defaultPath)
{
// First bare token that isn't the command or a value consumed by a value-flag.
var valueFlags = new HashSet<string> { "--process", "--window", "--seconds", "--fps", "--quality", "--vbitrate", "--abitrate" };
for (int i = 1; i < args.Length; i++)
{
if (args[i].StartsWith("--"))
continue;
if (valueFlags.Contains(args[i - 1]))
continue; // this token is a flag's value
return args[i];
}
return defaultPath;
}
static async Task RunScreenshotAsync(string[] args)
{
var target = ParseTarget(args);
var outPath = OutputPath(args, "screenshot.png");
WarnIfExclusiveFullscreen();
await using var session = await CaptureSession.CreateAsync(target, BuildOptions(args));
Console.WriteLine($"Backend: {session.BackendName}; target {target}");
await session.SaveScreenshotAsync(outPath);
Console.WriteLine($"Saved screenshot to {Path.GetFullPath(outPath)}");
}
static async Task RunRecordAsync(string[] args)
{
var target = ParseTarget(args);
var outPath = OutputPath(args, "recording.mp4");
int seconds = GetSeconds(args, 10);
bool audio = args.Contains("--audio");
bool mic = args.Contains("--mic");
var quality = GetStringFlag(args, "--quality")?.ToLowerInvariant() switch
{
"low" => VideoQuality.Low,
"medium" => VideoQuality.Medium,
"veryhigh" => VideoQuality.VeryHigh,
_ => VideoQuality.High,
};
var video = new VideoOptions
{
Codec = args.Contains("--hevc") ? VideoCodec.Hevc : VideoCodec.H264,
FrameRate = GetIntFlag(args, "--fps"),
Quality = quality,
Bitrate = GetIntFlag(args, "--vbitrate") is { } kbps ? kbps * 1000 : null,
};
var audioOptions = new AudioOptions
{
SystemLoopback = audio,
Microphone = mic ? MicrophoneOptions.Default : null,
SeparateTracks = true,
Bitrate = (GetIntFlag(args, "--abitrate") ?? 128) * 1000,
};
WarnIfExclusiveFullscreen();
await using var session = await CaptureSession.CreateAsync(target, BuildOptions(args));
Console.WriteLine($"Backend: {session.BackendName}; recording {seconds}s to {outPath} " +
$"({video.Codec} @ {video.ResolveFrameRate()}fps {quality}, audio: {audio}, mic: {mic})");
var recording = await session.StartRecordingAsync(new RecordingOptions
{
OutputPath = outPath,
Video = video,
Audio = audioOptions,
});
await Task.Delay(TimeSpan.FromSeconds(seconds));
await recording.StopAsync();
Console.WriteLine($"Saved recording to {Path.GetFullPath(outPath)}");
}
static async Task RunMetricsAsync(string[] args)
{
var target = ParseTarget(args);
int seconds = GetSeconds(args, 10);
await using var session = await CaptureSession.CreateAsync(target);
Console.WriteLine($"Backend: {session.BackendName}; collecting metrics for {seconds}s");
var metrics = session.Metrics;
metrics.SampleReady += (_, s) => { /* could render a live overlay */ };
metrics.Start();
var sw = Stopwatch.StartNew();
while (sw.Elapsed < TimeSpan.FromSeconds(seconds))
await Task.Delay(500);
metrics.Stop();
Console.WriteLine(metrics.GetSummary());
}