笔者最近的工作在前端数据可视化领域,会出现一些对长时间运行的前端页面进行监控的需求。以往我的解决办法是通过一些现有的平台,在个人PC上通过浏览器进行录制,或者更早的方法是通过一些录屏工具进行录制。
在这样的方式中,经常会遇到以下问题:
So,基于上述的需求,我们需要达到以下的要求:
基础语言和框架——js&nodejs
对于指定时间运行任务 —— cron job
对于打开网页 —— puppeteer
对于视频录制有以下备选方案
getDisplayMedia进行录制对于录制日志 —— puppeteer提供的devtools相关事件
对于并发处理 —— 引入加权计算
对于视频处理 —— ffmpeg
getDisplayMedia时,受到浏览器的协议限制。这个api只在访问协议为https下可用,且音频的录制需要依赖其他的api。getDisplayMedia的性能,在多网页并发录制时优化空间小,而且最致命的问题时,录制过程的性能开销,是由浏览器负担的。这意味着,如果页面本身对性能比较敏感,使用这个api基本无法录制出网页正常运行的情况。
node-xvfb存在一些问题,创建的虚拟桌面,似乎共享了同一个流的缓冲区,在并发录制时,会出现抢占的情况,导致视频内容出现加速,所以需要封装一个新的node调用xvfb的功能Typescriptimport * as process from 'child_process'; class XvfbMap { private xvfb: { [key: string]: { process: process.ChildProcessWithoutNullStreams; display: number; execPath?: string; }; } = {}; setXvfb = (key: string, display: number, process: process.ChildProcessWithoutNullStreams, execPath?: string) => { this.xvfb[key] = { display, process, execPath, }; }; getSpecXvfb = (key: string) => { return this.xvfb[key]; }; getXvfb = () => this.xvfb; } const xvfbIns = new XvfbMap(); /** * 检测虚拟桌面是否运行 * @param num 虚拟桌面窗口编号 * @param execPath 内存缓冲文件映射路径 * @returns Promise<boolean> */ const checkoutDisplay = (num: number, execPath?: string) => { const path = execPath || '/dev/null'; return new Promise<boolean>((res, rej) => { const xdpyinfo = process.spawn('xdpyinfo', [ '-display', `:${num}>${path}`, '2>&1', '&&', 'echo', 'inUse', '||', 'echo', 'free', ]); xdpyinfo.stdout.on('data', (data) => res(data.toString() === 'inUse')); xdpyinfo.stderr.on('data', (data) => rej(data.toString())); }); }; const getRunnableNumber = async (execPath?: string): Promise<number> => { const num = Math.floor(62396 * Math.random()); const isValid = await checkoutDisplay(num, execPath); if (isValid) { return num; } else { return getRunnableNumber(execPath); } }; export const xvfbStart = async ( key: string, option: { width: number; height: number; depth: 15 | 16 | 24 }, execPath?: string ) => { const randomNum = Math.floor(62396 * Math.random()); const { width, height, depth } = option; try { const xvfb = process.spawn('Xvfb', [ `:${randomNum}`, '-screen', '0', `${width}x${height}x${depth}`, '-ac', '-noreset', ]); xvfbIns.setXvfb(key, randomNum, xvfb, execPath); return randomNum; } catch (error) { console.log(error); return 99; } }; export const xvfbStop = (key: string) => { const xvfb = xvfbIns.getSpecXvfb(key); return xvfb.process.kill(); }; export default xvfbIns;
typescriptimport { CronJob } from 'cron'; interface CacheType { [key: string]: CronJob; } class CronCache { private cache: CacheType = {}; private cacheCount = 0; setCache = (key: string, value: CronJob) => { this.cache[key] = value; this.cacheCount++; return; }; getCache = (key: string) => { return this.cache[key]; }; deleteCache = (key: string) => { if (this.cache[key]) { delete this.cache[key]; } this.cacheCount = this.cacheCount > 0 ? this.cacheCount - 1 : 0; }; getCacheCount = () => this.cacheCount; getCacheMap = () => this.cache; } export default new CronCache();
启动puppeteer时,需要提供一系列参数
typescriptconst browser = await puppeteer.launch({ headless: false, executablePath: '/usr/bin/google-chrome', defaultViewport: null, args: [ '--enable-usermedia-screen-capturing', '--allow-http-screen-capture', '--ignore-certificate-errors', '--enable-experimental-web-platform-features', '--allow-http-screen-capture', '--disable-infobars', '--no-sandbox', '--disable-setuid-sandbox',//关闭沙箱 '--start-fullscreen', '--display=:' + display, '-–disable-dev-shm-usage', '-–no-first-run', //没有设置首页。 '–-single-process', //单进程运行 '--disable-gpu', //GPU硬件加速 `--window-size=${width},${height}`,//窗口尺寸 ], });
该api的调用,会导致chrome弹出选择具体录制哪个网页的交互窗口。关闭这个窗口需要在启动puppeteer时启用以下参数
typescript'--enable-usermedia-screen-capturing', `--auto-select-desktop-capture-source=recorder-page`, '--allow-http-screen-capture', '--ignore-certificate-errors', '--enable-experimental-web-platform-features', '--allow-http-screen-capture', '--disable-infobars', '--no-sandbox', '--disable-setuid-sandbox',
执行录制时,需要通过puppeteer page.exposeFunction注入函数进行执行。
Q:为什么要引入xvfb?
A:在尝试的方案中,getDisplayMedia需要运行环境提供一个桌面环境。在现行方案中,则是需要把xvfb的视频流直接推入到ffmpeg中
Q:为什么对内存有一定要求?
A:提供chrome的最小运行内存
https://github.com/sadofriod/time-recorder