mirror of
https://github.com/LANCommander/GameOverlay.Capture.git
synced 2026-08-01 03:38:56 -04:00
268 lines
8.8 KiB
C#
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());
|
|
}
|