I recently work in the field of front-end data visualization, and the need for some monitoring of long-running front-end pages comes up. In the past, my solution was to record through some existing platform on my personal PC via browser, or an earlier approach was to record through some screen recording tools.
In such an approach, the following problems were often encountered.
So, based on the above needs, we need to achieve the following requirements.
Base language and framework - js & nodejs
For running tasks at specified times -- cron job
For opening web pages -- puppeteer
For video recording the following options are available
getDisplayMedia for recordingFor recording logs -- puppeteer provides devtools related events
For concurrent processing -- introduce weighted calculations
For video processing -- ffmpeg
getDisplayMedia is limited by the browser's protocol. This api is only available when the access protocol is https, and the recording of audio depends on other api.getDisplayMedia has little room for optimization when recording multiple pages concurrently, and the most fatal problem is that the performance overhead of the recording process is borne by the browser. This means that if the page itself is more performance sensitive, it is basically impossible to record the page running properly using this api.
node-xvfb has some problems, the virtual desktops created, seem to share the same stream buffer, in the case of concurrent recording, there will be a situation of preemption, resulting in accelerated video content, so the need to encapsulate a new node call xvfbTypescriptimport * 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();
When starting puppeteer, you need to provide parameters
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}`,//窗口尺寸 ], });
The api call causes chrome to pop up an interactive window to choose which specific web page to record. Closing this window requires the following parameters to be enabled when starting 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',
To execute the recording, you need to inject the function via puppeteer page.exposeFunction.
Q: Why do I need to introduce xvfb?
A: In the tried and tested solution, getDisplayMedia requires the runtime environment to provide a desktop environment. In the current solution, it is necessary to push the video stream from xvfb directly into ffmpeg
Q: Why are there certain memory requirements?
A: To provide the minimum running memory for chrome
https://github.com/sadofriod/time-recorder