@milkstraw/traceframe 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/docs/recording-flows.md +10 -0
- package/package.json +1 -1
- package/src/cli.mjs +1 -1
- package/src/config.mjs +1 -1
- package/src/jpeg-capture.mjs +10 -4
package/README.md
CHANGED
|
@@ -61,16 +61,18 @@ The cursor and click pulses are always drawn at the full frame rate. Chromium ma
|
|
|
61
61
|
|
|
62
62
|
Use `--capture-diagnostics` (`captureDiagnostics: true` with `record` or `recordPage`) to include `captureStats` in the result. It reports received frames, their timestamp span and rate, the largest forward timestamp jumps and delivery gaps, and out-of-order timestamps. `lateFrames` counts frames older than the first received frame or the last written frame; `overflowFrames` counts frames dropped at the 64 MiB capture-buffer limit, and `maxBufferedBytes` reports its peak use. `writeWaitMs` and `maxWriteWaitMs` measure total and maximum time waiting for frame-write callbacks, including the final static frame; they do not measure durable disk writes. Slow writes can delay frame acknowledgments and reduce what Chromium sends, even when the writer drop counters stay at zero. These measurements describe frames delivered by Chromium. They do not count distinct pictures or prove smoothness; a static page legitimately delivers few frames, and a 30 FPS video cannot supply 60 distinct source frames per second.
|
|
63
63
|
|
|
64
|
-
`--capture-quality <1–100>` (`captureQuality` in either API) controls the JPEG frames exported by Chromium; the default
|
|
64
|
+
`--capture-quality <1–100>` (`captureQuality` in either API) controls the JPEG frames exported by Chromium; the default is 85. In 3024 × 1964 tests, 85 made captured frames about a third smaller than 95, with slightly lower CPU use and no visible difference in the exported MP4. Quality did not change how many frames Chromium captured. Use 95 for maximum detail, or a lower setting such as 75 for smaller temporary captures with softer fine detail. This is separate from the final MP4 encoder. Check representative moving content and text at the intended resolution before choosing a non-default value.
|
|
65
65
|
|
|
66
66
|
Since v0.6.0, the retained raw file is `capture.mkv` instead of `capture.webm`; update scripts that inspect it. Final output remains MP4.
|
|
67
67
|
|
|
68
|
-
Traceframe saves Chromium's JPEG frames and presentation timestamps directly into a temporary `capture.mkv`, then encodes the final MP4 after the flow ends. This avoids a live intermediate video encode and works with CPU or GPU browser rendering. Chromium frame timestamps are retained with
|
|
68
|
+
Traceframe saves Chromium's JPEG frames and presentation timestamps directly into a temporary `capture.mkv`, then encodes the final MP4 after the flow ends. This avoids a live intermediate video encode and works with CPU or GPU browser rendering. Chromium frame timestamps are retained with microsecond precision; a 150 ms buffer orders delayed arrivals. The first received frame establishes the audio clock, so subsequently delivered frames older than that origin are dropped. Slow storage applies backpressure. Moving content needs more temporary disk space than an inter-frame codec: at high resolution, allow hundreds of megabytes for short takes and potentially gigabytes for longer ones. Static pages need only a few stored frames. If your temporary directory is backed by RAM, choose a disk-backed `--run-dir` for large takes and remove it afterward. The final renderer supplies the requested frame rate while keeping cursor animations smooth.
|
|
69
69
|
|
|
70
70
|
GPU rendering and hardware video decoding can help the browser, but frame export still costs CPU. The bundled headless browser used software rendering on the tested Linux setup; selecting an executable or adding a GPU flag does not guarantee acceleration. For a caller-owned browser, configure and verify its GPU use before `recordPage`; use full Chromium for extensions. A GPU capability flag alone does not establish that the active video is hardware-decoded. When measuring performance, keep the browser version, source codec, viewport and device scale fixed, and compare recorded image updates and audio timing as well as file metadata.
|
|
71
71
|
|
|
72
72
|
Start performance comparisons with the Chromium build bundled with Playwright. A custom `--browser` executable can have different screencast performance even when recording succeeds. Traceframe preserves your requested resolution and quality. If the workload cannot sustain the desired cadence, increasing the encoded FPS repeats existing frames; choose a lower quality or smaller dimensions explicitly if that tradeoff suits your recording.
|
|
73
73
|
|
|
74
|
+
For video-heavy flows, see [checking video playback](docs/recording-flows.md#check-video-playback), including optional page effects and media sizing.
|
|
75
|
+
|
|
74
76
|
Use `--run-dir .traceframe/runs/name` (`runDirectory` in the API) to keep the raw capture and FFmpeg filter file, including a partial capture if the flow fails during recording. An interrupted or failed capture can be incomplete; a retained file is not proof that recording finalized successfully. On a preparation or recording failure, Traceframe also tries to save a viewport screenshot as `failure.png`. Keep that directory out of Git, because captures and screenshots can contain private page data.
|
|
75
77
|
|
|
76
78
|
When available, failure output includes the error code, flow-file locations, elapsed recording time, and paths to artifacts saved for that attempt. Without `--run-dir`, temporary captures are removed and no failure screenshot is saved. See [diagnosing a failed take](docs/recording-flows.md#diagnose-a-failed-take).
|
package/docs/recording-flows.md
CHANGED
|
@@ -24,6 +24,16 @@ Put setup and page readiness in `ready({ page, signal })`. This runs after initi
|
|
|
24
24
|
|
|
25
25
|
If your script already owns the page or an extension context, use [`recordPage`](../README.md#record-a-page-you-already-opened) with an inline callback. Prepare the page before calling it; the caller owns navigation and browser cleanup.
|
|
26
26
|
|
|
27
|
+
### Check video playback
|
|
28
|
+
|
|
29
|
+
For a video-heavy journey, check playback in your first take at the final recording settings; a separate check before recording is not needed. Record that take with `--capture-diagnostics` (`captureDiagnostics: true` in the API) and [inspect the saved motion](#record-inspect-and-refine) of its video segment. Capture diagnostics count delivered frames, which can contain repeated pictures; a final MP4 reporting 60 FPS does not establish 60 distinct captured images per second, and a 30 FPS source cannot provide 60 distinct video frames per second. If the video looks smooth, later takes need no new playback check until the browser, recording settings or workload changes.
|
|
30
|
+
|
|
31
|
+
On YouTube, the player's **Settings (gear) → Ambient mode → Off** can reduce expensive background drawing during CPU-only playback. Ambient mode is available only with YouTube's dark theme; check the setting under the theme you will record. Disable this decorative effect only when it is unnecessary for the demonstration, and verify the setting before the take. Use the site's setting in `ready`, or prepare the caller-owned page before `recordPage`; Traceframe does not change it automatically.
|
|
32
|
+
|
|
33
|
+
For video assets you control, matching the source dimensions to the displayed physical pixels can avoid costly browser resizing during CPU-only playback. Physical dimensions include device scale: a video displayed 1512 CSS pixels wide at scale 2 occupies 3024 physical pixels. Preparing another asset can change image quality, so compare it with the original and preserve the intended framing. Do not silently replace third-party media, crop content or lower the required output resolution to claim a performance improvement.
|
|
34
|
+
|
|
35
|
+
If the video looks choppy, apply any preparation above that suits the demonstration and make one more take at the same recording settings. If it is still choppy, or nothing applies, keep the requested output dimensions, playback speed and source quality, finish the requested recording and report the limitation. For optional diagnosis of playback without recording, let buffering and any extension processing warm up, then read the video's `getVideoPlaybackQuality()` counters before and after a representative segment with recording off, for example in `ready` or before `recordPage`. Page preparation can improve playback while capture remains expensive.
|
|
36
|
+
|
|
27
37
|
## Save and switch login state
|
|
28
38
|
|
|
29
39
|
Add `.auth/` to your project's `.gitignore`; saved state can grant access to the account. To create it interactively, install full Chromium and open Playwright's login window:
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -46,7 +46,7 @@ Options:
|
|
|
46
46
|
--viewport-height <pixels> CSS viewport height
|
|
47
47
|
--scale <1|2> Device scale factor; the video is the viewport times this
|
|
48
48
|
--fps <30|60> Encoded frame rate
|
|
49
|
-
--capture-quality <1–100> JPEG frame quality (default:
|
|
49
|
+
--capture-quality <1–100> JPEG frame quality (default: 85); higher keeps more detail
|
|
50
50
|
--capture-diagnostics Include frame delivery cadence and gaps in the JSON summary
|
|
51
51
|
--post-roll <seconds> Tail after the flow (default: 0.3)
|
|
52
52
|
--ready-timeout <seconds> Playwright default timeout during navigation and ready() (default: 30)
|
package/src/config.mjs
CHANGED
|
@@ -54,7 +54,7 @@ function recordingConfig(options, viewport, defaults) {
|
|
|
54
54
|
fps: options.fps ?? defaults.fps,
|
|
55
55
|
}
|
|
56
56
|
check(video.fps === 30 || video.fps === 60, 'FPS must be 30 or 60.')
|
|
57
|
-
const captureQuality = options.captureQuality ??
|
|
57
|
+
const captureQuality = options.captureQuality ?? 85
|
|
58
58
|
check(Number.isInteger(captureQuality) && captureQuality >= 1 && captureQuality <= 100, 'Capture quality must be an integer from 1 to 100.')
|
|
59
59
|
check(options.captureDiagnostics === undefined || typeof options.captureDiagnostics === 'boolean', 'Capture diagnostics must be a boolean.')
|
|
60
60
|
check(video.width % 2 === 0 && video.height % 2 === 0, 'At scale 1, the viewport width and height must be even.')
|
package/src/jpeg-capture.mjs
CHANGED
|
@@ -6,6 +6,9 @@ import { finished } from 'node:stream/promises'
|
|
|
6
6
|
// A short reorder window handles JPEGs that finish compression out of order.
|
|
7
7
|
const REORDER_MS = 150
|
|
8
8
|
const MAX_BUFFER_BYTES = 64 * 1024 * 1024
|
|
9
|
+
// Store microseconds. Millisecond rounding can move distinct pictures into the
|
|
10
|
+
// same output frame when export resamples to a fixed frame rate.
|
|
11
|
+
const TIMESTAMP_SCALE_NS = 1_000
|
|
9
12
|
|
|
10
13
|
export function createJpegCapture(path, { width, height, fps, onError }) {
|
|
11
14
|
const output = createWriteStream(path)
|
|
@@ -25,7 +28,7 @@ export function createJpegCapture(path, { width, height, fps, onError }) {
|
|
|
25
28
|
try {
|
|
26
29
|
await new Promise((resolve, reject) => {
|
|
27
30
|
output.cork()
|
|
28
|
-
output.write(cluster(
|
|
31
|
+
output.write(cluster(ticks(frame.timestamp - origin), frame.data.length))
|
|
29
32
|
output.write(frame.data, cause => cause ? reject(cause) : resolve())
|
|
30
33
|
output.uncork()
|
|
31
34
|
})
|
|
@@ -70,7 +73,7 @@ export function createJpegCapture(path, { width, height, fps, onError }) {
|
|
|
70
73
|
await finished(output)
|
|
71
74
|
if (last) {
|
|
72
75
|
const duration = Buffer.from('4489880000000000000000', 'hex')
|
|
73
|
-
duration.writeDoubleBE(
|
|
76
|
+
duration.writeDoubleBE(ticks(last.timestamp - origin + 1000 / fps), 3)
|
|
74
77
|
const file = await open(path, 'r+')
|
|
75
78
|
try {
|
|
76
79
|
const { bytesWritten } = await file.write(duration, 0, duration.length, durationOffset)
|
|
@@ -94,7 +97,10 @@ export function createJpegCapture(path, { width, height, fps, onError }) {
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// The fixed Matroska subset contains one MJPEG track and one keyframe per cluster.
|
|
97
|
-
// Timestamps are
|
|
100
|
+
// Timestamps are microseconds; JPEG dimensions and pixels remain unchanged.
|
|
101
|
+
function ticks(milliseconds) {
|
|
102
|
+
return Math.round(milliseconds * 1e6 / TIMESTAMP_SCALE_NS)
|
|
103
|
+
}
|
|
98
104
|
function uint(value) {
|
|
99
105
|
const bytes = []
|
|
100
106
|
do { bytes.unshift(value % 256); value = Math.floor(value / 256) } while (value)
|
|
@@ -119,7 +125,7 @@ function header(width, height, fps) {
|
|
|
119
125
|
])
|
|
120
126
|
// Reserve a Void element, then replace it with Duration after the last write.
|
|
121
127
|
const durationSlot = Buffer.from('ec89000000000000000000', 'hex')
|
|
122
|
-
const info = element('1549a966', [element('2ad7b1',
|
|
128
|
+
const info = element('1549a966', [element('2ad7b1', TIMESTAMP_SCALE_NS), element('4d80', 'traceframe'), element('5741', 'traceframe'), durationSlot])
|
|
123
129
|
const bytes = Buffer.concat([prefix, info,
|
|
124
130
|
element('1654ae6b', [element('ae', [element('d7', 1), element('73c5', 1), element('83', 1), element('9c', 0), element('86', 'V_MJPEG'), element('23e383', Math.round(1e9 / fps)), element('e0', [element('b0', width), element('ba', height)])])]),
|
|
125
131
|
])
|