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 record --process [--seconds N] [--audio] [--mic] metrics --process [--seconds N] (requires admin) --audio embed system/game audio in the video --mic also record the microphone to a sidecar .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 | --window | --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 { "--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()); }