Commit c1a1af60 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Pre-reset backup: promo videos, test scripts, screenshots

parent 1a19a25c
import puppeteer from 'puppeteer';
import { mkdir } from 'fs/promises';
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com';
const AUTH_URL = 'https://safe-supabase-kong.caprover.al-arcade.com/auth/v1';
const API_URL = `${BASE_URL}/api`;
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84';
const PLAYER1 = { email: 'testplayer1@el3ab.com', password: 'ChessTest1!', id: 'd14a27c6-2448-4319-a775-a9f94dd6de87', name: 'testplayer1', displayName: 'لاعب اختبار' };
const PLAYER2 = { email: 'test@el3ab.com', password: 'ChessTest2!', id: '37f947c0-e10b-4bef-88ec-c0aa0e8b8034', name: 'TestPlayer', displayName: 'TestPlayer' };
const wait = ms => new Promise(r => setTimeout(r, ms));
const SHOT_DIR = '/Users/mahmoudaglan/el3ab-player/screenshots/chess-test';
async function getToken(email, password) {
const res = await fetch(`${AUTH_URL}/token?grant_type=password`, {
method: 'POST',
headers: { 'apikey': ANON_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!data.access_token) throw new Error(`Login failed: ${JSON.stringify(data)}`);
return { token: data.access_token, refreshToken: data.refresh_token };
}
async function setupPage(browser, player, auth) {
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
page.on('console', msg => {
if (msg.type() === 'error' && !msg.text().includes('favicon'))
console.log(` [${player.name} ERR] ${msg.text().slice(0, 120)}`);
});
await page.goto(BASE_URL, { waitUntil: 'networkidle2', timeout: 30000 });
await page.evaluate((token, refreshToken, userId, displayName, username) => {
localStorage.setItem('el3ab_state', JSON.stringify({
auth: { token, refreshToken, userId },
player: { id: userId, display_name: displayName, username, level: 5, coins: 1000, xp: 500 },
activeWorld: 'play', audioEnabled: false, language: 'ar'
}));
}, auth.token, auth.refreshToken, player.id, player.displayName, player.name);
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await wait(2000);
return page;
}
async function navigateToQueue(page, playerName) {
// 1. Click chess tile
console.log(` [${playerName}] Clicking chess tile...`);
await page.evaluate(() => document.querySelector('[data-game="chess"]').click());
await wait(1000);
// 2. Click "أونلاين" (#btn-multi) in the popup menu
console.log(` [${playerName}] Clicking أونلاين...`);
await page.evaluate(() => document.querySelector('#btn-multi').click());
await wait(1000);
// 3. Now on time-select — click a time control to start search
console.log(` [${playerName}] Clicking time control...`);
const clicked = await page.evaluate(() => {
// Look for time control buttons in the scene
const allEls = document.querySelectorAll('button, [class*="btn"], [class*="time"], [role="button"]');
for (const el of allEls) {
const t = el.textContent.trim();
if (t.includes('10+0') || t.includes('10 + 0') || t.includes('سريعة') || t.includes('Rapid') || t.includes('10')) {
el.click(); return t;
}
}
// Fallback: look for any button in the current scene
const scene = document.querySelector('.scene');
if (scene) {
const btns = scene.querySelectorAll('button, [style*="cursor: pointer"]');
for (const btn of btns) {
if (btn.id !== 'menu-close') { btn.click(); return btn.textContent.slice(0,30); }
}
}
return null;
});
console.log(` [${playerName}] Clicked: "${clicked}"`);
await wait(500);
}
async function run() {
await mkdir(SHOT_DIR, { recursive: true });
console.log('=== Chess Multiplayer UI Test ===\n');
console.log('1. Authenticating...');
const p1Auth = await getToken(PLAYER1.email, PLAYER1.password);
const p2Auth = await getToken(PLAYER2.email, PLAYER2.password);
console.log(' ✓ Both authenticated');
console.log('\n2. Launching browser...');
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors']
});
const page1 = await setupPage(browser, PLAYER1, p1Auth);
const page2 = await setupPage(browser, PLAYER2, p2Auth);
console.log(' ✓ Both pages loaded');
// Navigate Player 1 to queue
console.log('\n3. Player 1 navigates to matchmaking...');
await navigateToQueue(page1, PLAYER1.name);
await page1.screenshot({ path: `${SHOT_DIR}/01-p1-queue.png` });
// Navigate Player 2 to queue
console.log('\n4. Player 2 navigates to matchmaking...');
await navigateToQueue(page2, PLAYER2.name);
await page2.screenshot({ path: `${SHOT_DIR}/02-p2-queue.png` });
// Wait for match — polling happens every 3s
console.log('\n5. Waiting for match (8s)...');
await wait(8000);
await page1.screenshot({ path: `${SHOT_DIR}/03-p1-matched.png` });
await page2.screenshot({ path: `${SHOT_DIR}/03-p2-matched.png` });
// Check if chess board appeared
const p1Board = await page1.$('canvas');
const p2Board = await page2.$('canvas');
console.log(` P1 has chess board: ${!!p1Board}`);
console.log(` P2 has chess board: ${!!p2Board}`);
if (p1Board && p2Board) {
console.log(' ✓ BOTH PLAYERS IN GAME!');
await wait(2000);
await page1.screenshot({ path: `${SHOT_DIR}/04-p1-board.png` });
await page2.screenshot({ path: `${SHOT_DIR}/04-p2-board.png` });
// Verify they're not playing against themselves — check opponent name
const p1OpponentName = await page1.evaluate(() => {
const el = document.querySelector('#opponent-name');
return el ? el.textContent : 'not found';
});
const p2OpponentName = await page2.evaluate(() => {
const el = document.querySelector('#opponent-name');
return el ? el.textContent : 'not found';
});
console.log(` P1 opponent: "${p1OpponentName}"`);
console.log(` P2 opponent: "${p2OpponentName}"`);
// Wait for syncing to happen (poll cycle)
await wait(5000);
await page1.screenshot({ path: `${SHOT_DIR}/05-p1-playing.png` });
await page2.screenshot({ path: `${SHOT_DIR}/05-p2-playing.png` });
} else {
// Debug: what page are they on?
const p1Text = await page1.evaluate(() => document.body.innerText.slice(0, 200));
const p2Text = await page2.evaluate(() => document.body.innerText.slice(0, 200));
console.log(` P1 page: ${p1Text.slice(0, 100)}`);
console.log(` P2 page: ${p2Text.slice(0, 100)}`);
}
await browser.close();
console.log(`\n Screenshots saved to: ${SHOT_DIR}/`);
console.log('\n=== Done ===');
}
run().catch(err => {
console.error('\n✗ FATAL ERROR:', err.message);
process.exit(1);
});
This diff is collapsed.
import puppeteer from 'puppeteer';
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com';
const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkMTRhMjdjNi0yNDQ4LTQzMTktYTc3NS1hOWY5NGRkNmRlODciLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzgwNzMzOTIxLCJpYXQiOjE3ODA3MzAzMjEsImVtYWlsIjoidGVzdHBsYXllcjFAZWwzYWIuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6eyJkaXNwbGF5X25hbWUiOiLZhNin2LnYqCDYp9iu2KrYqNin2LEiLCJlbWFpbCI6InRlc3RwbGF5ZXIxQGVsM2FiLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInN1YiI6ImQxNGEyN2M2LTI0NDgtNDMxOS1hNzc1LWE5Zjk0ZGQ2ZGU4NyIsInVzZXJuYW1lIjoidGVzdHBsYXllcjEifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc4MDczMDMyMX1dLCJzZXNzaW9uX2lkIjoiZjcxZDRmZDktOWU3YS00ZTk1LWIwNDItNzM2YmQ5MjIxODYyIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.xJmblxW4Kgf0vBZlDV6K8AOyolAOuijgMmz9z8De-58';
const USER_ID = 'd14a27c6-2448-4319-a775-a9f94dd6de87';
const wait = ms => new Promise(r => setTimeout(r, ms));
async function run() {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors']
});
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', err => errors.push(`PAGE_ERROR: ${err.message}`));
console.log('1. Loading app...');
await page.goto(BASE_URL, { waitUntil: 'networkidle2', timeout: 30000 });
console.log('2. Injecting auth...');
await page.evaluate((token, userId) => {
localStorage.setItem('el3ab_state', JSON.stringify({
auth: { token, refreshToken: '7emeg6xkuddg', userId },
player: { id: userId, display_name: 'لاعب اختبار', username: 'testplayer1', level: 5, coins: 1000, xp: 500 },
activeWorld: 'play', audioEnabled: false, language: 'ar'
}));
}, TOKEN, USER_ID);
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await wait(2000);
console.log('3. Logged in');
// Click domino game card
console.log('4. Clicking domino card...');
await page.click('[data-game="domino"]');
await wait(1000);
// Click "لاعب واحد" (single player button in popup)
console.log('5. Clicking #btn-single...');
const singleClicked = await page.evaluate(() => {
const btn = document.querySelector('#btn-single');
if (btn) { btn.click(); return 'clicked'; }
return 'not found';
});
console.log(' Result:', singleClicked);
await wait(1500);
// Check for bot picker or game
const state1 = await page.evaluate(() => ({
text: document.body.innerText.slice(0, 300),
hasBotPicker: document.body.innerText.includes('خبير'),
hasGame: !!document.querySelector('#domino-wrap')
}));
console.log('6. After single:', state1.hasBotPicker ? 'BOT PICKER' : state1.hasGame ? 'GAME' : 'OTHER');
console.log(' Text:', state1.text.slice(0, 150));
await page.screenshot({ path: '/tmp/domino-step6.png' });
// Click intermediate
if (state1.hasBotPicker) {
console.log('7. Clicking intermediate...');
await page.evaluate(() => {
const btns = [...document.querySelectorAll('[data-level], button')];
const mid = btns.find(b => b.dataset?.level === 'intermediate' || b.textContent?.includes('متوسط'));
if (mid) mid.click();
});
await wait(3000);
}
await page.screenshot({ path: '/tmp/domino-step7.png' });
// ===== MAIN GAME STATE CHECK =====
const gs = await page.evaluate(() => {
const wrap = document.querySelector('#domino-wrap');
const canvas = document.querySelector('canvas');
const hand = document.querySelector('#domino-hand-area');
const board = document.querySelector('#domino-board');
const drawBtn = document.querySelector('#btn-draw');
const turnEl = document.querySelector('#turn-status');
const boneEl = document.querySelector('#boneyard-count');
const oppEl = document.querySelector('#opp-count');
const scoreEl = document.querySelector('#my-score');
let handChildren = [];
if (hand) {
handChildren = [...hand.children].map(c => ({
tag: c.tagName,
class: c.className,
style: c.getAttribute('style')?.slice(0, 80),
dataId: c.dataset?.tileId || c.getAttribute('data-tile-id') || '',
childCount: c.children.length
}));
}
return {
hasWrap: !!wrap,
wrapChildren: wrap ? wrap.children.length : 0,
hasCanvas: !!canvas,
canvasW: canvas?.width, canvasH: canvas?.height,
hasBoard: !!board,
boardChildren: board ? board.children.length : 0,
hasHand: !!hand,
handChildCount: hand ? hand.children.length : 0,
handChildren: handChildren.slice(0, 10),
hasDrawBtn: !!drawBtn,
turnText: turnEl?.textContent,
boneText: boneEl?.textContent,
oppText: oppEl?.textContent,
scoreText: scoreEl?.textContent,
fullHTML: document.querySelector('#domino-wrap')?.innerHTML?.slice(0, 800) || document.body.innerHTML.slice(0, 800)
};
});
console.log('\n======= GAME STATE =======');
console.log('Has #domino-wrap:', gs.hasWrap, '| children:', gs.wrapChildren);
console.log('Has canvas:', gs.hasCanvas, gs.canvasW + 'x' + gs.canvasH);
console.log('Has #domino-board:', gs.hasBoard, '| children:', gs.boardChildren);
console.log('Has #domino-hand-area:', gs.hasHand, '| children:', gs.handChildCount);
console.log('Hand children:', JSON.stringify(gs.handChildren, null, 2));
console.log('Has #btn-draw:', gs.hasDrawBtn);
console.log('Turn:', gs.turnText);
console.log('Boneyard:', gs.boneText);
console.log('Opponent:', gs.oppText);
console.log('Score:', gs.scoreText);
if (!gs.hasWrap) {
console.log('\nFull HTML preview:', gs.fullHTML.slice(0, 500));
}
// ===== TILE INTERACTION TEST =====
if (gs.hasHand && gs.handChildCount > 0) {
console.log('\n======= INTERACTION TEST =======');
// Get first tile position
const tileInfo = await page.evaluate(() => {
const hand = document.querySelector('#domino-hand-area');
if (!hand || !hand.children.length) return null;
const firstTile = hand.children[0];
const rect = firstTile.getBoundingClientRect();
return {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
w: rect.width, h: rect.height,
text: firstTile.textContent?.slice(0, 20),
style: firstTile.getAttribute('style')?.slice(0, 80)
};
});
console.log('First tile:', JSON.stringify(tileInfo));
if (tileInfo && tileInfo.w > 0) {
// TAP test
console.log('\nA) TAP on tile...');
await page.mouse.click(tileInfo.x, tileInfo.y);
await wait(500);
const afterTap = await page.evaluate(() => ({
turnText: document.querySelector('#turn-status')?.textContent,
handHTML: document.querySelector('#domino-hand-area')?.innerHTML?.slice(0, 200),
canvasChanged: document.querySelector('canvas')?.toDataURL()?.length
}));
console.log(' After tap - turn:', afterTap.turnText);
await page.screenshot({ path: '/tmp/domino-tap.png' });
// DRAG test
console.log('\nB) DRAG tile to board...');
const boardPos = await page.evaluate(() => {
const b = document.querySelector('#domino-board');
if (!b) return null;
const r = b.getBoundingClientRect();
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
});
if (boardPos) {
await page.mouse.move(tileInfo.x, tileInfo.y);
await page.mouse.down();
// Move in steps
const steps = 15;
for (let i = 1; i <= steps; i++) {
const p = i / steps;
await page.mouse.move(
tileInfo.x + (boardPos.x - tileInfo.x) * p,
tileInfo.y + (boardPos.y - tileInfo.y) * p
);
await wait(20);
}
await wait(200);
// Check for proxy/ghost
const duringDrag = await page.evaluate(() => {
const proxy = document.querySelector('.drag-proxy, .domino-drag-proxy, [style*="position: fixed"]');
const ghost = document.querySelector('[style*="opacity: 0.5"], .ghost');
return { hasProxy: !!proxy, hasGhost: !!ghost };
});
console.log(' During drag:', duringDrag);
await page.screenshot({ path: '/tmp/domino-drag.png' });
await page.mouse.up();
await wait(1000);
await page.screenshot({ path: '/tmp/domino-afterdrag.png' });
}
}
}
// Errors
if (errors.length > 0) {
console.log('\n======= JS ERRORS =======');
errors.slice(0, 20).forEach(e => console.log(' ✗', e.slice(0, 300)));
} else {
console.log('\n✓ No JS errors');
}
await browser.close();
}
run().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
This diff is collapsed.
import puppeteer from 'puppeteer';
import { execSync } from 'child_process';
import { mkdirSync, rmSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FRAMES_DIR = path.join(__dirname, 'frames-16x9');
const OUTPUT = path.join(__dirname, 'promo-16x9.mp4');
const HTML_PATH = path.join(__dirname, 'video-16x9.html');
const FPS = 30;
const DURATION = 120; // 2 minutes
const TOTAL_FRAMES = FPS * DURATION;
// 1920x1080 Full HD 16:9
const WIDTH = 1920;
const HEIGHT = 1080;
async function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function main() {
if (existsSync(FRAMES_DIR)) rmSync(FRAMES_DIR, { recursive: true });
mkdirSync(FRAMES_DIR, { recursive: true });
console.log(`Recording ${DURATION}s at ${FPS}fps → ${TOTAL_FRAMES} frames`);
console.log(`Resolution: ${WIDTH}×${HEIGHT}`);
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: WIDTH, height: HEIGHT, deviceScaleFactor: 1 });
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'networkidle0' });
await wait(500);
// Pause all animations and the rAF timeline
await page.evaluate(() => {
// Stop the rAF loop
window.__PAUSED = true;
document.getAnimations().forEach(a => a.pause());
});
// Inject a function to manually advance the timeline
await page.evaluate(() => {
window.advanceTo = function(ms) {
// Run timeline logic
const TIMELINE = [
[0, 's0', null],
[2000, 's1', 'white'],
[7000, 's2', 'white'],
[10000, 's3', 'gold'],
[14000, 's4', 'white'],
[20000, 's5', 'white'],
[25000, 's6', 'white'],
[33000, 's7', 'gold'],
[37000, 's8', 'white'],
[45000, 's9', 'gold'],
[49000, 's10', 'gold'],
[54000, 's11', 'white'],
[65000, 's12', 'white'],
[73000, 's13', 'white'],
[82000, 's14', 'white'],
[90000, 's15', 'gold'],
[97000, 's16', 'white'],
[108000, 's17', 'gold'],
];
let active = TIMELINE[0];
for (const entry of TIMELINE) {
if (ms >= entry[0]) active = entry;
else break;
}
// Show scene
document.querySelectorAll('.scene').forEach(s => s.classList.remove('active'));
const el = document.getElementById(active[1]);
if (el) el.classList.add('active');
// Progress bar
const prog = document.getElementById('prog');
if (prog) prog.style.width = ((ms / 120000) * 100) + '%';
// Advance CSS animations
document.getAnimations().forEach(a => {
a.currentTime = ms;
});
};
});
const msPerFrame = (DURATION * 1000) / TOTAL_FRAMES;
for (let i = 0; i < TOTAL_FRAMES; i++) {
const t = i * msPerFrame;
await page.evaluate((time) => {
window.advanceTo(time);
}, t);
await wait(10);
const frameNum = String(i).padStart(5, '0');
await page.screenshot({
path: path.join(FRAMES_DIR, `frame_${frameNum}.png`),
type: 'png'
});
if (i % 150 === 0) {
console.log(` Frame ${i}/${TOTAL_FRAMES} (${Math.round(t/1000)}s)`);
}
}
await browser.close();
console.log('Screenshots done. Encoding video...');
const ffmpegCmd = [
'ffmpeg', '-y',
'-framerate', String(FPS),
'-i', path.join(FRAMES_DIR, 'frame_%05d.png'),
'-c:v', 'libx264',
'-preset', 'medium',
'-crf', '18',
'-pix_fmt', 'yuv420p',
OUTPUT
].join(' ');
console.log(`Running ffmpeg...`);
execSync(ffmpegCmd, { stdio: 'inherit' });
rmSync(FRAMES_DIR, { recursive: true });
console.log(`\n✅ Done! Video saved to: ${OUTPUT}`);
console.log(` Duration: ${DURATION}s, ${WIDTH}×${HEIGHT}, ${FPS}fps`);
}
main().catch(e => { console.error(e); process.exit(1); });
import puppeteer from 'puppeteer';
import { execSync } from 'child_process';
import { mkdirSync, rmSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FRAMES_DIR = path.join(__dirname, 'frames');
const OUTPUT = path.join(__dirname, 'banner-loop.mp4');
const HTML_PATH = path.join(__dirname, 'banner-mega.html');
const FPS = 30;
const DURATION = 20; // 20s = one full loop (covers 16s crossfade + 20s feature rotation)
const TOTAL_FRAMES = FPS * DURATION;
// Display: 1m x 2.5m → render at 1080 x 2700px (1:2.5)
const WIDTH = 1080;
const HEIGHT = 2700;
async function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function main() {
// Clean up old frames
if (existsSync(FRAMES_DIR)) rmSync(FRAMES_DIR, { recursive: true });
mkdirSync(FRAMES_DIR, { recursive: true });
console.log(`Recording ${DURATION}s at ${FPS}fps → ${TOTAL_FRAMES} frames`);
console.log(`Resolution: ${WIDTH}×${HEIGHT}`);
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: WIDTH, height: HEIGHT, deviceScaleFactor: 1 });
// Load the banner
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'networkidle0' });
await wait(1000); // let animations initialize
// Capture frames by advancing CSS animations
// We override animation play state and manually seek time
await page.evaluate(() => {
// Pause all animations initially
document.getAnimations().forEach(a => a.pause());
});
const msPerFrame = (DURATION * 1000) / TOTAL_FRAMES;
for (let i = 0; i < TOTAL_FRAMES; i++) {
const t = i * msPerFrame;
// Set all animations to the correct time
await page.evaluate((time) => {
document.getAnimations().forEach(a => {
a.currentTime = time;
});
}, t);
// Small delay to let browser paint
await wait(20);
const frameNum = String(i).padStart(5, '0');
await page.screenshot({
path: path.join(FRAMES_DIR, `frame_${frameNum}.png`),
type: 'png'
});
if (i % 60 === 0) {
console.log(` Frame ${i}/${TOTAL_FRAMES} (${Math.round(t/1000)}s)`);
}
}
await browser.close();
console.log('Screenshots done. Encoding video...');
// Stitch frames into video with ffmpeg
const ffmpegCmd = [
'ffmpeg', '-y',
'-framerate', String(FPS),
'-i', path.join(FRAMES_DIR, 'frame_%05d.png'),
'-c:v', 'libx264',
'-preset', 'slow',
'-crf', '18',
'-pix_fmt', 'yuv420p',
'-vf', 'scale=1080:2700',
OUTPUT
].join(' ');
console.log(`Running: ${ffmpegCmd}`);
execSync(ffmpegCmd, { stdio: 'inherit' });
// Clean up frames
rmSync(FRAMES_DIR, { recursive: true });
console.log(`\n✅ Done! Video saved to: ${OUTPUT}`);
console.log(` Duration: ${DURATION}s, ${WIDTH}×${HEIGHT}, ${FPS}fps`);
}
main().catch(e => { console.error(e); process.exit(1); });
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment