(msg: Api.Message, command: "q" | "quote")
| 1327 | }; |
| 1328 | messages.forEach(scan); |
| 1329 | return out; |
| 1330 | } |
| 1331 | |
| 1332 | async function encodeFramesToWebm(frames: Buffer[], fps = TG_STICKER_FPS): Promise<Buffer> { |
| 1333 | const t0 = Date.now(); |
| 1334 | const tmpBase = path.join(os.tmpdir(), `telebox_quote_webm_${Date.now()}_${Math.random().toString(16).slice(2)}`); |
| 1335 | const dir = `${tmpBase}_frames`; |
| 1336 | const outputFor = (crf: number) => `${tmpBase}_${crf}.webm`; |
| 1337 | const outputs: string[] = []; |
| 1338 | try { |
| 1339 | fs.mkdirSync(dir, { recursive: true }); |
| 1340 | const tw = Date.now(); |
| 1341 | frames.forEach((frame, i) => fs.writeFileSync(path.join(dir, `frame_${String(i + 1).padStart(3, "0")}.png`), frame)); |
| 1342 | quoteTiming("webm.write_frames", tw, { frames: frames.length }); |
| 1343 | |
| 1344 | let best: Buffer | undefined; |
| 1345 | let bestCrf = WEBM_CRF_STEPS[0]; |
| 1346 | for (const crf of WEBM_CRF_STEPS) { |
| 1347 | const output = outputFor(crf); |
| 1348 | outputs.push(output); |
| 1349 | const te = Date.now(); |
| 1350 | await execFileAsync("ffmpeg", [ |
| 1351 | "-hide_banner", "-loglevel", "error", "-y", |
| 1352 | "-framerate", String(fps), |
| 1353 | "-i", path.join(dir, "frame_%03d.png"), |
| 1354 | "-vf", "split[v][a];[a]alphaextract[alpha];[v][alpha]alphamerge,format=yuva420p", |
| 1355 | "-an", "-c:v", "libvpx-vp9", |
| 1356 | "-deadline", "good", "-cpu-used", "4", |
| 1357 | "-b:v", "0", "-crf", String(crf), |
| 1358 | "-row-mt", "1", "-tile-columns", "1", |
| 1359 | "-auto-alt-ref", "0", "-pix_fmt", "yuva420p", |
| 1360 | "-metadata:s:v:0", "alpha_mode=1", |
| 1361 | output, |
| 1362 | ], 30000); |
| 1363 | const encoded = fs.readFileSync(output); |
| 1364 | quoteTiming("webm.ffmpeg_encode", te, { frames: frames.length, fps, crf, bytes: encoded.length }); |
| 1365 | best = encoded; |
| 1366 | bestCrf = crf; |
| 1367 | if (encoded.length <= TG_STICKER_MAX_BYTES) break; |
| 1368 | } |
| 1369 | quoteTiming("webm.encode_total", t0, { frames: frames.length, bytes: best?.length || 0, crf: bestCrf }); |
| 1370 | return best || Buffer.alloc(0); |
| 1371 | } finally { |
| 1372 | for (const output of outputs) try { if (fs.existsSync(output)) fs.unlinkSync(output); } catch (err: any) { |
| 1373 | console.debug("[quote] waitForStableFile loop error:", err?.message || err); |
| 1374 | } |
| 1375 | try { if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); } catch (err: any) { |
| 1376 | console.debug("[quote] waitForStableFile loop error:", err?.message || err); |
| 1377 | } |
| 1378 | } |
| 1379 | } |
| 1380 | async function generateAnimatedQuoteWebm(quoteMessages: any[], args: QuoteArgs): Promise<{ image: Buffer; ext: string; width?: number; height?: number; duration?: number }> { |
| 1381 | const t0 = Date.now(); |
| 1382 | const emojiIds = collectAnimatedEmojiIds(quoteMessages); |
| 1383 | const mediaMessages = collectAnimatedMediaMessages(quoteMessages); |
| 1384 | const sources: { kind: "emoji" | "media"; key: string | any; raw: Buffer; size: number; info: { fps: number; duration: number } }[] = []; |
| 1385 | |
| 1386 | const rawSources = [ |
no test coverage detected