@milkstraw/traceframe 0.6.0 → 0.6.1

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 CHANGED
@@ -65,7 +65,7 @@ Use `--capture-diagnostics` (`captureDiagnostics: true` with `record` or `record
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 millisecond 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 before drawing the cursor.
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 millisecond 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milkstraw/traceframe",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Record scripted browser flows and render smooth cursor overlays into video.",
5
5
  "keywords": [
6
6
  "playwright",
package/src/render.mjs CHANGED
@@ -57,20 +57,24 @@ export async function renderVideo({ rawPath, trace, directory, config, ffmpeg, s
57
57
  // Event times are seconds from the first frame of the raw video.
58
58
  export function buildFilterScript({ trace, scale, fps = 60, audio, overlayEofAction = 'pass' }) {
59
59
  const { clicks, moves } = trace
60
+ const animatedOverlays = clicks.length > 0 || moves.length > 0
61
+ const cadence = `tpad=stop_mode=clone:stop_duration=1,fps=${fps}`
60
62
  // Pass mode avoids waiting on looping image inputs; FFmpeg 7 needs repeat for padding.
61
63
  // yuv444 places overlays on whole pixels rather than snapping them to even pixels.
62
64
  const overlay = `eof_action=${overlayEofAction}:format=yuv444`
63
- // Expand sparse capture timestamps before overlays so the cursor stays smooth
64
- // even on a static page. Padding also covers a sub-frame take or final rounding.
65
+ // Animated overlays need every output tick, even on a static page.
66
+ // With stationary overlays, composite before duplicating identical frames.
67
+ // Padding also covers a sub-frame take or final rounding.
65
68
  // Chrome JPEGs are full range; normalize before compositing limited-range overlays.
66
69
  // Convert to the overlay format before duplicating frames. Converting afterward
67
70
  // can queue a full-size buffer for every repeated frame in a long static gap.
68
- const filters = [`[0:v]scale=out_range=tv,format=yuv420p,format=yuv444p,setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=1,fps=${fps}[video0]`]
71
+ const filters = [`[0:v]scale=out_range=tv,format=yuv420p,format=yuv444p,setpts=PTS-STARTPTS${animatedOverlays ? ',' + cadence : ''}[video0]`]
69
72
  if (clicks.length) filters.push(`[2:v]format=rgba,split=${clicks.length}${clicks.map((_, index) => `[pulse${index}]`).join('')}`)
70
73
  clicks.forEach((click, index) => filters.push(...clickFilter(click, index, scale, overlay)))
71
74
 
72
75
  const cursorLabels = moves.map((_, index) => `[cursor${index}]`).join('')
73
- filters.push(`[1:v]format=rgba,split=${moves.length + 1}${cursorLabels}[cursor${moves.length}]`)
76
+ // Convert the cursor once before splitting it into movement segments.
77
+ filters.push(`[1:v]format=yuva444p,split=${moves.length + 1}${cursorLabels}[cursor${moves.length}]`)
74
78
  const coordinate = (value, hotspot) => number(value * scale - hotspot * scale)
75
79
  const initialEnable = moves.length ? `:enable='lt(t,${number(moves[0].at)})'` : ''
76
80
  filters.push(`[video${clicks.length}][cursor0]overlay=x='${coordinate(trace.initialPointer.x, CURSOR_HOTSPOT.x)}':y='${coordinate(trace.initialPointer.y, CURSOR_HOTSPOT.y)}':${overlay}${initialEnable}[cursorVideo0]`)
@@ -85,7 +89,7 @@ export function buildFilterScript({ trace, scale, fps = 60, audio, overlayEofAct
85
89
  const y = `${coordinate(move.from.y, CURSOR_HOTSPOT.y)}+${number((move.to.y - move.from.y) * scale)}*${eased}`
86
90
  filters.push(`[cursorVideo${index}][cursor${index + 1}]overlay=x='${x}':y='${y}':${overlay}:enable='${enable}'[cursorVideo${index + 1}]`)
87
91
  })
88
- filters.push(`[cursorVideo${moves.length}]colorspace=ispace=bt470bg:iprimaries=bt709:itrc=bt709:all=bt709:format=yuv420p[final]`)
92
+ filters.push(`[cursorVideo${moves.length}]colorspace=ispace=bt470bg:iprimaries=bt709:itrc=bt709:all=bt709:format=yuv420p${animatedOverlays ? '' : ',' + cadence}[final]`)
89
93
  if (audio) {
90
94
  const input = clicks.length ? 3 : 2
91
95
  const trim = number(Math.max(0, -audio.offset))