rrWeb整体方案
1、整体流程
1.1、流程图

生产负载调用rrWeb情况
前端调用保存录制使用固定地址,调用预生产服务进行保存
定时任务执行,设置地址配置为预生产地址
1.2、功能说明
1.2.1 录制保存功能
- 地址
/rrWeb/saveRrWeb
- 功能
首次录制,生成本次录制的唯一id
同一订单可能存在多个录制id
录制id在当前录制流程中唯一,关闭录制或关闭浏览器重新进入开始录制,会生成新的id
使用Redis记录录制id每次片段的顺序
保存数据到backTrackRecord、backTrackVideo表
保存录制数据到服务器中,保存目录/data/service/rrWeb/sava/录制id/片段数据
/data/service/rrWeb目录在sys_config表中配置
1.2.2 定时合并数据
- 地址
/job/convertVideo
- 功能
查询backTrackRecord表中未合并过的数据
获取对应录制id
根据录制id获取/data/service/rrWeb/sava/录制id/目录下所有的片段数据
根据片段录制顺序,读取文件内容,写到/data/service/rrWeb/录制id.txt文件下
更新backTrackRecord状态
1.2.3 Linux下定时转换视频
- 地址
无
- 功能
将/data/service/rrWeb/下对应的文件备份到history目录
使用rrvideo命令,将对应文件转换为视频,保存到/data/service/rrWeb/mp4下
转换过程超过2分钟的停止
转换完成删除/data/service/rrWeb/下对应文件
- 配置脚本执行
# 打开定时任务配置文件
crontab -e
# 在配置文件中写入定时任务的操作, 这里就是指定每天1点10分定时执行脚本,并把执行脚本的日志写入文件 crontabLoad.log
10 1 * * * sh /data/service/rrWeb/start.sh > /data/service/rrWeb/crontabLoad.log 2>&1
- 脚本
#!/bin/bash# 这个很重要,不引用环境变量,脚本执行rrvideo时会报错找不到命令source /etc/profile
#set -xINPUT_DIR=/data/service/rrWeb
OUTPUT_DIR=/data/service/rrWeb/mp4
HISTORY_DIR=/data/service/rrWeb/history
TIMEOUT=120# 2分钟超时time=$(date-d"7 minute ago" +"%Y-%m-%d %H:%M:%S")echo"${time} :开始执行rrWeb转视频"files=$(ls $INPUT_DIR/*.txt |wc-l);if["$files"!="0"];thenforfin$INPUT_DIR/*.txt;dofilename=$(basename"$f")echo"当前转换filename:${filename}"# 复制到history目录cp"$f""$HISTORY_DIR/$filename"nohup rrvideo --input"$f"--config"$INPUT_DIR/rrvideo.config"--output"$OUTPUT_DIR/${filename%.*}.mp4"&pid=$!timeout=$TIMEOUTwhileps-p$pid> /dev/null;dosleep1timeout=$((timeout-1))if[$timeout-eq0];thenkill-9$pidecho"killed ${filename}"breakfidoneif[$timeout-ne0];then# 删除txt文件rm"$f"fiecho"Finished $f to $filename.mp4"doneelseecho"没有要转换的文件"fi# 关闭无头浏览器进程PID=`ps-ef|grep puppeteer |awk'{print $2}'`echo"得到进程ID:${PID}"echo"结束进程"foridin${PID}dokill-9${id}echo"killed ${id}"doneecho"结束进程完成"echo"执行rrWeb转视频结束"
rrvideo.config
{
"width":1280,
"height":720,
"speed": 1,
"skipInactive": true,
"mouseTail": {
"strokeStyle": "green",
"lineWidth": 2
},
"startDelayTime": 1000
}
1.2.4 定时上传OBS
- 地址
/job/uploadVideoToObs
- 功能
查询backTrackVideo表中未上传OBS的数据
3天内还未上传到OBS,进行告警
根据录制id获取/data/service/rrWeb/map目录下的视频文件
获取视频流,上传OBS
更新backTrackVideo表OBS视频地址
2、rrWeb环境搭建
2.1、安装nodejs
2.1.1、下载node.js
node官网下载地址:https://nodejs.org/en/download/
下载对应的包:node-v14.17.6-linux-x64.tar.gz
或者使用命令下载
wget https://nodejs.org/download/release/v14.17.6/node-v14.17.6-linux-x64.tar.gz
2.1.2、上传文件到服务器
将文件放到预生产/opt/nodejs下

2.1.3、安装
# 解压文件tar zxvf node-v14.17.6-linux-x64.tar.gz
## 更改名称mv node-v14.17.6-linux-x64 node14.17.6
## 赋予执行权限chmod777 node14.17.6/
# 配置环境变量vim /etc/profile
# 在文件中增加配置,保存后退出exportNODE_HOME=/opt/nodejs/node14.17.6
exportPATH=$NODE_HOME/bin:$PATH# 配置生效source /etc/profile
#验证是否安装成功node-vnpm-v
2.2、ffmpeg安装
2.2.1、下载
ffmpeg官方网站:FFmpeg ;在官方网站内也可以下载ffmpeg的源码以及ffmpeg编译好的库文件;官方网站首页如下图;点击下图绿色按键"Download"可以进入ffmpeg的下载页面;在官方网站首页的左侧有几个子目录,其中包含下载目录Download和使用帮助文档目录Documentation。

在点击Download后可以进入ffmpeg的下载页面,如下图;通过点击Download Source Code就可以下载最新的ffmpeg源代码;也可以下载Linux/Windows/MacOS这三种平台下ffmpeg的可执行程序和lib库文件,如下图红色框。

2.2.2、ffmpeg的安装
cd /data/service
# 创建文件夹mkdir ffmpeg
# 上传下载的文件ffmpeg-release-i686-static.tar到此文件夹下# 解压tar-xvf ffmpeg-release-i686-static.tar
## 更改名称mv ffmpeg-6.0-i686-static ffmpeg
cd ffmpeg
# 查看版本
./ffmpeg -version# 配置环境变量vim /etc/profile
# 在文件中增加配置,保存后退出exportPATH=$PATH:/data/service/ffmpeg/ffmpeg
# 配置生效source /etc/profile
# 创建软连接ln-s /data/service/ffmpeg/ffmpeg ffmpeg
# 查看环境变量是否生效
ffmpeg -version
2.3、rrvideo安装
# 全局安装rrvideonpm i -g rrvideo --unsafe-perm=true
# 到puppeteer目录cd /opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer
# 安装依赖npminstall# 安装成功后执行命令转换视频
rrvideo --input rrWeb_48234910066649106.txt --output rrWeb_48234910066649106.mp4
# 报错
Failed to transform this session.
Error: Failed to launch the browser process!
/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/.local-chromium/linux-818858/chrome-linux/chrome: error while loading shared libraries: libX11-xcb.so.1: cannot open shared object file: No such file or directory
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
at onClose (/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:193:20)
at Interface.<anonymous>(/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:183:68)
at Interface.emit (events.js:412:35)
at Interface.close (readline.js:451:8)
at Socket.onend (readline.js:224:10)
at Socket.emit (events.js:412:35)
at endReadableNT (internal/streams/readable.js:1317:12)
at processTicksAndRejections (internal/process/task_queues.js:82:21)# 安装缺失的库文件
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y
yum update nss -y
yum install libX11-devel --nogpg
yum install libgbm*
yum install libdrm*
# 再次执行转换视频命令
rrvideo --input rrWeb_48234910066649106.txt --output rrWeb_48234910066649106.mp4
# 报错
Failed to transform this session.
Error: Failed to launch the browser process!
/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/.local-chromium/linux-818858/chrome-linux/chrome: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directory
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
at onClose (/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:193:20)
at Interface.<anonymous>(/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:183:68)
at Interface.emit (events.js:412:35)
at Interface.close (readline.js:451:8)
at Socket.onend (readline.js:224:10)
at Socket.emit (events.js:412:35)
at endReadableNT (internal/streams/readable.js:1317:12)
at processTicksAndRejections (internal/process/task_queues.js:82:21)# 确认报错为缺少的库文件,libgbm.so.1# 或使用ldd /opt/nodejs/node14.17.6/lib/node_modules/rrvideo/node_modules/puppeteer/.local-chromium/linux-818858/chrome-linux/chrome 命令 查看缺失的库文件# 去安装过libgbm.so.1的服务器,查看libgbm.so.1通过安装什么软件,此次我是去测试环境执行# 测试环境执行如下命令[root@test-webapp-svr20 ffmpeg]# rpm -qf /lib64/libgbm.so.1
mesa-libgbm-21.1.5-1.el8.x86_64
# 预生产执行
yum install mesa-libgbm-21.1.5-1.el8.x86_64
# 安装成功后执行转换命令,转换成功表示安装完成
rrvideo --input rrWeb_48234910066649106.txt --output rrWeb_48234910066649106.mp4
缺少其他库文件解决方法
# 如缺少libpcre.so.1文件
一种是安装libpcre.so.1对应的软件,
一种是获取libpcre.so.1库文件并放置在 /lib64目录下。
最后一种是获取libpcre.so.1库文件库文件并上传至服务器A任意目录下上并设置LD_LIBRARY_PATH变量
方法一:获取软件并设置LD_LIBRARY_PATH变量方法。如下:
Step1:从服务器B上下载libpcre.so.1对应软件,上传至服务器A上任意目录下。如/opt。
Step2:设置LD_LIBRARY_PATH变量。执行export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt。说明:LD_LIBRARY_PATH是Linux环境变量名,该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径。
Step3:重新执行程序,问题解决。
方法二:获取软件并放置指定目录下。如下:
Step1:从服务器B上下载libpcre.so.1对应软件,上传至服务器A上的/lib64目录下。/lib64为步骤(2)中查出缺失文件的目录。
Step2:重新执行程序,问题解决。
方法三:安装libpcre.so.1对应的软件方法如下:
Step1:在服务器B上执行rpm -qf /lib64/libpcre.so.1。
[root@www ~]# rpm -qf /lib64/libpcre.so.1
libpcre-3.2-21.el5 --> 说明libpcre.so.1文件是通过安装libpcre-3.2-21.el5获取。
Step2:查看Linux服务器系统版本,获取对应的镜像包,从而获取libpcre-3.2-21.el5.rpm安装软件
Step3:执行rpm -ivh libpcre-3.2-21.el5.rpm安装。
Step4:重新执行程序,问题解决。
2.4、使用rrvideo转换视频问题
2.4.1、rrvideo源码
rrvideo-main\src\index.ts
import*as fs from"fs";import*as path from"path";import{ spawn }from"child_process";import puppeteer from"puppeteer";import type { eventWithTime }from"rrweb/typings/types";import type { RRwebPlayerOptions }from"rrweb-player";import type { Page, Browser }from"puppeteer";const rrwebScriptPath = path.resolve(
require.resolve("rrweb-player"),"../../dist/index.js");const rrwebStylePath = path.resolve(rrwebScriptPath,"../style.css");const rrwebRaw = fs.readFileSync(rrwebScriptPath,"utf-8");const rrwebStyle = fs.readFileSync(rrwebStylePath,"utf-8");interfaceConfig{// start playback delay time
startDelayTime?: number,}functiongetHtml(events: Array<eventWithTime>,
config?: Omit<RRwebPlayerOptions["props"]& Config,"events">): string {return`
<html>
<head>
<style>${rrwebStyle}</style>
</head>
<body>
<script>
${rrwebRaw};
/*<!--*/
const events = ${JSON.stringify(events).replace(/<\/script>/g,"<\\/script>")};
/*-->*/
const userConfig = ${config ?JSON.stringify(config):{}};
window.replayer = new rrwebPlayer({
target: document.body,
props: {
events,
showController: false,
autoPlay: false, // autoPlay off by default
...userConfig
},
});
window.replayer.addEventListener('finish', () => window.onReplayFinish());
let time = userConfig.startDelayTime || 1000 // start playback delay time, default 1000ms
let start = fn => {
setTimeout(() => {
fn()
}, time)
}
// It is recommended not to play auto by default. If the speed is not 1, the page block in the early stage of autoPlay will be blank
if (userConfig.autoPlay) {
start = fn => {
fn()
};
}
start(() => {
window.onReplayStart();
window.replayer.play();
})
</script>
</body>
</html>
`;}
type RRvideoConfig ={fps: number;headless: boolean;input: string;cb:(file: string,error:null| Error)=>void;output: string;rrwebPlayer: Omit<RRwebPlayerOptions["props"]& Config,"events">;};constdefaultConfig: RRvideoConfig ={fps:15,headless:true,input:"",cb:()=>{},output:"rrvideo-output.mp4",rrwebPlayer:{},};classRRvideo{private browser!: Browser;private page!: Page;privatestate:"idle"|"recording"|"closed"="idle";privateconfig: RRvideoConfig;constructor(config?: Partial<RRvideoConfig>&{input: string }){this.config ={fps: config?.fps || defaultConfig.fps,headless: config?.headless || defaultConfig.headless,input: config?.input || defaultConfig.input,cb: config?.cb || defaultConfig.cb,output: config?.output || defaultConfig.output,rrwebPlayer: config?.rrwebPlayer || defaultConfig.rrwebPlayer,};}publicasyncinit(){try{// 定义puppeteer 相关配置可以参考:https://zhuanlan.zhihu.com/p/624900686this.browser =await puppeteer.launch({headless:this.config.headless,});// 初始化时创建一个新页面this.page =awaitthis.browser.newPage();awaitthis.page.goto("about:blank");// 页面开始时执行的方法awaitthis.page.exposeFunction("onReplayStart",()=>{this.startRecording();});// 页面结束时执行的方法awaitthis.page.exposeFunction("onReplayFinish",()=>{this.finishRecording();});const eventsPath = path.isAbsolute(this.config.input)?this.config.input
: path.resolve(process.cwd(),this.config.input);const events =JSON.parse(fs.readFileSync(eventsPath,"utf-8"));// 向页面中传参,传入录制的dom数据和配置awaitthis.page.setContent(getHtml(events,this.config.rrwebPlayer));}catch(error){this.config.cb("", error);}}privateasyncstartRecording(){this.state ="recording";let wrapperSelector =".replayer-wrapper";if(this.config.rrwebPlayer.width &&this.config.rrwebPlayer.height){
wrapperSelector =".rr-player";}const wrapperEl =awaitthis.page.$(wrapperSelector);if(!wrapperEl){thrownewError("failed to get replayer element");}// start ffmpegconst args =[// fps"-framerate",this.config.fps.toString(),// input"-f","image2pipe","-i","-",// output"-y",this.config.output,];const ffmpegProcess =spawn("ffmpeg", args);
ffmpegProcess.stderr.setEncoding("utf-8");
ffmpegProcess.stderr.on("data", console.log);letprocessError: Error |null=null;const timer =setInterval(async()=>{if(this.state ==="recording"&&!processError){try{const buffer =await wrapperEl.screenshot({encoding:"binary",});
ffmpegProcess.stdin.write(buffer);}catch(error){// ignore}}else{clearInterval(timer);if(this.state ==="closed"&&!processError){
ffmpegProcess.stdin.end();}}},1000/this.config.fps);const outputPath = path.isAbsolute(this.config.output)?this.config.output
: path.resolve(process.cwd(),this.config.output);
ffmpegProcess.on("close",()=>{if(processError){return;}this.config.cb(outputPath,null);});
ffmpegProcess.on("error",(error)=>{if(processError){return;}
processError = error;this.config.cb(outputPath, error);});
ffmpegProcess.stdin.on("error",(error)=>{if(processError){return;}
processError = error;this.config.cb(outputPath, error);});}privateasyncfinishRecording(){this.state ="closed";awaitthis.browser.close();}}exportfunctiontransformToVideo(config: Partial<RRvideoConfig>&{input: string }): Promise<string>{returnnewPromise((resolve, reject)=>{const rrvideo =newRRvideo({...config,cb(file, error){if(error){returnreject(error);}resolve(file);},});
rrvideo.init();});}
2.4.2、问题一:rrvideo默认转换超时时间为30秒,调整为无超时时间,超时时间通过脚本进行控制
// 在index.ts中的public async init()方法增加awaitthis.page.setDefaultNavigationTimeout(0);
全方法
publicasyncinit(){try{this.browser =await puppeteer.launch({headless:this.config.headless,});this.page =awaitthis.browser.newPage();// 增加设置超时时间为0awaitthis.page.setDefaultNavigationTimeout(0);awaitthis.page.goto("about:blank");awaitthis.page.exposeFunction("onReplayStart",()=>{this.startRecording();});awaitthis.page.exposeFunction("onReplayFinish",()=>{this.finishRecording();});const eventsPath = path.isAbsolute(this.config.input)?this.config.input
: path.resolve(process.cwd(),this.config.input);const events =JSON.parse(fs.readFileSync(eventsPath,"utf-8"));awaitthis.page.setContent(getHtml(events,this.config.rrwebPlayer));}catch(error){this.config.cb("", error);}}
2.4.3、问题二:转换后的视频抖动问题,有白边
白边问题为puppeteer默认打开浏览器的大小与rrvideo播放器的大小冲突导致
puppeteer打开的页面默认的窗口大小是800*600,与rrWeb-player的窗口大小不符合导致的白边问题
// 在index.ts中的public async init()方法增加 窗口可以设置1280*720 或者1920*1080awaitthis.page.setViewport({width:1280,height:720,deviceScaleFactor:1});
此大小需要与rrvideo的rrvideo.config配置文件相同
rrvideo.config
{
"width":1280,
"height":720,
"speed": 1,
"skipInactive": true,
"mouseTail": {
"strokeStyle": "green",
"lineWidth": 2
},
"startDelayTime": 1000
}
全方法
publicasyncinit(){try{this.browser =await puppeteer.launch({headless:this.config.headless,});this.page =awaitthis.browser.newPage();// 增加设置超时时间为0awaitthis.page.setDefaultNavigationTimeout(0);// 设置puppeteer打开谷歌浏览器的大小awaitthis.page.setViewport({width:1280,height:720,deviceScaleFactor:1});awaitthis.page.goto("about:blank");awaitthis.page.exposeFunction("onReplayStart",()=>{this.startRecording();});awaitthis.page.exposeFunction("onReplayFinish",()=>{this.finishRecording();});const eventsPath = path.isAbsolute(this.config.input)?this.config.input
: path.resolve(process.cwd(),this.config.input);const events =JSON.parse(fs.readFileSync(eventsPath,"utf-8"));awaitthis.page.setContent(getHtml(events,this.config.rrwebPlayer));}catch(error){this.config.cb("", error);}}
2.4.4、问题三:视频转换速度和清晰度问题
转换速度
// 在index.ts中的RRvideoConfig中,将fps由10改为15constdefaultConfig: RRvideoConfig ={fps:15,headless:true,input:"",cb:()=>{},output:"rrvideo-output.mp4",rrwebPlayer:{},};
清晰度
// 在index.ts中的startRecording方法,调整ffmpeg的配置// 原const args =[// fps"-framerate",this.config.fps.toString(),// input"-f","image2pipe","-i","-",// output"-y",this.config.output,];// 改为
args =[// fps"-framerate",this.config.fps.toString(),// input"-f","image2pipe","-i","-",// output"-y","-b:v","2000k",this.config.output,];
ffmpeg配置含义参考:
2.4.5、更改index.ts如何在服务器上生效
本地修改完index.ts代码后,需要将其编译为js文件,上传到服务器中
上传路径:nodejs目录下安装的rrvideo下build文件夹
以测试环境为例:/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/build/index.js
将本地反编译的文件替换上面目录的index.js即可
.ts转为js
ts文件是TypeScript (一种JavaScript的超集)文件,要将其编译成,js文件,你需要使用TypeScript编译器.以下是使用TypeScript编译器编译.ts文件的步骤:
- 1.安装TypeScript: 首先,确保你的系统上安装了Node.s。然后,在终端或命令提示符中运行以下命令今来全局安装TypeScript:
npm instal1 -g typescript
这将安装TypeScript编译器并在你的系统上设置一个类型化的命令tsc。
- 2.编译ts文件:在终端或命令提示符中,导航到包含ts文件的目录,并运行以下命今来编译.ts文件
tsc index.ts
2.4.6、rrvideo 录制弹窗展示不全,展示多个等问题
只要你使用rrWeb-player播放的内容和最终转换视频后的内容不一致,都是此问题导致
此问题的原因为rrvideo的版本没有人维护了,所使用的rrWeb-player版本很低,所以播放的时候有很多问题
找到你的rrvideo的位置,打开package.json查看rrWeb-player版本
cat /opt/nodejs/node14.17.6/lib/node_modules/rrvideo/package.json (你的rrvideo安装的地址)
{"_from":"rrvideo","_id":"[email protected]","_inBundle":false,"_integrity":"sha512-EumIkBkXq+C2Ki6MKXYH3bxik5kTnZWn1IO6YmdJrLXHqgoPla7XUib0HpITE8UevFMq8xufXuo0ElHdwD5AZQ==","_location":"/rrvideo","_phantomChildren":{},"_requested":{"type":"tag","registry":true,"raw":"rrvideo","name":"rrvideo","escapedName":"rrvideo","rawSpec":"","saveSpec":null,"fetchSpec":"latest"},"_requiredBy":["#USER"],"_resolved":"https://registry.npmjs.org/rrvideo/-/rrvideo-0.2.1.tgz","_shasum":"8849ead66853621884e21d3e254f33e18ca93378","_spec":"rrvideo","_where":"/opt/nodejs/node14.17.6/lib","author":{"name":"[email protected]"},"bin":{"rrvideo":"build/cli.js"},"bundleDependencies":false,"dependencies":{"@types/minimist":"^1.2.1","@types/puppeteer":"^5.4.0","minimist":"^1.2.5","puppeteer":"^5.4.1","rrweb-player":"^0.6.5","typescript":"^4.0.5"},"deprecated":false,"description":"transform rrweb session into video","files":["build"],"license":"MIT","main":"build/index.js","name":"rrvideo","scripts":{"build":"tsc","prepublish":"yarn build","test":"test"},"version":"0.2.1"}
可以看到,rrvideo引用的rrWeb-player版本为0.6.5
修改此内容为你的rrWeb-player对应版本,我的是1.0.0-alpha.4
修改后执行
# 在/opt/nodejs/node14.17.6/lib/node_modules/rrvideo/下执行npminstall# 若下载太慢,切换淘宝镜像(我太贴心了)npm config set registry https://registry.npm.taobao.org
执行后重新转换视频,转换的视频和本地使用rrWeb-player播放的效果一致
吐槽:rrvideo好像很长时间没有人维护了,一堆问题,按照我的方式将它的代码优化后,基本使用时完全没有问题的。github上还有很多人的提问都没有回答,感觉是没人维护了,所以这些改动代码我没有提上去,大家有时间的可以将改好的代码提交上去,或者回答下github上的问题
2.5、rrvideo完整修改后代码
操作步骤
- 按照2.4.2、2.4.3、2.4.4、2.4.6更改下载的rrvideo源码中的index.ts
- 按照2.4.5生成对应的index.js
- 将生成的index.js替换服务器安装的rrvideo对应的index.js
rrvideo中index.js地址
#找到你安装的rrvideo地址# 以下是我的地址[root@test-webapp-svr20 rrvideo]# cd /opt/nodejs/node14.17.6/lib/node_modules/rrvideo[root@test-webapp-svr20 rrvideo]# ll
total 44
-rw-r--r-- 1 root root 978 Oct 261985 README.md
-rw-r--r-- 1 root root 971 Oct 261985 README.zh_CN.md
drwxr-xr-x 2 root root 4096 Dec 1316:42 build
drwxr-xr-x 72 root root 4096 Dec 1415:21 node_modules
-rw-r--r-- 1 root root 22561 Dec 1415:21 package-lock.json
-rw-r--r-- 1 root root 1307 Dec 1415:16 package.json
[root@test-webapp-svr20 rrvideo]# cd build[root@test-webapp-svr20 rrvideo]# vim index.js
下面分享我修改好后的index.js,替换掉原rrvideo的即可,喜欢的可以点个收藏,谢谢~
"use strict";var __assign =(this&&this.__assign)||function(){
__assign = Object.assign ||function(t){for(var s, i =1, n = arguments.length; i < n; i++){
s = arguments[i];for(var p in s)if(Object.prototype.hasOwnProperty.call(s, p))
t[p]= s[p];}return t;};return__assign.apply(this, arguments);};var __createBinding =(this&&this.__createBinding)||(Object.create ?(function(o, m, k, k2){if(k2 ===undefined) k2 = k;
Object.defineProperty(o, k2,{enumerable:true,get:function(){return m[k];}});}):(function(o, m, k, k2){if(k2 ===undefined) k2 = k;
o[k2]= m[k];}));var __setModuleDefault =(this&&this.__setModuleDefault)||(Object.create ?(function(o, v){
Object.defineProperty(o,"default",{enumerable:true,value: v });}):function(o, v){
o["default"]= v;});var __importStar =(this&&this.__importStar)||function(mod){if(mod && mod.__esModule)return mod;var result ={};if(mod !=null)for(var k in mod)if(k !=="default"&&Object.prototype.hasOwnProperty.call(mod, k))__createBinding(result, mod, k);__setModuleDefault(result, mod);return result;};var __awaiter =(this&&this.__awaiter)||function(thisArg, _arguments,P, generator){functionadopt(value){return value instanceofP? value :newP(function(resolve){resolve(value);});}returnnew(P||(P= Promise))(function(resolve, reject){functionfulfilled(value){try{step(generator.next(value));}catch(e){reject(e);}}functionrejected(value){try{step(generator["throw"](value));}catch(e){reject(e);}}functionstep(result){ result.done ?resolve(result.value):adopt(result.value).then(fulfilled, rejected);}step((generator =generator.apply(thisArg, _arguments ||[])).next());});};var __generator =(this&&this.__generator)||function(thisArg, body){var _ ={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1];},trys:[],ops:[]}, f, y, t, g;return g ={next:verb(0),"throw":verb(1),"return":verb(2)},typeof Symbol ==="function"&&(g[Symbol.iterator]=function(){returnthis;}), g;functionverb(n){returnfunction(v){returnstep([n, v]);};}functionstep(op){if(f)thrownewTypeError("Generator is already executing.");while(_)try{if(f =1, y &&(t = op[0]&2? y["return"]: op[0]? y["throw"]||((t = y["return"])&&t.call(y),0): y.next)&&!(t =t.call(y, op[1])).done)return t;if(y =0, t) op =[op[0]&2, t.value];switch(op[0]){case0:case1: t = op;break;case4: _.label++;return{value: op[1],done:false};case5: _.label++; y = op[1]; op =[0];continue;case7: op = _.ops.pop(); _.trys.pop();continue;default:if(!(t = _.trys, t = t.length >0&& t[t.length -1])&&(op[0]===6|| op[0]===2)){ _ =0;continue;}if(op[0]===3&&(!t ||(op[1]> t[0]&& op[1]< t[3]))){ _.label = op[1];break;}if(op[0]===6&& _.label < t[1]){ _.label = t[1]; t = op;break;}if(t && _.label < t[2]){ _.label = t[2]; _.ops.push(op);break;}if(t[2]) _.ops.pop();
_.trys.pop();continue;}
op =body.call(thisArg, _);}catch(e){ op =[6, e]; y =0;}finally{ f = t =0;}if(op[0]&5)throw op[1];return{value: op[0]? op[1]:void0,done:true};}};var __importDefault =(this&&this.__importDefault)||function(mod){return(mod && mod.__esModule)? mod :{"default": mod };};
Object.defineProperty(exports,"__esModule",{value:true});
exports.transformToVideo =void0;var fs =__importStar(require("fs"));var path =__importStar(require("path"));var child_process_1 =require("child_process");var puppeteer_1 =__importDefault(require("puppeteer"));var rrwebScriptPath = path.resolve(require.resolve("rrweb-player"),"../../dist/index.js");var rrwebStylePath = path.resolve(rrwebScriptPath,"../style.css");var rrwebRaw = fs.readFileSync(rrwebScriptPath,"utf-8");var rrwebStyle = fs.readFileSync(rrwebStylePath,"utf-8");functiongetHtml(events, config){return"\n<html>\n <head>\n <style>"+ rrwebStyle +"</style>\n </head>\n <body>\n <script>\n "+ rrwebRaw +";\n /*<!--*/\n const events = "+JSON.stringify(events).replace(/<\/script>/g,"<\\/script>")+";\n /*-->*/\n const userConfig = "+(config ?JSON.stringify(config):{})+";\n window.replayer = new rrwebPlayer({\n target: document.body,\n props: {\n events,\n showController: false,\n ...userConfig\n },\n });\n window.onReplayStart();\n window.replayer.play();\n window.replayer.addEventListener('finish', () => window.onReplayFinish());\n </script>\n </body>\n</html>\n";}var defaultConfig ={fps:10,headless:true,input:"",cb:function(){},output:"rrvideo-output.mp4",rrwebPlayer:{},};var RRvideo =/** @class */(function(){functionRRvideo(config){this.state ="idle";this.config ={fps:(config ===null|| config ===void0?void0: config.fps)|| defaultConfig.fps,headless:(config ===null|| config ===void0?void0: config.headless)|| defaultConfig.headless,input:(config ===null|| config ===void0?void0: config.input)|| defaultConfig.input,cb:(config ===null|| config ===void0?void0: config.cb)|| defaultConfig.cb,output:(config ===null|| config ===void0?void0: config.output)|| defaultConfig.output,rrwebPlayer:(config ===null|| config ===void0?void0: config.rrwebPlayer)|| defaultConfig.rrwebPlayer,};}RRvideo.prototype.init=function(){return__awaiter(this,void0,void0,function(){var _a, _b, eventsPath, events, error_1;var _this =this;return__generator(this,function(_c){switch(_c.label){case0:
_c.trys.push([0,8,,9]);
_a =this;return[4/*yield*/, puppeteer_1.default.launch({headless:this.config.headless,defaultViewport:{width:1920,height:1080,},})];case1:
_a.browser = _c.sent();
_b =this;return[4/*yield*/,this.browser.newPage()];case2:
_b.page = _c.sent();return[4/*yield*/,this.page.setDefaultNavigationTimeout(0)];case3:
_c.sent();return[4/*yield*/,this.page.goto("about:blank")];case4:
_c.sent();return[4/*yield*/,this.page.exposeFunction("onReplayStart",function(){
_this.startRecording();})];case5:
_c.sent();return[4/*yield*/,this.page.exposeFunction("onReplayFinish",function(){
_this.finishRecording();})];case6:
_c.sent();
eventsPath = path.isAbsolute(this.config.input)?this.config.input
: path.resolve(process.cwd(),this.config.input);
events =JSON.parse(fs.readFileSync(eventsPath,"utf-8"));return[4/*yield*/,this.page.setContent(getHtml(events,this.config.rrwebPlayer))];case7:
_c.sent();return[3/*break*/,9];case8:
error_1 = _c.sent();this.config.cb("", error_1);return[3/*break*/,9];case9:return[2/*return*/];}});});};RRvideo.prototype.startRecording=function(){return__awaiter(this,void0,void0,function(){var wrapperSelector, wrapperEl, args, ffmpegProcess, processError, timer, outputPath;var _this =this;return__generator(this,function(_a){switch(_a.label){case0:this.state ="recording";
wrapperSelector =".replayer-wrapper";if(this.config.rrwebPlayer.width &&this.config.rrwebPlayer.height){
wrapperSelector =".rr-player";//wrapperSelector = ".replayer-wrapper";}return[4/*yield*/,this.page.$(wrapperSelector)];case1:
wrapperEl = _a.sent();if(!wrapperEl){thrownewError("failed to get replayer element");}
args =[// fps"-framerate",this.config.fps.toString(),// input"-f","image2pipe","-i","-",// output"-y",//"-qscale",//"1","-b:v","2000k",// "-s",//"1280x720", this.config.output,];
ffmpegProcess = child_process_1.spawn("ffmpeg", args);
ffmpegProcess.stderr.setEncoding("utf-8");
ffmpegProcess.stderr.on("data", console.log);
processError =null;
timer =setInterval(function(){return__awaiter(_this,void0,void0,function(){var buffer, error_2;return__generator(this,function(_a){switch(_a.label){case0:if(!(this.state ==="recording"&&!processError))return[3/*break*/,5];
_a.label =1;case1:
_a.trys.push([1,3,,4]);return[4/*yield*/, wrapperEl.screenshot({encoding:"binary",})];case2:
buffer = _a.sent();
ffmpegProcess.stdin.write(buffer);return[3/*break*/,4];case3:
error_2 = _a.sent();return[3/*break*/,4];case4:return[3/*break*/,6];case5:clearInterval(timer);if(this.state ==="closed"&&!processError){
ffmpegProcess.stdin.end();}
_a.label =6;case6:return[2/*return*/];}});});},1000/this.config.fps);
outputPath = path.isAbsolute(this.config.output)?this.config.output
: path.resolve(process.cwd(),this.config.output);
ffmpegProcess.on("close",function(){if(processError){return;}
_this.config.cb(outputPath,null);});
ffmpegProcess.on("error",function(error){if(processError){return;}
processError = error;
_this.config.cb(outputPath, error);});
ffmpegProcess.stdin.on("error",function(error){if(processError){return;}
processError = error;
_this.config.cb(outputPath, error);});return[2/*return*/];}});});};RRvideo.prototype.finishRecording=function(){return__awaiter(this,void0,void0,function(){return__generator(this,function(_a){switch(_a.label){case0:this.state ="closed";return[4/*yield*/,this.browser.close()];case1:
_a.sent();return[2/*return*/];}});});};return RRvideo;}());functiontransformToVideo(config){returnnewPromise(function(resolve, reject){var rrvideo =newRRvideo(__assign(__assign({}, config),{cb:function(file, error){if(error){returnreject(error);}resolve(file);}}));
rrvideo.init();});}
exports.transformToVideo = transformToVideo;
版权归原作者 万物~ 所有, 如有侵权,请联系我们删除。