Recording, tracing and profiling
Record page video with an optional cursor and contact sheet, stream screencast frames, capture Chrome performance traces and V8 CPU profiles; all JVM (CDP) only.
page.requireCapabilities(PageCapability.VideoRecording, PageCapability.Tracing)
page.tracing.start(path = "build/trace.json")
page.video.start("build/checkout.mp4", contactSheetPath = "build/checkout.png")
page.goto("https://example.com/shop")
page.getByRole("button", name = "Add to cart").click()
val video = page.video.stop()
val trace = page.tracing.stop()
println("${video.path}: ${video.frameCount} frames, ${video.durationMs} ms, ${video.width}x${video.height}")Everything on this page uses Chrome DevTools Protocol domains (Tracing, Profiler, Page.startScreencast), so it works only on the JVM backend.
Backend support
| Capability | API | Android | iOS | JVM (CDP) |
|---|---|---|---|---|
PageCapability.Tracing | page.tracing | Unsupported | Unsupported | Supported |
PageCapability.Profiling | page.profiler | Unsupported | Unsupported | Supported |
PageCapability.VideoRecording | page.video | Unsupported | Unsupported | Supported |
PageCapability.Screencast | page.screencast() | Unsupported | Unsupported | Supported |
On Android and iOS every call throws KmpwrightUnsupportedException (for example "Tracing is unavailable on this backend"). page.screencast() throws when you call it, before you collect the flow.
To fail early in shared code, check capabilities first:
if (page.capabilities().supports(PageCapability.VideoRecording)) {
page.video.start("run.mp4")
}
// or throw up front:
page.requireCapabilities(PageCapability.Tracing)See Capabilities.
Video
page.video.start(
path = "build/login.webm",
fps = 24,
cursor = true,
contactSheetPath = "build/login-sheet.png",
)
// ... drive the page ...
val result = page.video.stop()
result.contactFrames.forEach { println("frame ${it.index} at ${it.timestampMs} ms") }page.video.start(path, ...) records the page to a video file until page.video.stop() (agent-browser record). Recording is video only, without audio.
Encoding needs an ffmpeg executable on the JVM host. start runs ffmpeg -version and a short test encode first, and throws KmpwrightException("ffmpeg is required before recording: …") when it is missing. Point ffmpegExecutable at a full path if ffmpeg is not on PATH.
Prop
Type
Out-of-range values make start throw IllegalArgumentException.
page.video.stop() returns:
data class RecordingResult(
val path: String, // absolute output path
val fps: Int,
val frameCount: Long,
val durationMs: Long, // frameCount * 1000 / fps
val width: Int, // first frame size, rounded up to even numbers
val height: Int,
val contactSheetPath: String? = null,
val contactFrames: List<RecordingContactFrame> = emptyList() // (timestampMs, index) of each sheet frame
)How it works:
- Frames come from
Page.startScreencaston a separate CDP session, so you can stream a screencast at the same time. - Chromium only sends frames when the page changes. The encoder writes frames at a fixed
fps, repeating the latest frame, so the video plays in real time. Frames are letterboxed onto a canvas the size of the first frame. - The video is encoded into a temporary file next to the output and moved into place on
stop(), so a failed recording never leaves a partial file atpath. startreturns once the first frame is encoded (up to 10 s).- The cursor is drawn from trusted mouse pointer events, whether they come from KMPwright's CDP mouse input or a person using a headed browser. It is drawn into the video only, never into the page or the contact sheet.
- Contact sheet: each frame is reduced to 640 px wide and kept when at least
contactSheetThresholdof its pixels changed noticeably since the last kept frame; the last frame is always included. Kept frames are laid out four per row and labeled with their time and frame index. When the limit is reached, newer key frames replace the last slot.
Errors:
| Situation | Exception |
|---|---|
start while recording | IllegalStateException("Video recording is already active") |
stop without a recording | KmpwrightException("Video recording is not active") |
Output exists and overwrite = false | KmpwrightException("Output already exists: …") |
| No frame was captured | KmpwrightException("Recording captured no frames") |
| The encoder falls more than 500 ms behind, or blocks a frame for 2 s | KmpwrightException from stop() |
Closing the page stops an active recording. Recording passes the action policy as ActionType.Screenshot.
Playwright: video is a context option (recordVideo) there and is saved when the page closes; here you start and stop it explicitly on the page.
Screencast
Experimental. The frame stream may change.
page.screencast(maxFps = 10, quality = 70).collect { frame ->
// frame.bytes is a JPEG
preview.update(frame.bytes)
}page.screencast(maxFps = 30, quality = 80, maxWidth = 1920, maxHeight = 1080): Flow<ScreencastFrame> streams JPEG frames of the page (agent-browser screencast). The flow is cold: each collector attaches its own CDP session and starts Page.startScreencast; cancelling the collector stops it.
The flow is conflated: when your collector is slower than the page, intermediate frames are dropped and you always get the latest one. Chromium only emits frames when something changes, so a static page produces few frames; maxFps limits delivery and never adds duplicate frames.
Prop
Type
ScreencastFrame fields: bytes (JPEG), timestampMs, viewportWidth, viewportHeight (the device size Chromium reports in the frame metadata), pageScaleFactor, scrollOffsetX, scrollOffsetY, mimeType ("image/jpeg").
The flow fails with KmpwrightException when the page crashes, its session detaches, the CDP connection closes, or a frame exceeds 16 MiB. Collection passes the action policy as ActionType.Screenshot.
Tracing
page.tracing.start(screenshots = true, path = "build/trace.json")
page.goto("https://example.com")
val json: ByteArray? = page.tracing.stop()page.tracing records a Chrome performance trace with the CDP Tracing domain. The result is Chrome trace-event JSON, which you can load in the Performance panel of Chrome DevTools or in Perfetto.
Playwright: context.tracing produces a Playwright trace (a zip for the Trace Viewer). KMPwright's tracing is the Chromium performance trace, like Puppeteer's page.tracing.
Prop
Type
stop()returns the trace JSON, ornullwhen no trace was active.- Tracing belongs to the browser connection: only one page per
Browsercan trace at a time. A secondstartthrowsKmpwrightException("Tracing is already owned by another page on this browser"). - With
KmpwrightConfig.enableTraceExport = false,startwith apaththrowsKmpwrightException; tracing without a path still works.
CPU profiling
page.profiler.start()
page.getByRole("button", name = "Run report").click()
val profile: String? = page.profiler.stop()
// JVM: java.io.File("report.cpuprofile").writeText(profile!!)page.profiler runs the V8 sampling profiler of the page (agent-browser profiler) through the CDP Profiler domain. stop() returns the Profiler.Profile object as JSON, which DevTools can open as a .cpuprofile file, or null when the profiler was not running.
Related
- Screenshots and PDFs: single images work on every backend.
- Capabilities
- Page reference
Reading, vitals, React and WebMCP
Fetch pages as Markdown with page.read and HttpReader, measure Core Web Vitals, inspect React component trees, and call WebMCP tools exposed by a page.
Compose
Host an automated WebView in Jetpack Compose with the kmpwright-compose module, or share one WebView across Android and iOS in Compose Multiplatform with AndroidView and UIKitView.