Implementation:DevExpress Testcafe VideoRecorderProcess
| Knowledge Sources | |
|---|---|
| Domains | Video Recording, FFmpeg Integration |
| Last Updated | 2026-02-12 12:00 GMT |
Overview
VideoRecorderProcess orchestrates FFmpeg-based video recording of test executions, spanning three classes: a low-level FFmpeg process wrapper, a per-browser-job recorder coordinator, and a per-test-run recorder that manages temp files and timecodes.
Description
The video recording subsystem consists of three cooperating classes:
VideoRecorderProcess (process.js) wraps a spawned FFmpeg child process. It pipes raw frame data (PNG images from the browser provider) into FFmpeg's stdin, which encodes them as H.264 video. It manages the FFmpeg lifecycle -- spawning, frame capture loop, and graceful shutdown -- with configurable encoding options merged over sensible defaults (image2pipe input, libx264 codec, ultrafast preset, yuv420p pixel format, 30fps output).
VideoRecorder (recorder.js) extends EventEmitter and coordinates recording across an entire BrowserJob. It subscribes to browser job lifecycle events (start, done, test-run-create, test-run-ready, test-run-before-done, test-run-restart), creates TestRunVideoRecorder instances per test, manages a temp directory for intermediate files, handles single-file concatenation via FFmpeg, and copies final video files to the target path. It supports failedOnly mode to discard recordings of passing tests.
TestRunVideoRecorder (test-run-video-recorder.js) manages video recording for a single test run. It creates a VideoRecorderProcess, generates temp file names, tracks start times and timecodes for quarantine restarts, and exposes capability checks (isVideoSupported, isVideoEnabled).
Usage
The Videos class (instantiated by Task when opts.videoPath is set) creates one VideoRecorder per BrowserJob. The recorder automatically manages the full recording lifecycle through event subscriptions. Test authors configure video recording via CLI options or API (--video, --video-options, --video-encoding-options).
Code Reference
Source Location
- Repository: DevExpress_Testcafe
- File (Process): src/video-recorder/process.js
- Lines: 1-198
- File (Recorder): src/video-recorder/recorder.js
- Lines: 1-202
- File (TestRunVideoRecorder): src/video-recorder/test-run-video-recorder.js
- Lines: 1-103
Signature (VideoRecorderProcess)
const DEFAULT_OPTIONS = {
'f': 'image2pipe',
'y': true,
'use_wallclock_as_timestamps': 1,
'i': 'pipe:0',
'c:v': 'libx264',
'preset': 'ultrafast',
'pix_fmt': 'yuv420p',
'vf': 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
'r': 30,
};
export default class VideoRecorderProcess extends AsyncEmitter {
constructor (basePath, ffmpegPath, connection, customOptions) {
this.customOptions = customOptions;
this.videoPath = basePath;
this.connection = connection;
this.ffmpegPath = ffmpegPath;
this.ffmpegProcess = null;
this.disposed = false;
this.closed = false;
// ...
}
get active();
async init();
async dispose();
async startCapturing();
async finishCapturing();
}
Signature (VideoRecorder)
export default class VideoRecorder extends EventEmitter {
constructor (browserJob, basePath, opts, encodingOpts, warningLog) {
this.browserJob = browserJob;
this.basePath = basePath;
this.failedOnly = opts.failedOnly;
this.singleFile = opts.singleFile;
this.ffmpegPath = opts.ffmpegPath;
this.customPathPattern = opts.pathPattern;
this.timeStamp = opts.timeStamp;
this.encodingOptions = encodingOpts;
this.warningLog = warningLog;
this.tempDirectory = new TempDirectory(TEMP_DIR_PREFIX);
this.testRunVideoRecorders = {};
// ...
}
}
Signature (TestRunVideoRecorder)
export default class TestRunVideoRecorder {
constructor ({ testRun, test, index }, { path, ffmpegPath, encodingOptions }) {
this.testRun = testRun;
this.test = test;
this.index = index;
this.tempFiles = null;
this.videoRecorder = null;
this.path = path;
this.ffmpegPath = ffmpegPath;
this.encodingOptions = encodingOptions;
this.start = null;
this.timecodes = null;
}
get testRunInfo();
get hasErrors();
async startCapturing();
async finishCapturing();
async init();
async isVideoSupported();
async isVideoEnabled();
onTestRunRestart();
}
Import
import VideoRecorderProcess from '../video-recorder/process';
import VideoRecorder from '../video-recorder/recorder';
import TestRunVideoRecorder from '../video-recorder/test-run-video-recorder';
I/O Contract
Inputs (VideoRecorderProcess)
| Name | Type | Required | Description |
|---|---|---|---|
| basePath | string |
Yes | Output video file path |
| ffmpegPath | string |
Yes | Path to the FFmpeg binary |
| connection | BrowserConnection |
Yes | Browser connection providing frame data via its provider |
| customOptions | object |
No | Custom FFmpeg options merged over defaults |
Inputs (VideoRecorder)
| Name | Type | Required | Description |
|---|---|---|---|
| browserJob | BrowserJob |
Yes | The browser job whose test runs are being recorded |
| basePath | string |
Yes | Base output path for video files |
| opts | object |
Yes | Options: failedOnly, singleFile, ffmpegPath, pathPattern, timeStamp
|
| encodingOpts | object |
Yes | FFmpeg encoding options passed to the process |
| warningLog | WarningLog |
Yes | Warning log for unsupported browsers and path issues |
Inputs (TestRunVideoRecorder)
| Name | Type | Required | Description |
|---|---|---|---|
| testRun | TestRun |
Yes | The test run instance being recorded |
| test | Test |
Yes | The test definition |
| index | number |
Yes | Test index for file naming and recorder lookup |
| path | string |
Yes | Temp directory path for intermediate video files |
| ffmpegPath | string |
Yes | Path to the FFmpeg binary |
| encodingOptions | object |
Yes | FFmpeg encoding options |
Outputs
| Name | Type | Description |
|---|---|---|
| Video files | .mp4 files |
H.264 encoded video files at the configured output path |
| testRunInfo | object |
Test metadata: { testIndex, fixture, test, timecodes, alias, parsedUserAgent }
|
| hasErrors | boolean |
Whether the associated test run had errors (used for failedOnly filtering)
|
| test-run-video-saved event | { testRun, videoPath, singleFile, timecodes? } |
Emitted by VideoRecorder when a video file is saved |
Usage Examples
// VideoRecorder is created internally by the Videos class
const recorder = new VideoRecorder(
browserJob,
videoPath,
{
failedOnly: false,
singleFile: false,
ffmpegPath: '/usr/bin/ffmpeg',
pathPattern: '${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.mp4',
timeStamp: moment(),
},
{ crf: 18 }, // encoding options
warningLog
);
// Events are auto-subscribed; recorder manages lifecycle via browserJob events
// TestRunVideoRecorder is created by VideoRecorder._onTestRunCreate
const testRunRecorder = new TestRunVideoRecorder(
{ testRun, test, index },
{ path: tempDir, ffmpegPath: '/usr/bin/ffmpeg', encodingOptions: {} }
);
await testRunRecorder.init(); // Spawns FFmpeg process
await testRunRecorder.startCapturing(); // Begins frame capture loop
// ... test executes ...
await testRunRecorder.finishCapturing(); // Stops capture and disposes FFmpeg
Internal Mechanics
FFmpeg Default Configuration
The process spawns FFmpeg with these defaults (overridable via customOptions):
- Input format:
image2pipe(reads PNG frames from stdin) - Codec:
libx264(H.264) - Preset:
ultrafast(fastest encoding, larger files) - Pixel format:
yuv420p(maximum compatibility) - Scaling filter: Ensures frame dimensions are divisible by 2 (yuv420p requirement)
- Frame rate: 30fps output
- Overwrite: Enabled (
-y)
Frame Capture Loop
VideoRecorderProcess._capture runs a continuous loop while the process is active:
- Calls
connection.provider.getVideoFrameData(connectionId)to get a PNG frame - If frame data exists, writes it to FFmpeg's stdin via
_addFrame - If no frame data, waits 20ms before retrying
- Errors are caught and logged without breaking the loop
Single-File Concatenation
When singleFile mode is enabled, the VideoRecorder concatenates video files across test runs using FFmpeg's concat demuxer. A temporary merge config file lists the files to concatenate, and spawnSync runs FFmpeg in concat mode.
Temp File Management
TestRunVideoRecorder._generateTempNames creates three temp file paths per test run:
tmp-video-{connectionId}.mp4-- the raw video outputconfig-{connectionId}.txt-- concat config for single-file modetmp-video-merge-{connectionId}.mp4-- intermediate merge output
Timecodes
In quarantine mode, onTestRunRestart records timecodes (milliseconds from start) at each restart boundary. These are included in the test-run-video-saved event for reporter integration.