Add experimental video recorder sample extension for MTP#9377
Add experimental video recorder sample extension for MTP#9377Evangelink wants to merge 8 commits into
Conversation
A sample Microsoft.Testing.Platform extension exposing a test-facing service
to start/stop screen recording during a test run via an external ffmpeg
process. Produced videos are attached as session artifacts.
Features:
- Opt-in via --capture-video with retention modes (on-failure default, always)
- --capture-video-source screen|window (DPI-correct, region-based window capture
resolving main window -> foreground -> console)
- --capture-video-args to pass extra ffmpeg options
- Cross-platform inputs (gdigrab/x11grab/avfoundation), graceful stop via stdin
- Best-effort, never-throwing API; empty-file cleanup; warns on capture failure
Public API is marked [Experimental("TPEXP")]. Wired into the Playground sample
with a demo test and documented in samples/VideoRecorder/README.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new sample Microsoft.Testing.Platform (MTP) extension (Microsoft.Testing.Extensions.VideoRecorder) that allows tests to start/stop screen recordings by driving an external ffmpeg process, and wires it into the Playground sample for demonstration.
Changes:
- Added a new
samples/VideoRecorderproject implementing a video recorder service, ffmpeg-based engine, CLI options provider, and session handler that attaches kept recordings as session artifacts. - Integrated the sample into the repo solution and the
samples/Playgroundapp.
Show a summary per file
| File | Description |
|---|---|
| TestFx.slnx | Adds the new VideoRecorder sample project to the solution. |
| samples/VideoRecorder/VideoRecorder.csproj | Introduces the new multi-targeted sample project referencing Microsoft.Testing.Platform. |
| samples/VideoRecorder/IVideoRecorder.cs | Defines the test-facing recorder service contract. |
| samples/VideoRecorder/VideoRecorder.cs | Provides a process-wide VideoRecorder.Current entry point with a no-op fallback. |
| samples/VideoRecorder/VideoRecorderOptions.cs | Adds experimental options/enums controlling capture source, retention, codec, etc. |
| samples/VideoRecorder/FfmpegVideoRecorder.cs | Implements ffmpeg process management, capture argument building, and output handling. |
| samples/VideoRecorder/VideoRecorderSessionHandler.cs | Hooks recorder into session lifetime, tracks failures, and publishes artifacts. |
| samples/VideoRecorder/VideoRecorderCommandLineProvider.cs | Adds --capture-video* CLI options and validation. |
| samples/VideoRecorder/VideoRecorderExtensions.cs | Adds AddVideoRecorderProvider builder extension for registration. |
| samples/VideoRecorder/ExperimentalAttribute.cs | Adds a net462-only polyfill for [Experimental]. |
| samples/VideoRecorder/README.md | Documents usage/registration/CLI (currently has some mismatches with implementation). |
| samples/Playground/Playground.csproj | References the new VideoRecorder sample project. |
| samples/Playground/Program.cs | Registers the provider in the Playground app. |
Copilot's findings
- Files reviewed: 13/14 changed files
- Comments generated: 6
| options.Format = VideoRecorderFormat.Mp4H264; // or WebMVp9 (royalty-free) | ||
| options.PersistMode = VideoRecorderPersistenceMode.OnFailure; // or Always | ||
| options.CaptureCurrentProcessWindow = false; // true = capture only this process's window (Windows) | ||
| // options.OutputDirectory = ...; // defaults to <TestResults>/VideoRecordings |
There was a problem hiding this comment.
Fixed in a74253a - the snippet now uses options.Source = VideoCaptureSource.Screen (the renamed property).
| | `--capture-video` | *(none)*, `on-failure`, `always` | Enables screen recording for the run. The optional argument controls retention: `on-failure` (**default** — keep the video only if at least one test fails) or `always`. | | ||
| | `--capture-video-window` | *(flag)* | Capture only the **current process window** instead of the full screen. Windows only (uses `gdigrab title=`); falls back to full-screen capture elsewhere. Requires `--capture-video`. | | ||
| | `--capture-video-args` | any string | Extra arguments passed to the underlying recorder (currently ffmpeg), as output/encoding options. Requires `--capture-video`. | |
There was a problem hiding this comment.
Fixed in a74253a - the table now documents --capture-video-source with screen|window.
| # Record only the current process window (e.g. your terminal) instead of the whole screen | ||
| yourtests --capture-video always --capture-video-window | ||
|
|
||
| # Record and pass extra recorder args. NOTE: because the value starts with '-', you must use the | ||
| # '=' (or ':') delimiter form so MTP binds it to the option instead of parsing it as a new option. |
There was a problem hiding this comment.
Fixed in a74253a - examples now use --capture-video-source window and a direct --capture-video-args=... form.
There was a problem hiding this comment.
The --capture-video-window flag was later replaced by --capture-video-source screen|window; docs/examples updated accordingly in 0f820a5.
| > Retention is decided at **session** granularity: if any test in the run fails, all recordings | ||
| > produced during the run are kept; if every test passes, they are all discarded. | ||
|
|
||
| > The `on-failure` mode decides at **session** granularity: if any test in the run fails, all | ||
| > recordings produced during the run are kept; if every test passes, they are all discarded. |
There was a problem hiding this comment.
Fixed in a74253a - removed the duplicated retention note, keeping a single explanation.
| // Resolves the screen rectangle of the current process's window so gdigrab can capture just | ||
| // that region. Returns false when there is no usable visible window (headless runs, or | ||
| // terminals like Windows Terminal whose visible window is not owned by this process), in | ||
| // which case the caller falls back to full-screen capture. | ||
| // Resolves the screen rectangle of the window to capture so gdigrab can record just that | ||
| // region. Candidates are tried in order: the process main window (a GUI app under test owns | ||
| // it), then the foreground window (the terminal you launched from — this is what makes | ||
| // Windows Terminal work, since its window isn't owned by the test process), then the console | ||
| // window (classic conhost). Returns false when none is a usable visible window, in which case | ||
| // the caller falls back to full-screen capture. |
There was a problem hiding this comment.
Fixed in a74253a - removed the accidental duplicate comment block.
| // On .NET Framework the token only prevents the wait task from starting; once | ||
| // WaitForExit is blocking it cannot be interrupted, so a cancelled stop still waits up to | ||
| // the timeout. That bound is acceptable here. | ||
| return await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds), cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Good catch - fixed in a74253a. The net462 path no longer passes the token to Task.Run, so an already-cancelled token can't throw OperationCanceledException during the best-effort stop.
…tionale Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- README: fix stale --capture-video-window / CaptureCurrentProcessWindow references after the rename to --capture-video-source / Source; update examples; remove duplicated retention note - FfmpegVideoRecorder: remove accidental duplicate comment above TryGetCurrentProcessWindowRegion - FfmpegVideoRecorder: net462 WaitForExitAsync no longer passes an already-cancelled token to Task.Run (avoids OperationCanceledException on best-effort stop during a cancelled run) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| _messageBus = messageBus; | ||
| _outputDevice = outputDevice; | ||
| _logger = logger; | ||
|
|
||
| _enabled = commandLineOptions.IsOptionSet(VideoRecorderCommandLineProvider.EnableOptionName); | ||
| if (!_enabled) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| ApplyCommandLineOverrides(options, commandLineOptions); | ||
| _persistMode = options.PersistMode; | ||
|
|
There was a problem hiding this comment.
This is a false positive: in C# readonly fields are definitely-assigned to their default when not explicitly set (unlike locals), so the early return compiles fine. _recorder is a nullable reference (defaults to null) and is null-checked everywhere; _persistMode/_granularity default to their enum zero value and are unused when disabled. The build is green for net9.0 and net462.
Recording is now automatic per test by default (one video per test, named
after the test), addressing the expectation of a video per test rather than a
single video for the whole run. Adds:
- VideoCaptureGranularity { PerTest, PerSession, Manual } option
- --capture-video-granularity test|session|manual CLI option
- Per-test retention (on-failure keeps only failing tests' videos)
- Manual mode exposes VideoRecorder.Current; automatic modes drive recording
themselves (Current is a no-op there to avoid conflicts)
Updated demo, README, and DESIGN.md. Per-test expects serial execution
([DoNotParallelize]) since a screen recorder follows one test at a time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Manual granularity (tests driving recording via VideoRecorder.Current) was an awkward middle ground: anyone wanting full control would delegate to the test framework rather than wire up a CLI option. Removing it lets the extension be purely declarative (like --crashdump/--hangdump) and shrinks the experimental public surface. - Drop VideoCaptureGranularity.Manual and the --capture-video-granularity 'manual' value (now test|session, default test) - Delete the public IVideoRecorder interface and VideoRecorder.Current static accessor (and its no-op fallback); FfmpegVideoRecorder is now a plain internal class driven only by the session handler - Programmatic configuration still flows through AddVideoRecorderProvider( options => ...) (FfmpegPath, ExtraRecorderArguments, Format, etc.) - Update demo to two automatically-recorded tests, README and DESIGN.md Build green (net9.0 + net462); per-test and per-session verified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // Add the video recorder service so tests can record the screen via VideoRecorder.Current. | ||
| testApplicationBuilder.AddVideoRecorderProvider(); | ||
|
|
There was a problem hiding this comment.
Fixed in 0f820a5 - the Program.cs comment now describes the declarative --capture-video usage (recording is automatic; there is no VideoRecorder.Current API).
| A Microsoft.Testing.Platform (MTP) extension that exposes a small **service** letting tests | ||
| start/stop **screen recording** while they run. Recording is performed by an external | ||
| **ffmpeg** process (the only engine that is cross-platform *and* covers screen capture). | ||
| Each produced video is attached to the test session as a file artifact. |
There was a problem hiding this comment.
Fixed in 0f820a5 - the README intro now says recording is automatic/declarative (no test-facing service), matching the code.
|
|
||
| _log?.Invoke("Could not resolve a visible current-process window (headless run, or a terminal whose window is not owned by this process such as Windows Terminal); capturing the full screen instead."); | ||
| } |
There was a problem hiding this comment.
Fixed in 0f820a5 - the fallback log message now reflects the real candidate-window logic (process main window -> foreground -> console) instead of the stale ''not owned by process'' wording.
| Expose a Microsoft.Testing.Platform (MTP) **service** that lets a test (or another extension) | ||
| start/stop **video recording** during a test run, and attach the produced video to the test | ||
| session as an artifact. Primary scenario: **capture the OS screen / a window** while a UI test | ||
| runs (WinForms/WPF/Avalonia/console/browser). |
There was a problem hiding this comment.
Fixed in 0f820a5 - the DESIGN goal now states an automatic/declarative model with no public test-facing API, aligning with the decision below.
MD032 (blanks-around-lists) and MD036 (no-emphasis-as-heading) in DESIGN.md, MD028 (no-blanks-blockquote) in README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ConsumeAsync: use OfType<>.FirstOrDefault instead of SingleOrDefault so a node with >1 state property can't throw out of the consumer pump - Per-test start now gated on recorder availability (no per-test warning spam when ffmpeg is missing) and claims ownership only after Start() is called - Document that the synchronous await of StopAsync is deliberate (serializes stop before the next test's InProgress) and that per-session OnFailure retention is safe because the platform drains data consumers first - Fix stale Program.cs comment and Source doc strings (region capture, not gdigrab title=) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- README intro and DESIGN goal: describe automatic/declarative recording (no public test-facing service), matching the implemented model - Window-capture fallback log message now reflects the actual candidate-window logic (main/foreground/console) instead of the stale "not owned by process" Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| lines, | ||
| line => line.IndexOf("error", StringComparison.OrdinalIgnoreCase) >= 0 | ||
| || line.IndexOf("denied", StringComparison.OrdinalIgnoreCase) >= 0 | ||
| || line.IndexOf("Could not", StringComparison.OrdinalIgnoreCase) >= 0 | ||
| || line.IndexOf("Failed", StringComparison.OrdinalIgnoreCase) >= 0); |
| 2. Build and run the `Playground` sample **with `--capture-video`** (recording is opt-in). It | ||
| contains demo tests (`VideoRecorderDemoTests`) that simulate a couple of seconds of UI work. |
| // Add the video recorder. Run with --capture-video to record the screen | ||
| // (one video per test by default; --capture-video-granularity session for one video). | ||
| testApplicationBuilder.AddVideoRecorderProvider(); |
Summary
Adds a sample Microsoft.Testing.Platform (MTP) extension that exposes a test-facing service to start/stop screen recording during a test run, driving an external ffmpeg process. Produced videos are attached to the test session as artifacts. It's a local sample (not a shipping package) wired into the
Playgroundsample.The public API is marked
[Experimental("TPEXP")].Screenshot of a recording
A frame from a video produced by the extension (window-capture mode targeting a test-pattern window — note it captures just the window, at the correct DPI/region):
What's added
samples/VideoRecorder/(assemblyMicrosoft.Testing.Extensions.VideoRecorder, multi-targetsnet9.0;net462,IsPackable=false):IVideoRecorder+ staticVideoRecorder.Current(no-op fallback) — test-facingStart(name)/StopAsync().FfmpegVideoRecorder— the engine: per-OS capture (gdigrab/x11grab/avfoundation), graceful stop by sendingqto ffmpeg stdin, DPI-correct region-based window capture (MainWindowHandle→ foreground → console, Per-Monitor-V2), empty-file cleanup, best-effort/never-throwing.VideoRecorderSessionHandler—ITestSessionLifetimeHandler+IDataConsumer+IDataProducer+IOutputDeviceDataProducer; tracks failures, persists/discards/attaches videos at session end.VideoRecorderCommandLineProvider/AddVideoRecorderProvider(...)— registration + CLI.samples/Playgroundwith a lenient demo test; documented insamples/VideoRecorder/README.md.Command-line options
Recording is opt-in per run (like
--report-trx/--crashdump):--capture-videoon-failure,alwayson-failure(default — keep only if a test fails) oralways.--capture-video-sourcescreen(default),window--capture-video-argsNotes
mp4/H.264(libx264) is fine for a "bring your own ffmpeg on PATH" scenario; a royalty-freeWebM/VP9format option is provided for any bundled-binary scenario.Verification
net9.0andnet462.--capture-video; verified retention modes (on-failure discards a passing run; always keeps),--capture-video-argspassthrough, and DPI-correct window capture (screenshot above).