Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"singleQuote": true,
"bracketSpacing": false,
"useTabs": true,
"printWidth": 80,
"printWidth": 50,
"sortImports": {"newlinesBetween": false},
"sortPackageJson": false,
"ignorePatterns": [
Expand Down
127 changes: 91 additions & 36 deletions packages/canvas-capture/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import {
ALL_FORMATS,
BufferSource,
BufferTarget,
CanvasSource,
Conversion,
Input,
Output,
WebMOutputFormat,
} from 'mediabunny';
import React, {
forwardRef,
useCallback,
Expand Down Expand Up @@ -53,6 +63,11 @@ type MouseMovement = {
readonly cursor: string;
};

type PointerClick = {
readonly timeInSeconds: number;
readonly type: 'pointer-down' | 'pointer-up';
};

type CaptureMetadata = {
readonly density: number;
readonly contentRect: {
Expand All @@ -79,6 +94,7 @@ type RecordingState = {
readonly source: CanvasVideoSource;
readonly startedAt: number;
readonly mouseMovements: MouseMovement[];
readonly pointerClicks: PointerClick[];
lastTimestampInSeconds: number | null;
lastFramePromise: Promise<void>;
frameCount: number;
Expand All @@ -94,13 +110,13 @@ export type HtmlInCanvasCaptureHandle = {

type HtmlInCanvasCaptureProps = {
readonly children: React.ReactNode;
readonly density: number;
readonly filename: string;
readonly density: number;
};

type WithHtmlInCanvasCaptureProps = {
readonly density: number;
readonly filename: string;
readonly density: number;
};

const canvasStyle: React.CSSProperties = {
Expand Down Expand Up @@ -201,9 +217,9 @@ const downloadBlob = (blob: Blob, filename: string) => {
URL.revokeObjectURL(url);
};

const getJsonFilename = (filename: string) => {
return filename.replace(/\.[^.]+$/, '') + '.json';
};
const CAPTURE_METADATA_TAG_KEY = 'REMOTION_CAPTURE_DATA';

export {CAPTURE_METADATA_TAG_KEY};

const logCaptureError = (message: string, err: unknown) => {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -254,38 +270,44 @@ const finalizeRecording = async (
throw new Error('Mediabunny did not return an output buffer.');
}

downloadBlob(
new Blob([recording.target.buffer], {type: 'video/webm'}),
filename,
);
downloadBlob(
new Blob(
[
JSON.stringify(
{
startedAt: recording.startedAt,
endedAt: performance.now(),
captureMetadata: recording.captureMetadata,
mouseMovements: recording.mouseMovements,
},
null,
2,
),
],
{type: 'application/json'},
),
getJsonFilename(filename),
);
const captureData = JSON.stringify({
startedAt: recording.startedAt,
endedAt: performance.now(),
captureMetadata: recording.captureMetadata,
mouseMovements: recording.mouseMovements,
pointerClicks: recording.pointerClicks,
});

const remuxInput = new Input({
formats: ALL_FORMATS,
source: new BufferSource(recording.target.buffer),
});
const remuxTarget = new BufferTarget();
const remuxOutput = new Output({
format: new WebMOutputFormat(),
target: remuxTarget,
});
const conversion = await Conversion.init({
input: remuxInput,
output: remuxOutput,
tags: {
raw: {[CAPTURE_METADATA_TAG_KEY]: captureData},
},
showWarnings: false,
});
await conversion.execute();

if (!remuxTarget.buffer) {
throw new Error('Mediabunny remux did not return an output buffer.');
}

downloadBlob(new Blob([remuxTarget.buffer], {type: 'video/webm'}), filename);
};

export const HtmlInCanvasCapture = forwardRef<
HtmlInCanvasCaptureHandle,
HtmlInCanvasCaptureProps
>(({children, density, filename}, ref) => {
if (!Number.isFinite(density) || density <= 0) {
throw new Error('HTML-in-canvas capture density must be greater than 0.');
}

>(({children, filename, density}, ref) => {
const isSupported = useMemo(() => isHtmlInCanvasAvailable(), []);
const canvasRef = useRef<HtmlInCanvasElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -307,8 +329,6 @@ export const HtmlInCanvasCapture = forwardRef<
return;
}

const {BufferTarget, CanvasSource, Output, WebMOutputFormat} =
await import('mediabunny');
const target = new BufferTarget();
const output = new Output({
format: new WebMOutputFormat(),
Expand All @@ -330,6 +350,7 @@ export const HtmlInCanvasCapture = forwardRef<
source,
startedAt: performance.now(),
mouseMovements: [],
pointerClicks: [],
lastTimestampInSeconds: null,
lastFramePromise: Promise.resolve(),
frameCount: 0,
Expand Down Expand Up @@ -461,6 +482,40 @@ export const HtmlInCanvasCapture = forwardRef<
};
}, [density]);

useEffect(() => {
const onPointerDown = () => {
const recording = recordingRef.current;
if (!recording || recording.isFinalizing) {
return;
}

recording.pointerClicks.push({
timeInSeconds: (performance.now() - recording.startedAt) / 1000,
type: 'pointer-down',
});
};

const onPointerUp = () => {
const recording = recordingRef.current;
if (!recording || recording.isFinalizing) {
return;
}

recording.pointerClicks.push({
timeInSeconds: (performance.now() - recording.startedAt) / 1000,
type: 'pointer-up',
});
};

window.addEventListener('pointerdown', onPointerDown, true);
window.addEventListener('pointerup', onPointerUp, true);

return () => {
window.removeEventListener('pointerdown', onPointerDown, true);
window.removeEventListener('pointerup', onPointerUp, true);
};
}, []);

useEffect(() => {
if (!isSupported) {
return;
Expand Down Expand Up @@ -549,9 +604,9 @@ export const withHtmlInCanvasCapture = <Props extends object>(
return forwardRef<
HtmlInCanvasCaptureHandle,
Props & WithHtmlInCanvasCaptureProps
>(({density, filename, ...props}, ref) => {
>(({filename, density, ...props}, ref) => {
return (
<HtmlInCanvasCapture ref={ref} density={density} filename={filename}>
<HtmlInCanvasCapture ref={ref} filename={filename} density={density}>
<Component {...(props as Props)} />
</HtmlInCanvasCapture>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/example/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 50
}
Binary file added packages/example/public/arrow-around.webm
Binary file not shown.
Binary file added packages/example/public/canvas-all-improved.webm
Binary file not shown.
Binary file added packages/example/public/canvas-all-v3.webm
Binary file not shown.
Binary file added packages/example/public/canvas-capture-cursor.mov
Binary file not shown.
Binary file not shown.
Loading
Loading