| name | slide-to-video-export |
| description | Export slide-based educational content as MP4 videos with audio and subtitles. Pipeline: Playwright screenshots → ffmpeg video synthesis → SRT subtitle generation. Works with any structured slide data (JSON with canvas elements, audio files). |
| tags | ["video","ffmpeg","playwright","education","export","slides"] |
Slide-to-Video Export
Convert structured slide presentations into MP4 videos with synchronized audio and subtitles.
When to Use
- Exporting interactive classroom/presentation content as shareable videos
- Converting slide decks with audio narration into standalone video files
- Batch-exporting educational content for platforms (B站, YouTube, etc.)
Pipeline Overview
Slide JSON → Playwright screenshot (per slide) → ffmpeg encode (image+audio → clip) → ffmpeg concat → MP4
↓
SRT subtitle file (extracted from slide text)
Prerequisites
- Node.js with Playwright installed (
npx playwright install chromium)
- ffmpeg with libx264 + aac support
- Slide data: JSON with canvas elements (positions, text, shapes)
- Audio files: MP3 per scene (optional)
Core Implementation
1. Scale Canvas Viewport to 1920×1080
Slides are authored at a canvas viewport (e.g. 1000×563). Scale all element positions/sizes to fill 1920×1080:
const canvasW = canvas.viewportSize || 1000;
const canvasH = canvasW * (canvas.viewportRatio || 0.5625);
const scaleX = 1920 / canvasW;
const scaleY = 1080 / canvasH;
const left = (el.left || 0) * scaleX;
const top = (el.top || 0) * scaleY;
const width = (el.width || 100) * scaleX;
const height = (el.height || 50) * scaleY;
const fontSize = 32 * Math.min(scaleX, scaleY);
content = content.replace(/font-size:\s*(\d+)px/g, (_, px) => `font-size:${parseInt(px) * scaleX}px`);
2. Render Slides with Playwright
const { chromium } = await import('playwright');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
writeFileSync(htmlPath, slideHtml);
await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle' });
await page.screenshot({ path: imgPath, type: 'png' });
Pitfall: Reuse browser across multiple slides/classrooms. Launch once, create new context per classroom, close context after.
3. Encode Clips with ffmpeg
ffmpeg -y -loop 1 -i slide.png -i audio.mp3 \
-c:v libx264 -tune stillimage -c:a aac -b:a 192k \
-pix_fmt yuv420p -t $DURATION \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:white" \
-shortest clip.mp4
ffmpeg -y -loop 1 -i slide.png \
-c:v libx264 -tune stillimage -pix_fmt yuv420p -t 3 \
-vf "scale=1920:1080:..." clip.mp4
4. Concatenate Clips
echo "file 'clip_0.mp4'" > concat.txt
echo "file 'clip_1.mp4'" >> concat.txt
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy output.mp4
5. Generate SRT Subtitles
Extract text from slide elements, split into timed segments matching audio duration:
function formatSrtTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 1000);
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')},${String(ms).padStart(3,'0')}`;
}
Write SRT alongside MP4 for external player use (VLC, PotPlayer).
Pitfalls
HTML Content is a String, Not an Object
Slide element content can be an HTML string (<p style="font-size:32px"><strong>text</strong></p>), not an object with .text property. Always check typeof content === 'string' first.
ffmpeg drawtext Escaping is a Nightmare
Do NOT use inline drawtext filters with Chinese/special characters. The shell escaping is unreliable. Use SRT files instead — they're simpler and supported by all video players.
If you must burn subtitles into video, use -filter_complex_script filter.txt with the filter written to a file, not passed via shell.
ffmpeg subtitle Filter May Not Be Available
Some ffmpeg builds (especially Homebrew default) don't include libass. Check with:
ffmpeg -filters 2>&1 | grep subtitle
If missing, use SRT files externally or drawtext as fallback.
Node.js stdout Buffering
console.log() output is buffered in Node.js. For long-running batch jobs, output won't appear until the buffer flushes. Monitor progress via filesystem (count output files) instead of relying on stdout.
Shell Argument Length Limits
When building ffmpeg filter chains with many drawtext entries, the command can exceed shell argument limits. Write filters to a file and use -filter_complex_script.
Content Only Fills 1/4 of Frame
Symptom: Slide content appears tiny in the 1920x1080 video, occupying only the top-left quarter.
Root cause: Canvas elements are positioned relative to the original viewport (e.g., 1000×563). If you render at 1920x1080 viewport but don't scale element positions, they cluster in the top-left 1000×563 area.
Fix: Scale ALL dimensions — positions, sizes, font-sizes, border-radius — by scaleX = 1920/canvasW and scaleY = 1080/canvasH. Also scale inline font-size in HTML content strings:
content = content.replace(/font-size:\s*(\d+)px/g, (_, px) => `font-size:${parseInt(px) * scaleX}px`);
The slide HTML container must be exactly 1920×1080:
.slide { position: relative; width: 1920px; height: 1080px; overflow: hidden; }
Batch Export Strategy
const browser = await chromium.launch({ headless: true });
for (const id of targetIds) {
try {
await exportClassroom(id, browser);
} catch (e) {
console.log(`ERROR: ${id} — ${e.message}`);
}
}
await browser.close();
Speed: ~76 seconds per classroom (10-15 slides with audio). 700 classrooms ≈ 15 hours.
Monitoring Batch Progress
stdout is buffered in Node.js. Monitor via filesystem:
ls videos/*.mp4 | wc -l
ps aux | grep "ffmpeg.*clip"
ls -lt .video-tmp/ | head -3
Output
videos/<title>.mp4 — 1080P H.264+AAC video
videos/<title>.srt — SRT subtitle file (for external players)