diff --git a/packages/extension/src/background/gif.ts b/packages/extension/src/background/gif.ts index 123e2a0..50668cd 100644 --- a/packages/extension/src/background/gif.ts +++ b/packages/extension/src/background/gif.ts @@ -145,6 +145,15 @@ context.fillStyle = "#10121c"; context.fillRect(0, 0, size, size); context.drawImage(image, sx, sy, s, s, 0, 0, size, size); + // paint the pointer at the interpolated cursor position + const scale = image.width / options.viewport.w; + const cursor = cursorAt(options.track, frame.at - options.startedAt, options.viewport); + drawCursorMarker( + context, + (cursor.x * scale - sx) * (size / s), + (cursor.y * scale - sy) * (size / s), + Math.max(12, Math.round(size * 0.034)) + ); await encodeFrame(encoder, context, size, size, delayMs); } finally { image.close(); @@ -168,4 +177,42 @@ point = candidate; } return point; +} + +/** + * Paints a pointer arrow (white fill, dark outline) with its tip hotspot at + * (x, y). CDP screencast frames don't include the OS cursor, so the GIF and + * WebM encoders draw this marker at the interpolated cursor position. + */ +export function drawCursorMarker( + context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, + x: number, + y: number, + size: number +): void { + // classic arrow silhouette in a 10x17 box, tip = the cursor hotspot + const outline: [number, number][] = [ + [0, 0], + [0, 13.4], + [3.2, 10.3], + [5.3, 14.7], + [7.1, 13.9], + [4.9, 9.6], + [8.9, 9.6], + ]; + const k = size / 17; + context.save(); + context.translate(x, y); + context.scale(k, k); + context.beginPath(); + context.moveTo(outline[0][0], outline[0][1]); + for (let i = 1; i < outline.length; i++) context.lineTo(outline[i][0], outline[i][1]); + context.closePath(); + context.fillStyle = "#ffffff"; + context.strokeStyle = "rgba(16, 18, 28, 0.9)"; + context.lineWidth = 1.2; + context.lineJoin = "round"; + context.fill(); + context.stroke(); + context.restore(); } \ No newline at end of file diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts index 861d40c..90bc105 100644 --- a/packages/extension/src/background/index.ts +++ b/packages/extension/src/background/index.ts @@ -506,7 +506,7 @@ await api.attach({ tabId }, "1.3"); await api.sendCommand({ tabId }, "Page.startScreencast", { format: "jpeg", - quality: 55, + quality: 70, maxWidth: VIDEO_MAX_DIMENSION, maxHeight: VIDEO_MAX_DIMENSION, everyNthFrame: 1, diff --git a/packages/extension/src/background/video.ts b/packages/extension/src/background/video.ts index 254dafa..0ba1472 100644 --- a/packages/extension/src/background/video.ts +++ b/packages/extension/src/background/video.ts @@ -8,7 +8,7 @@ * to the GIF path. */ import { Muxer, ArrayBufferTarget } from "webm-muxer"; -import { cursorCropRect, type CursorCropOptions } from "./gif"; +import { cursorAt, cursorCropRect, drawCursorMarker, type CursorCropOptions } from "./gif"; import type { VideoFrame } from "./gif"; /** VP8 needs even dimensions. */ @@ -56,13 +56,13 @@ failure.error = error instanceof Error ? error : new Error(String(error)); }, }); - // ~0.18 bits per pixel per frame keeps VP8 readable at 15fps (capped so - // huge squares don't explode the upload) + // ~0.3 bits per pixel per frame keeps VP8 sharp at 15fps (capped so huge + // squares don't explode the upload) encoder.configure({ codec: "vp8", width, height, - bitrate: Math.min(4_000_000, Math.max(1_000_000, Math.round(width * height * 15 * 0.18))), + bitrate: Math.min(8_000_000, Math.max(1_000_000, Math.round(width * height * 15 * 0.3))), }); const canvas = new OffscreenCanvas(width, height); @@ -90,6 +90,16 @@ context.fillStyle = "#10121c"; context.fillRect(0, 0, width, height); context.drawImage(image, sx, sy, s, s, 0, 0, width, height); + // the OS pointer isn't part of screencast frames — paint the marker + // at the interpolated cursor position (the crop centers on it) + const scale = image.width / crop.viewport.w; + const cursor = cursorAt(crop.track, frame.at - crop.startedAt, crop.viewport); + drawCursorMarker( + context, + (cursor.x * scale - sx) * (width / s), + (cursor.y * scale - sy) * (height / s), + Math.max(12, Math.round(width * 0.034)) + ); } else { context.drawImage(image, 0, 0, width, height); }