Changeset 7ecbefc in Klonkt
- Timestamp:
- 06/30/2026 05:56:01 PM (2 months ago)
- Branches:
- main
- Children:
- 1c33804
- Parents:
- 857a06f
- File:
-
- 1 edited
-
src/services/VideoCoverService.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/VideoCoverService.js
r857a06f r7ecbefc 22 22 function ensureLib() { if (!_lib) _lib = WebP.Image.initLib(); return _lib; } 23 23 24 // node-webpmux's getFrameData(i) returns ONLY frame i's own sub-region (x,y,width,height) — it does 25 // NOT composite onto the canvas. Real (tool-made) animated WebPs use partial frames of varying size, 26 // so we composite each onto a persistent W×H canvas (honoring blend + dispose) and emit consistent 27 // full frames. Feeding ffmpeg the raw varying-size sub-regions desyncs the stream → a torn/tiled video. 28 async function compositeFrames(img) { 29 const W = img.width, H = img.height, frames = img.anim.frames; 30 const canvas = Buffer.alloc(W * H * 4); // transparent black 31 const out = []; 32 let prev = null; // previous frame's rect + dispose 33 for (let i = 0; i < frames.length; i++) { 34 const fr = frames[i]; 35 if (prev && prev.dispose) { // dispose-to-background: clear the previous frame's rect first 36 for (let row = 0; row < prev.h; row++) { 37 const y = prev.y + row; if (y < 0 || y >= H) continue; 38 canvas.fill(0, (y * W + prev.x) * 4, (y * W + prev.x + prev.w) * 4); 39 } 40 } 41 const data = Buffer.from(await img.getFrameData(i)); // fr.width*fr.height*4 RGBA sub-region 42 const fx = fr.x, fy = fr.y, fw = fr.width, fh = fr.height, blend = fr.blend; 43 if (fx === 0 && fy === 0 && fw === W && fh === H && !blend) { 44 data.copy(canvas, 0); // full opaque overwrite (the typical base frame) 45 } else { 46 for (let row = 0; row < fh; row++) { 47 const cy = fy + row; if (cy < 0 || cy >= H) continue; 48 for (let col = 0; col < fw; col++) { 49 const cx = fx + col; if (cx < 0 || cx >= W) continue; 50 const s = (row * fw + col) * 4, d = (cy * W + cx) * 4, sa = data[s + 3]; 51 if (!blend || sa === 255) { canvas[d] = data[s]; canvas[d + 1] = data[s + 1]; canvas[d + 2] = data[s + 2]; canvas[d + 3] = sa; } 52 else if (sa !== 0) { // alpha-over the existing canvas pixel 53 const a = sa / 255, ia = 1 - a; 54 canvas[d] = (data[s] * a + canvas[d] * ia) | 0; 55 canvas[d + 1] = (data[s + 1] * a + canvas[d + 1] * ia) | 0; 56 canvas[d + 2] = (data[s + 2] * a + canvas[d + 2] * ia) | 0; 57 canvas[d + 3] = Math.min(255, sa + ((canvas[d + 3] * ia) | 0)); 58 } 59 } 60 } 61 } 62 out.push(Buffer.from(canvas)); // snapshot the full composited canvas 63 prev = { x: fx, y: fy, w: fw, h: fh, dispose: fr.dispose }; 64 } 65 return Buffer.concat(out); 66 } 67 24 68 // True if the file is an animated WebP (a VP8X chunk with the animation flag set). 25 69 export function isAnimatedWebp(filePath) { … … 44 88 await img.load(srcPath); 45 89 if (!img.hasAnim || !img.anim || !Array.isArray(img.anim.frames) || img.anim.frames.length < 2) return null; 46 const W = img.width, H = img.height , n = img.anim.frames.length;90 const W = img.width, H = img.height; 47 91 const fps = Math.max(1, Math.min(30, Math.round(1000 / (img.anim.frames[0].delay || 100)))); 48 // getFrameData(i) returns the FULL-canvas RGBA (W*H*4) for frame i (already composited).49 const bufs = [];50 for (let i = 0; i < n; i++) bufs.push(Buffer.from(await img.getFrameData(i)));51 92 await fs.promises.mkdir(outDir, { recursive: true }); 52 93 rawPath = path.join(outDir, baseName + '.rgba.tmp'); 53 await fs.promises.writeFile(rawPath, Buffer.concat(bufs));94 await fs.promises.writeFile(rawPath, await compositeFrames(img)); // full composited W×H frames 54 95 const videoPath = path.join(outDir, baseName + '.mp4'); 55 96 const posterPath = path.join(outDir, baseName + '.jpg');
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)