Skip to content

Add experimental video recorder sample extension for MTP#9377

Draft
Evangelink wants to merge 8 commits into
microsoft:mainfrom
Evangelink:evangelink-mtp-video-recorder-extension
Draft

Add experimental video recorder sample extension for MTP#9377
Evangelink wants to merge 8 commits into
microsoft:mainfrom
Evangelink:evangelink-mtp-video-recorder-extension

Conversation

@Evangelink

Copy link
Copy Markdown
Member

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 Playground sample.

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):

Sample recording frame

What's added

samples/VideoRecorder/ (assembly Microsoft.Testing.Extensions.VideoRecorder, multi-targets net9.0;net462, IsPackable=false):

  • IVideoRecorder + static VideoRecorder.Current (no-op fallback) — test-facing Start(name) / StopAsync().
  • FfmpegVideoRecorder — the engine: per-OS capture (gdigrab / x11grab / avfoundation), graceful stop by sending q to ffmpeg stdin, DPI-correct region-based window capture (MainWindowHandle → foreground → console, Per-Monitor-V2), empty-file cleanup, best-effort/never-throwing.
  • VideoRecorderSessionHandlerITestSessionLifetimeHandler + IDataConsumer + IDataProducer + IOutputDeviceDataProducer; tracks failures, persists/discards/attaches videos at session end.
  • VideoRecorderCommandLineProvider / AddVideoRecorderProvider(...) — registration + CLI.
  • Wired into samples/Playground with a lenient demo test; documented in samples/VideoRecorder/README.md.

Command-line options

Recording is opt-in per run (like --report-trx / --crashdump):

Option Values Description
--capture-video (none), on-failure, always Enable recording. Retention: on-failure (default — keep only if a test fails) or always.
--capture-video-source screen (default), window Capture the full screen or just the current process window (Windows; falls back to full screen elsewhere).
--capture-video-args any string Extra args passed to the underlying recorder (ffmpeg).

Notes

  • Licensing: default mp4/H.264 (libx264) is fine for a "bring your own ffmpeg on PATH" scenario; a royalty-free WebM/VP9 format option is provided for any bundled-binary scenario.
  • Window capture needs a real visible window and an accessible interactive desktop; it falls back to full screen (with a logged note) on headless/CI or Windows Terminal/ConPTY.

Verification

  • Builds clean for net9.0 and net462.
  • Demo passes with and without --capture-video; verified retention modes (on-failure discards a passing run; always keeps), --capture-video-args passthrough, and DPI-correct window capture (screenshot above).
  • The extension code went through multiple expert-review rounds (lifetime/threading/error-handling hardening).

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>
Copilot AI review requested due to automatic review settings June 23, 2026 19:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/VideoRecorder project 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/Playground app.
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

Comment on lines +45 to +48
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a74253a - the snippet now uses options.Source = VideoCaptureSource.Screen (the renamed property).

Comment on lines +65 to +67
| `--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`. |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a74253a - the table now documents --capture-video-source with screen|window.

Comment thread samples/VideoRecorder/README.md Outdated
Comment on lines +78 to +82
# 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a74253a - examples now use --capture-video-source window and a direct --capture-video-args=... form.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --capture-video-window flag was later replaced by --capture-video-source screen|window; docs/examples updated accordingly in 0f820a5.

Comment thread samples/VideoRecorder/README.md Outdated
Comment on lines +99 to +103
> 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a74253a - removed the duplicated retention note, keeping a single explanation.

Comment on lines +269 to +278
// 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a74253a - removed the accidental duplicate comment block.

Comment on lines +477 to +480
// 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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Evangelink and others added 2 commits June 23, 2026 21:44
…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>
Copilot AI review requested due to automatic review settings June 23, 2026 19:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 14/15 changed files
  • Comments generated: 1

Comment on lines +46 to +58
_messageBus = messageBus;
_outputDevice = outputDevice;
_logger = logger;

_enabled = commandLineOptions.IsOptionSet(VideoRecorderCommandLineProvider.EnableOptionName);
if (!_enabled)
{
return;
}

ApplyCommandLineOverrides(options, commandLineOptions);
_persistMode = options.PersistMode;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Evangelink and others added 2 commits June 24, 2026 08:55
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>
Copilot AI review requested due to automatic review settings June 24, 2026 07:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 12/13 changed files
  • Comments generated: 4

Comment thread samples/Playground/Program.cs Outdated
Comment on lines +43 to +45
// Add the video recorder service so tests can record the screen via VideoRecorder.Current.
testApplicationBuilder.AddVideoRecorderProvider();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f820a5 - the Program.cs comment now describes the declarative --capture-video usage (recording is automatic; there is no VideoRecorder.Current API).

Comment thread samples/VideoRecorder/README.md Outdated
Comment on lines +3 to +6
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f820a5 - the README intro now says recording is automatic/declarative (no test-facing service), matching the code.

Comment on lines +392 to +394

_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.");
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread samples/VideoRecorder/DESIGN.md Outdated
Comment on lines +9 to +12
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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f820a5 - the DESIGN goal now states an automatic/declarative model with no public test-facing API, aligning with the decision below.

Amaury Levé and others added 3 commits June 24, 2026 10:03
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>
Copilot AI review requested due to automatic review settings June 24, 2026 08:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 12/13 changed files
  • Comments generated: 3

Comment on lines +254 to +258
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);
Comment on lines +20 to +21
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.
Comment on lines +43 to +45
// 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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants