KMPwright

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

CapabilityAPIAndroidiOSJVM (CDP)
PageCapability.Tracingpage.tracingUnsupportedUnsupportedSupported
PageCapability.Profilingpage.profilerUnsupportedUnsupportedSupported
PageCapability.VideoRecordingpage.videoUnsupportedUnsupportedSupported
PageCapability.Screencastpage.screencast()UnsupportedUnsupportedSupported

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.startScreencast on 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 at path.
  • start returns 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 contactSheetThreshold of 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:

SituationException
start while recordingIllegalStateException("Video recording is already active")
stop without a recordingKmpwrightException("Video recording is not active")
Output exists and overwrite = falseKmpwrightException("Output already exists: …")
No frame was capturedKmpwrightException("Recording captured no frames")
The encoder falls more than 500 ms behind, or blocks a frame for 2 sKmpwrightException 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, or null when no trace was active.
  • Tracing belongs to the browser connection: only one page per Browser can trace at a time. A second start throws KmpwrightException("Tracing is already owned by another page on this browser").
  • With KmpwrightConfig.enableTraceExport = false, start with a path throws KmpwrightException; 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.

On this page