190 lines
5.6 KiB
JavaScript
190 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
||
// Katalog-Assets für das Developer Portal aufnehmen (Maße laut Portal, Stand 2026-11-13):
|
||
// featuring.mp4 3:2, 1500×1000, 6–15 s, ≤ 30 fps, ≤ 3 MB
|
||
// fallback.gif 3:2, 1000×667, 3–10 s Schleife, Ziel ≤ 2 MB
|
||
// fallback.png 1500×1000, ≤ 800 KB (Standbild statt GIF)
|
||
// tile.png 560×256, ≤ 800 KB
|
||
// Nur das Brett, keine Menüs. Braucht ffmpeg. Aufruf: npm run capture (Ergebnis in dist/marketing/)
|
||
import {execFileSync} from 'node:child_process'
|
||
import {
|
||
copyFileSync,
|
||
mkdirSync,
|
||
readdirSync,
|
||
readFileSync,
|
||
statSync,
|
||
} from 'node:fs'
|
||
import {createServer} from 'node:http'
|
||
import {dirname, extname, join, resolve} from 'node:path'
|
||
import {fileURLToPath} from 'node:url'
|
||
import {build} from 'esbuild'
|
||
import {chromium} from 'playwright'
|
||
|
||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||
const site = join(root, 'dist/capture')
|
||
const out = join(root, 'dist/marketing')
|
||
mkdirSync(site, {recursive: true})
|
||
mkdirSync(out, {recursive: true})
|
||
|
||
// 1. Aufnahmeseite bauen (gleiche Defines wie build:client)
|
||
await build({
|
||
entryPoints: [join(root, 'scripts/capture/capture.ts')],
|
||
bundle: true,
|
||
format: 'esm',
|
||
outdir: site,
|
||
logLevel: 'warning',
|
||
define: {
|
||
'import.meta.env.MODE': '"reddit"',
|
||
'import.meta.env.BASE_URL': '"/"',
|
||
__API_ORIGIN__: '""',
|
||
},
|
||
loader: {'.woff2': 'file', '.woff': 'file'},
|
||
assetNames: 'fonts/[name]-[hash]',
|
||
})
|
||
copyFileSync(
|
||
join(root, 'scripts/capture/capture.html'),
|
||
join(site, 'index.html'),
|
||
)
|
||
|
||
// 2. Statischer Server: zuerst die Aufnahmeseite, dann public/ (Sprites)
|
||
const types = {
|
||
'.html': 'text/html',
|
||
'.js': 'text/javascript',
|
||
'.css': 'text/css',
|
||
'.png': 'image/png',
|
||
'.woff2': 'font/woff2',
|
||
'.woff': 'font/woff',
|
||
}
|
||
const server = createServer((req, res) => {
|
||
const path = decodeURIComponent((req.url ?? '/').split('?')[0])
|
||
for (const dir of [site, join(root, 'public')]) {
|
||
const file = join(dir, path === '/' ? 'index.html' : path)
|
||
if (file.startsWith(dir) && existsFile(file)) {
|
||
res.writeHead(200, {
|
||
'Content-Type': types[extname(file)] ?? 'application/octet-stream',
|
||
})
|
||
res.end(readFileSync(file))
|
||
return
|
||
}
|
||
}
|
||
res.writeHead(404)
|
||
res.end()
|
||
})
|
||
function existsFile(f) {
|
||
try {
|
||
return statSync(f).isFile()
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
||
const base = `http://127.0.0.1:${server.address().port}/`
|
||
const ffmpeg = (...args) =>
|
||
execFileSync('ffmpeg', ['-v', 'error', '-y', ...args])
|
||
const size = f => statSync(join(out, f)).size
|
||
const kb = f => `${Math.round(size(f) / 1024)} KB`
|
||
|
||
const browser = await chromium.launch()
|
||
try {
|
||
// 3. Video: Partie mit Vorschau im Takt; die ersten 2 Züge (Burg + Bauer) stehen schon im ersten Bild
|
||
const videoDir = join(site, 'video')
|
||
const ctx = await browser.newContext({
|
||
viewport: {width: 1500, height: 1000},
|
||
recordVideo: {dir: videoDir, size: {width: 1500, height: 1000}},
|
||
})
|
||
const t0 = Date.now()
|
||
const page = await ctx.newPage()
|
||
await page.goto(base)
|
||
await page.evaluate(() => document.fonts.ready)
|
||
await page.waitForTimeout(400)
|
||
const start = (Date.now() - t0) / 1000
|
||
await page.evaluate(() => window.capture.play(2, 180, 200))
|
||
await page.waitForTimeout(2200) // Endbild mit allen Bannern stehen lassen
|
||
const length = (Date.now() - t0) / 1000 - start
|
||
await ctx.close()
|
||
const webm = join(videoDir, readdirSync(videoDir)[0])
|
||
|
||
// MP4 (H.264, 30 fps); CRF erhöhen, bis es unter 3 MB passt
|
||
for (const crf of [20, 24, 28, 32]) {
|
||
ffmpeg(
|
||
'-ss',
|
||
String(start),
|
||
'-i',
|
||
webm,
|
||
'-t',
|
||
String(Math.min(length, 15)),
|
||
'-r',
|
||
'30',
|
||
'-c:v',
|
||
'libx264',
|
||
'-preset',
|
||
'slow',
|
||
'-crf',
|
||
String(crf),
|
||
'-pix_fmt',
|
||
'yuv420p',
|
||
'-movflags',
|
||
'+faststart',
|
||
'-an',
|
||
join(out, 'featuring.mp4'),
|
||
)
|
||
if (size('featuring.mp4') <= 3 * 1024 * 1024) break
|
||
}
|
||
|
||
// GIF (aus der Mitte, 1000×667, eigene Palette); Stufen probieren, bis ≤ 2 MB
|
||
for (const [fps, colors, secs] of [
|
||
[12, 96, 8],
|
||
[10, 64, 8],
|
||
[10, 64, 6],
|
||
[8, 48, 6],
|
||
]) {
|
||
ffmpeg(
|
||
'-ss',
|
||
String(start + 1),
|
||
'-t',
|
||
String(secs),
|
||
'-i',
|
||
webm,
|
||
'-vf',
|
||
`fps=${fps},scale=1000:667:flags=lanczos,split[a][b];[a]palettegen=max_colors=${colors}:stats_mode=diff[p];[b][p]paletteuse=dither=none`,
|
||
'-loop',
|
||
'0',
|
||
join(out, 'fallback.gif'),
|
||
)
|
||
if (size('fallback.gif') <= 2 * 1024 * 1024) break
|
||
}
|
||
|
||
// 4. Standbilder: fertiges Brett (Fallback) und Kachel mit Schriftzug
|
||
const still = await browser.newPage({viewport: {width: 1500, height: 1000}})
|
||
await still.goto(base)
|
||
await still.evaluate(() => document.fonts.ready)
|
||
await still.evaluate(() => window.capture.jump(999))
|
||
await still.waitForTimeout(300)
|
||
await still.screenshot({path: join(out, 'fallback.png')})
|
||
await still.close()
|
||
|
||
const tile = await browser.newPage({viewport: {width: 1120, height: 512}})
|
||
await tile.goto(base)
|
||
await tile.evaluate(() => {
|
||
document.body.classList.add('tile')
|
||
return document.fonts.ready
|
||
})
|
||
await tile.evaluate(() => window.capture.jump(999))
|
||
await tile.waitForTimeout(300)
|
||
await tile.screenshot({path: join(site, 'tile@2x.png')})
|
||
await tile.close()
|
||
ffmpeg(
|
||
'-i',
|
||
join(site, 'tile@2x.png'),
|
||
'-vf',
|
||
'scale=560:256:flags=lanczos',
|
||
join(out, 'tile.png'),
|
||
)
|
||
|
||
console.log(`Fertig in ${out}:`)
|
||
for (const f of ['featuring.mp4', 'fallback.gif', 'fallback.png', 'tile.png'])
|
||
console.log(` ${f} ${kb(f)}`)
|
||
console.log(` Videolänge ≈ ${Math.min(length, 15).toFixed(1)} s`)
|
||
} finally {
|
||
await browser.close()
|
||
server.close()
|
||
}
|