UNPKG

taiko-video

Version:

A taiko plugin to record screencast as an mp4 video

131 lines (109 loc) 3.5 kB
const once = require('events').once; const fs = require('fs'); const path = require('path'); const spawn = require('child_process').spawn; const CAPTURE_OPTIONS = { format: 'jpeg', // jpeg or png quality: 80, // compression quality 0 ~ 100 maxWidth: 1280, maxHeight: 720, }; const IMAGE_DIGITS = 4; const DEFAULT_FPS = 8; const plugin = { client: null, deviceHeight: 0, deviceWidth: 0, eventHandler: null, filePath: null, fps: DEFAULT_FPS, frames: [], active: false, }; const mkdir = (path) => { if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true }); }; const zeroPad = (number, digits = IMAGE_DIGITS) => { const nDigits = Math.ceil(Math.log10(number + 1)); return '0'.repeat(digits - nDigits) + number; }; const frameHandler = frame => { if (!plugin.active) return; plugin.client.send('Page.screencastFrameAck', { sessionId: frame.sessionId }); plugin.deviceWidth = frame.metadata.deviceWidth; plugin.deviceHeight = frame.metadata.deviceHeight; plugin.frames.push(frame.data); }; const start = async (filePath, fps = DEFAULT_FPS) => { if (path.extname(filePath) !== '.mp4') throw new Error('Output file should have a .mp4 extension.'); mkdir(path.dirname(filePath)); plugin.filePath = filePath; plugin.fps = fps; plugin.active = true; plugin.client.on('Page.screencastFrame', frameHandler); await resume(); }; const pause = async () => { await plugin.client.send('Page.stopScreencast'); }; const resume = async () => { await plugin.client.send('Page.startScreencast', CAPTURE_OPTIONS); }; const stop = async () => { await pause(); plugin.active = false; // Save frame as images first. const filename = path.basename(plugin.filePath); const basename = filename.substring(0, filename.length - 4); const directory = path.dirname(plugin.filePath); for (const [ index, base64Data ] of plugin.frames.entries()) { const imagePath = `${directory}/${basename}${zeroPad(index + 1)}.${CAPTURE_OPTIONS.format}`; try { fs.writeFileSync(imagePath, base64Data, 'base64'); } catch (err) { console.error(err); } } // Clear out the frame buffer. plugin.frames = []; // Create a mp4 movie out of the image frames. const cmd = 'ffmpeg'; const args = [ '-y', '-i', `${directory}/${basename}%0${IMAGE_DIGITS}d.${CAPTURE_OPTIONS.format}`, '-s', `${CAPTURE_OPTIONS.maxWidth}x${CAPTURE_OPTIONS.maxHeight}`, '-codec:a', 'aac', '-b:a', '44.1k', '-b:v', '1000k', '-c:v', 'h264', '-f', 'mp4', '-vf', `setpts=N/(${plugin.fps}*TB)`, plugin.filePath, ]; const proc = spawn(cmd, args); if (process.env.DEBUG) { proc.stdout.on('data', data => console.log(data)); proc.stderr.setEncoding('utf8'); proc.stderr.on('data', data => console.error(data)); } await once(proc, 'close'); // Delete the images upon building a movie successfully. const regEx = new RegExp(`${basename}\\d{${IMAGE_DIGITS}}\\.${CAPTURE_OPTIONS.format}`, 'i'); fs.readdirSync(directory) .filter(file => regEx.test(file)) .map(file => fs.unlinkSync(`${directory}/${file}`)); }; const clientHandler = async (taiko, eventHandler) => { plugin.eventHandler = eventHandler; plugin.eventHandler.on('createdSession', async () => { plugin.client = await taiko.client(); }); }; module.exports = { ID: 'video', init: clientHandler, startRecording: start, pauseRecording: pause, resumeRecording: resume, stopRecording: stop, };