Capture simulator video and audio in sync
Browse files- app/record-sim.mjs +62 -19
app/record-sim.mjs
CHANGED
|
@@ -1,22 +1,65 @@
|
|
| 1 |
-
|
| 2 |
import fs from 'node:fs';
|
| 3 |
-
|
| 4 |
-
const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
|
| 5 |
-
const context=await browser.newContext({viewport:{width:1440,height:900}
|
| 6 |
-
const page=await context.newPage();
|
| 7 |
await page.goto('http://127.0.0.1:5175/?boot=1');
|
| 8 |
-
await page.waitForFunction(()=>window.__store?.getState().bootDone);
|
| 9 |
-
await page.evaluate(()=>window.__gameApi.startEntrance());
|
| 10 |
-
await page.waitForFunction(()=>window.rl&&!window.rl.inputLocked);
|
| 11 |
-
await page.waitForFunction(()=>window.__audioCaptureStream?.active);
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
});
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { chromium } from '@playwright/test';
|
| 2 |
import fs from 'node:fs';
|
| 3 |
+
|
| 4 |
+
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] });
|
| 5 |
+
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
| 6 |
+
const page = await context.newPage();
|
| 7 |
await page.goto('http://127.0.0.1:5175/?boot=1');
|
| 8 |
+
await page.waitForFunction(() => window.__store?.getState().bootDone);
|
| 9 |
+
await page.evaluate(() => window.__gameApi.startEntrance());
|
| 10 |
+
await page.waitForFunction(() => window.rl && !window.rl.inputLocked);
|
| 11 |
+
await page.waitForFunction(() => window.__audioCaptureStream?.active);
|
| 12 |
+
|
| 13 |
+
const script = JSON.parse(fs.readFileSync('public/scripts/reachy-microduck-conversation.json', 'utf8'));
|
| 14 |
+
await page.evaluate(() => {
|
| 15 |
+
const canvas = document.querySelector('canvas');
|
| 16 |
+
const videoStream = canvas.captureStream(30);
|
| 17 |
+
const tracks = [...videoStream.getVideoTracks(), ...window.__audioCaptureStream.getAudioTracks()];
|
| 18 |
+
const recorder = new MediaRecorder(new MediaStream(tracks), { mimeType: 'video/webm;codecs=vp8,opus' });
|
| 19 |
+
window.__filmChunks = [];
|
| 20 |
+
recorder.ondataavailable = (event) => { if (event.data.size) window.__filmChunks.push(event.data); };
|
| 21 |
+
recorder.start(250);
|
| 22 |
+
window.__filmRecorder = recorder;
|
| 23 |
});
|
| 24 |
+
await page.evaluate((s) => {
|
| 25 |
+
window.rl.chaseCam = false;
|
| 26 |
+
window.__gameApi.script.load(s);
|
| 27 |
+
window.__gameApi.script.run();
|
| 28 |
+
}, script);
|
| 29 |
+
|
| 30 |
+
await page.evaluate(async () => {
|
| 31 |
+
const start = performance.now();
|
| 32 |
+
await new Promise((resolve) => {
|
| 33 |
+
const frame = (now) => {
|
| 34 |
+
const t = (now - start) / 1000;
|
| 35 |
+
let x = 0, y = 0.82, z = 2.8;
|
| 36 |
+
if (t < 13) {
|
| 37 |
+
const a = t * 0.42; x = Math.sin(a) * 2.35; z = Math.cos(a) * 2.35;
|
| 38 |
+
} else if (t < 28) {
|
| 39 |
+
const a = (t - 13) * 0.5; x = 1.55 + Math.sin(a) * 0.35; z = 2.05 + Math.cos(a) * 0.25; y = 0.68;
|
| 40 |
+
} else {
|
| 41 |
+
const a = (t - 28) * 0.75; x = Math.sin(a) * 2.45; z = Math.cos(a) * 2.45; y = 0.88 + Math.sin(t * 0.5) * 0.08;
|
| 42 |
+
}
|
| 43 |
+
window.rl.camera.position.set(x, y, z);
|
| 44 |
+
window.rl.controls.target.set(0, 0.23, 0);
|
| 45 |
+
window.rl.controls.update();
|
| 46 |
+
if (t < 55) requestAnimationFrame(frame); else resolve();
|
| 47 |
+
};
|
| 48 |
+
requestAnimationFrame(frame);
|
| 49 |
+
});
|
| 50 |
+
});
|
| 51 |
+
await page.waitForTimeout(1200);
|
| 52 |
+
const filmB64 = await page.evaluate(() => new Promise((resolve) => {
|
| 53 |
+
const recorder = window.__filmRecorder;
|
| 54 |
+
recorder.onstop = () => {
|
| 55 |
+
const blob = new Blob(window.__filmChunks, { type: 'video/webm' });
|
| 56 |
+
const reader = new FileReader();
|
| 57 |
+
reader.onload = () => resolve(reader.result.split(',')[1]);
|
| 58 |
+
reader.readAsDataURL(blob);
|
| 59 |
+
};
|
| 60 |
+
recorder.stop();
|
| 61 |
+
}));
|
| 62 |
+
fs.writeFileSync('reachy-microduck-synced.webm', Buffer.from(filmB64, 'base64'));
|
| 63 |
+
await context.close();
|
| 64 |
+
await browser.close();
|
| 65 |
+
console.log('reachy-microduck-synced.webm');
|