Skip to content
Merged
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
8 changes: 8 additions & 0 deletions apps/player/scripts/phone-rec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# phone-rec.sh <url> <dir> : record the player app at phone size (Playwright in WSL, ~/phonerec).
# Drive it by appending JS lines to <dir>/cmd.txt; touch <dir>/stop to finish -> <dir>/phone.webm
set -e
mkdir -p "$2"; : > "$2/cmd.txt"; rm -f "$2/stop"
cp /mnt/d/RoomRiddle/apps/player/scripts/record-phone-pw.mjs ~/phonerec/
cd ~/phonerec
exec node record-phone-pw.mjs "$1" "$2"
44 changes: 44 additions & 0 deletions apps/player/scripts/record-phone-pw.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Dev tool (runs in WSL with Playwright): a recorded phone. Opens the player app
// at phone size and records a video. Each new line in <dir>/cmd.txt is
// JavaScript run in the page; touch <dir>/stop to finish.
// node record-phone-pw.mjs <url> <dir>
// Writes <dir>/phone.webm and <dir>/t0.txt (epoch ms when recording began).
import {chromium} from 'playwright';
import {existsSync, readFileSync, writeFileSync, renameSync, mkdirSync} from 'node:fs';

const [url, dir] = process.argv.slice(2);
mkdirSync(dir, {recursive: true});
const sleep = ms => new Promise(r => setTimeout(r, ms));
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: {width: 390, height: 844},
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
recordVideo: {dir, size: {width: 780, height: 1688}},
});
const page = await context.newPage();
const t0 = Date.now();
writeFileSync(`${dir}/t0.txt`, String(t0));
await page.goto(url);
console.log(`[phone] recording ${url} (t0=${t0})`);
const control = `${dir}/cmd.txt`;
if (!existsSync(control)) writeFileSync(control, '');
let done = 0;
while (!existsSync(`${dir}/stop`)) {
const lines = readFileSync(control, 'utf8').split('\n').filter(Boolean);
for (; done < lines.length; done++) {
try {
const v = await page.evaluate(lines[done]);
console.log(`[phone] ${Date.now() - t0}ms ${lines[done].slice(0, 70)} -> ${JSON.stringify(v ?? null).slice(0, 120)}`);
} catch (e) {
console.log(`[phone] ${Date.now() - t0}ms ERROR ${String(e).slice(0, 160)}`);
}
}
await sleep(150);
}
const video = page.video();
await context.close();
renameSync(await video.path(), `${dir}/phone.webm`);
await browser.close();
console.log('[phone] saved phone.webm');
41 changes: 41 additions & 0 deletions apps/server/scripts/demo-bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Dev tool for recording demos: one scripted phone ("Kabir") that joins a room
* and then performs whatever lines are appended to a control file:
* DISCOVER_CLUE <clueId> | SUBMIT_ANSWER <puzzleId> <answer> | REQUEST_HINT <puzzleId> | ADVANCE_SCENE
* npx tsx apps/server/scripts/demo-bot.ts <roomCode> <controlFile> [name] [wsUrl]
*/
import {existsSync, readFileSync, writeFileSync} from 'node:fs';
import {WebSocket} from 'ws';
import {RoomClient, initialRoomView, memorySessionStore, reduceRoomView, type RoomView, type SocketLike} from '@roomriddle/client';
import type {ClientMessage} from '@roomriddle/shared-types';

const [code, control, name = 'Kabir', url = 'ws://127.0.0.1:4000/ws'] = process.argv.slice(2);
if (!code || !control) throw new Error('usage: demo-bot.ts <roomCode> <controlFile> [name] [wsUrl]');
let view: RoomView = initialRoomView;
const client = new RoomClient({url, store: memorySessionStore(), createSocket: u => new WebSocket(u) as unknown as SocketLike, backoff: [200]});
client.onMessage(m => {
view = reduceRoomView(view, m);
if (m.type === 'ERROR') console.log(`[${name}] ERROR ${m.code}: ${m.message}`);
});
client.send({type: 'JOIN_ROOM', roomCode: code, playerName: name});
if (!existsSync(control)) writeFileSync(control, '');
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
let done = 0;
console.log(`[${name}] joining ${code}; control ${control}`);
for (;;) {
const lines = readFileSync(control, 'utf8').split('\n').map(l => l.trim()).filter(Boolean);
for (; done < lines.length; done++) {
const [type, a = '', b = ''] = lines[done]!.split(/\s+/);
if (type === 'QUIT') process.exit(0);
let message: ClientMessage | null = null;
if (type === 'DISCOVER_CLUE') message = {type, clueId: a};
else if (type === 'SUBMIT_ANSWER') message = {type, puzzleId: a, answer: b};
else if (type === 'REQUEST_HINT') message = {type, puzzleId: a};
else if (type === 'ADVANCE_SCENE') message = {type};
if (message) {
client.send(message);
console.log(`[${name}] ${lines[done]} (scene ${view.room?.gameState?.currentSceneId ?? '-'})`);
}
}
await sleep(150);
}
33 changes: 33 additions & 0 deletions apps/server/scripts/voiceover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Voice-over for the demo video: one Edge TTS clip per segment, placed at its start time.
// Usage: npx tsx apps/server/scripts/voiceover.ts <outDir>
import {mkdirSync, writeFileSync} from 'node:fs';
import {MsEdgeTTS, OUTPUT_FORMAT} from 'msedge-tts';

const SEGMENTS: {at: number; end: number; text: string}[] = [
{at: 0, end: 15, text: "Friday night. Everyone's on the couch, and everyone has a phone. Room Riddle turns the biggest screen in the house into a crime scene, and every phone into a detective's notebook."},
{at: 15, end: 35, text: 'There are five complete cases: a noir train mystery, a gothic manor, a tech thriller, a family temple adventure, and an all-ages riddle story. You can filter by age group or story type, so the whole family finds the right case in seconds.'},
{at: 35, end: 55, text: "One press on Quick Start opens a room. Phones scan the QR code. No app, no account. Up to six investigators, and you're in."},
{at: 55, end: 85, text: 'This is The Last Train. Midnight, platform two, and the conductor has vanished. You explore the painted scene with the Fire TV remote. Every clue has its own close-up, and every line is spoken aloud, with subtitles. The platform clock says eleven fifty-nine. Remember that.'},
{at: 85, end: 115, text: 'Now the best part. Pick a suspect on your phone, and ask anything in your own words. Inspector Voss answers on the TV, in character, and in his own voice. He can dodge, and he can lie, but he can only talk about what your group has already discovered. A guarded AI on free models, so he can never spoil the ending.'},
{at: 115, end: 140, text: 'Clues lead to puzzles. Anyone can answer from their phone, hints cost points, and the server checks every answer, so nobody can cheat. Everything you find is pinned in the case journal, on the TV, and on every phone.'},
{at: 140, end: 155, text: "When you're ready, the group names the truth together, and the case closes the way you solved it."},
{at: 155, end: 165, text: 'Room Riddle. Built with React Native for Vega, on Fire TV. Gather. Investigate. Unravel the mystery.'},
];

const out = process.argv[2] ?? 'voiceover';
mkdirSync(out, {recursive: true});
const tts = new MsEdgeTTS();
await tts.setMetadata('en-GB-RyanNeural', OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
for (const [i, s] of SEGMENTS.entries()) {
const {audioStream} = tts.toStream(s.text, {rate: '-2%'});
const chunks: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
audioStream.on('data', (c: Buffer) => chunks.push(c));
audioStream.on('end', () => resolve());
audioStream.on('error', reject);
});
writeFileSync(`${out}/seg${i}.mp3`, Buffer.concat(chunks));
console.log(`seg${i} at ${s.at}s (window ${s.end - s.at}s) ${Buffer.concat(chunks).length} B`);
}
writeFileSync(`${out}/segments.json`, JSON.stringify(SEGMENTS, null, 1));
tts.close();
48 changes: 48 additions & 0 deletions apps/tv/scripts/make-demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# make-demo.sh: cut the recordings in /tmp/rec into the demo video, timed to the voice-over.
set -e
R=/tmp/rec; W=$R/edit; mkdir -p $W; cd $W
VO=/mnt/d/Corpus/RoomRiddle/Assets/recordings/vo/voiceover.m4a
OUT=/mnt/d/Corpus/RoomRiddle/Assets/recordings/room-riddle-demo.mp4
ENC="-c:v libx264 -preset medium -crf 19 -pix_fmt yuv420p -r 30 -c:a aac -ar 48000 -ac 2 -b:a 160k"

# clip <out> <src> <start> <dur> [speed] — TV shot, 1920x1080, game audio kept.
clip() {
local sp=${5:-1}
ffmpeg -v error -y -ss "$3" -t "$(python3 -c "print($4*$sp)")" -i "$2" \
-filter_complex "[0:v]setpts=PTS/$sp,scale=1920:1080,fps=30[v];[0:a]atempo=$sp,aresample=48000[a]" \
-map "[v]" -map "[a]" -t "$4" $ENC "$1"
}
# withphone <out> <tvsrc> <start> <dur> <phonesrc> <phonestart> — TV and phone side by side (nothing covered).
withphone() {
ffmpeg -v error -y -ss "$3" -t "$4" -i "$2" -ss "$6" -t "$4" -i "$5" -filter_complex \
"[0:v]scale=1408:792,fps=30[tv];[1:v]crop=390:844:0:0,scale=-2:1000,fps=30,pad=iw+8:ih+8:4:4:color=0xC9A45C[ph];color=c=0x0b0d12:s=1920x1080:r=30[bg];[bg][tv]overlay=16:(H-h)/2:shortest=1[b1];[b1][ph]overlay=W-w-16:(H-h)/2[v];[0:a]aresample=48000[a]" \
-map "[v]" -map "[a]" -t "$4" $ENC "$1"
}

clip s0.mp4 $R/tvA.webm 0 15 0.7333
clip s1a.mp4 $R/tvA.webm 16 10
clip s1b.mp4 $R/tvA.webm 34 10
clip s2a.mp4 $R/tvA.webm 49 10
withphone s2b.mp4 $R/tvB.webm 24 10 $R/phone/phone.webm 24.7
clip s3a.mp4 $R/tvB.webm 72 15
clip s3b.mp4 $R/tvB.webm 122 15
withphone s4.mp4 $R/tvC.webm 0 30 $R/phone/phone.webm 190.1
withphone s5a.mp4 $R/tvC.webm 96 12 $R/phone/phone.webm 286.1
withphone s5b.mp4 $R/tvE.webm 68 13 $R/phone2/phone.webm 110.5
clip s6a.mp4 $R/tvD.webm 42 6
clip s6b.mp4 $R/tvD.webm 53 9

# Title card: the README banner, with the repo link, silent audio, gentle fade.
ffmpeg -v error -y -loop 1 -t 13 -i /mnt/d/RoomRiddle/docs/images/banner.jpg -f lavfi -t 13 -i anullsrc=r=48000:cl=stereo -filter_complex \
"[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=0x0b0d12,drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:text='github.com/perhaps-aksh/RoomRiddle':fontcolor=0xE8D9B0:fontsize=40:x=(w-tw)/2:y=h-120,fade=t=in:st=0:d=0.8,fade=t=out:st=12:d=1,fps=30[v]" \
-map "[v]" -map 1:a $ENC s7.mp4

printf "file '%s'\n" s0.mp4 s1a.mp4 s1b.mp4 s2a.mp4 s2b.mp4 s3a.mp4 s3b.mp4 s4.mp4 s5a.mp4 s5b.mp4 s6a.mp4 s6b.mp4 s7.mp4 > list.txt
ffmpeg -v error -y -f concat -safe 0 -i list.txt -c copy body.mp4
# Game audio under the voice-over (ducked), voice-over on top.
ffmpeg -v error -y -i body.mp4 -i "$VO" -filter_complex \
"[0:a]volume=0.35[g];[1:a]volume=1.6[vo];[g][vo]amix=inputs=2:duration=first:normalize=0,alimiter=limit=0.95[a]" \
-map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k -movflags +faststart "$OUT"
for f in s*.mp4; do printf '%s %s\n' "$f" "$(ffprobe -v error -show_entries format=duration -of csv=p=0 $f)"; done
echo "OUT $(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUT") $OUT"
6 changes: 6 additions & 0 deletions apps/tv/scripts/rec-info.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# rec-info.sh: durations and start times of the demo recordings in /tmp/rec
cd /tmp/rec
for f in tvA tvB tvC tvD; do echo "$f start=$(cat $f.start) dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 $f.webm)"; done
echo "phone t0=$(cat phone/t0.txt) dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 phone/phone.webm)"
ls -la phone
12 changes: 12 additions & 0 deletions apps/tv/scripts/sheet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# sheet.sh <video> <out.jpg> t1 t2 ... : a contact sheet (4 per row, 480 px wide each) of frames at the given seconds.
V="$1"; OUT="$2"; shift 2
D=$(mktemp -d); i=0; ins=(); lay=()
for t in "$@"; do
ffmpeg -v error -y -ss "$t" -i "$V" -frames:v 1 -vf "scale=480:270:force_original_aspect_ratio=decrease,pad=480:270,drawtext=text='$t':x=6:y=6:fontcolor=magenta:fontsize=22" "$D/$i.jpg" 2>/dev/null \
|| ffmpeg -v error -y -ss "$t" -i "$V" -frames:v 1 -vf "scale=480:270:force_original_aspect_ratio=decrease,pad=480:270" "$D/$i.jpg"
ins+=(-i "$D/$i.jpg"); lay+=("$(( (i % 4) * 480 ))_$(( (i / 4) * 270 ))"); i=$((i+1))
done
L=$(IFS='|'; echo "${lay[*]}")
ffmpeg -v error -y "${ins[@]}" -filter_complex "xstack=inputs=$i:layout=$L:fill=black" "$OUT"
rm -rf "$D"; echo "sheet $OUT ($i frames)"
8 changes: 8 additions & 0 deletions apps/tv/scripts/vvd-keys.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# vvd-keys.sh KEY_... [N secs] ... : press remote keys on the VVD (a bare number = sleep N s), no screenshot.
source ~/vega/env >/dev/null 2>&1
CMDS=""
for k in "$@"; do
if [[ "$k" =~ ^[0-9.]+$ ]]; then CMDS+="sleep $k; "; else CMDS+="inputd-cli button_press $k >/dev/null 2>&1; sleep 0.5; "; fi
done
printf '%s\nexit\n' "$CMDS echo KEYS_DONE" | timeout 280 vega device shell -d VirtualDevice 2>&1 | grep -E "KEYS_DONE|rror" | tail -1
20 changes: 20 additions & 0 deletions apps/tv/scripts/vvd-screenrec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# vvd-screenrec.sh start <file.webm> [limitSecs] | stop — record the VVD screen through the emulator console (port 5554).
# The file is written by the emulator on the WSL side (use a /tmp or /mnt path).
CMD="$1"; FILE="$2"; LIMIT="${3:-180}"
python3 - "$CMD" "$FILE" "$LIMIT" <<'PY'
import socket, os, sys, time
cmd, f, limit = sys.argv[1], sys.argv[2], sys.argv[3]
tok = open(os.path.expanduser('~/.emulator_console_auth_token')).read().strip()
s = socket.create_connection(('127.0.0.1', 5554), timeout=20)
line = f'screenrecord start --time-limit {limit} --fps 24 {f}' if cmd == 'start' else 'screenrecord stop'
out = b''
for c in (f'auth {tok}', line, 'quit'):
time.sleep(0.4); s.sendall((c + '\n').encode())
time.sleep(1.0)
try:
s.settimeout(1); out = s.recv(65536)
except Exception:
pass
print(out.decode(errors='replace').strip().splitlines()[-3:])
PY
Loading