小工具知识网

当前位置: 首页 > 行业知识 > rrWeb可回溯录制+rrvideo转视频整体方案实施

行业知识

rrWeb可回溯录制+rrvideo转视频整体方案实施

2026-11-13 18:00:00 来源:互联网转载

rrWeb整体方案

1、整体流程

1.1、流程图

生产负载调用rrWeb情况

前端调用保存录制使用固定地址,调用预生产服务进行保存

定时任务执行,设置地址配置为预生产地址

1.2、功能说明

1.2.1 录制保存功能

  • 地址

    /rrWeb/saveRrWeb

    • 功能

      1、首次录制,生成本次录制的唯一id

      2、同一订单可能存在多个录制id

      3、录制id在当前录制流程中唯一,关闭录制或关闭浏览器重新进入开始录制,会生成新的id

      4、使用Redis记录录制id每次片段的顺序

      5、保存数据到backTrackRecord、backTrackVideo表

      6、保存录制数据到服务器中,保存目录/data/service/rrWeb/sava/录制id/片段数据

      7、/data/service/rrWeb目录在sys_config表中配置

      1.2.2 定时合并数据

      • 地址

        /job/convertVideo

        • 功能

          1、查询backTrackRecord表中未合并过的数据

          2、获取对应录制id

          3、根据录制id获取/data/service/rrWeb/sava/录制id/目录下所有的片段数据

          4、根据片段录制顺序,读取文件内容,写到/data/service/rrWeb/录制id.txt文件下

          5、更新backTrackRecord状态

          1.2.3 Linux下定时转换视频

          • 地址

            无

            • 功能

              1、将/data/service/rrWeb/下对应的文件备份到history目录

              2、使用rrvideo命令,将对应文件转换为视频,保存到/data/service/rrWeb/mp4下

              3、转换过程超过2分钟的停止

              4、转换完成删除/data/service/rrWeb/下对应文件

              • 配置脚本执行
                # 打开定时任务配置文件crontab -e# 在配置文件中写入定时任务的操作, 这里就是指定每天1点10分定时执行脚本,并把执行脚本的日志写入文件 crontabLoad.log10 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/rrWebOUTPUT_DIR=/data/service/rrWeb/mp4HISTORY_DIR=/data/service/rrWeb/historyTIMEOUT=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" ]; then  for f in $INPUT_DIR/*.txt; do    filename=$(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=$TIMEOUT    while ps -p $pid > /dev/null; do      sleep 1       timeout=$((timeout-1))      if [ $timeout -eq 0 ]; then        kill -9 $pid          echo "killed ${filename}"         break      fi    done    if [ $timeout -ne 0 ]; then        # 删除txt文件        rm "$f"    fi    echo "Finished $f to $filename.mp4"  done    else         echo "没有要转换的文件"        fi# 关闭无头浏览器进程PID=`ps -ef | grep puppeteer | awk '{print }'`echo "得到进程ID:${PID}"echo "结束进程"for id in ${PID}do        kill -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

                    • 功能

                      1、查询backTrackVideo表中未上传OBS的数据

                      2、3天内还未上传到OBS,进行告警

                      3、根据录制id获取/data/service/rrWeb/map目录下的视频文件

                      4、获取视频流,上传OBS

                      5、 更新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## 赋予执行权限chmod 777 node14.17.6/# 配置环境变量vim /etc/profile# 在文件中增加配置,保存后退出export NODE_HOME=/opt/nodejs/node14.17.6export PATH=$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 ffmpegcd ffmpeg# 查看版本./ffmpeg -version# 配置环境变量vim /etc/profile# 在文件中增加配置,保存后退出export PATH=$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# 安装依赖npm install# 安装成功后执行命令转换视频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 directoryTROUBLESHOOTING: 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. (/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 -yyum update nss -yyum install libX11-devel --nogpgyum 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 directoryTROUBLESHOOTING: 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. (/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.1mesa-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.1libpcre-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");interface Config {  // start playback delay time  startDelayTime?: number,} function getHtml(  events: Array,  config?: Omit): string {  return `    ${rrwebStyle}              ${rrwebRaw};      /*JSON.stringify(events).replace(        //g,        ""      )};      /*-->*/      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();      })      `;}type RRvideoConfig = {  fps: number;  headless: boolean;  input: string;  cb: (file: string, error: null | Error) => void;  output: string;  rrwebPlayer: Omit;};const defaultConfig: RRvideoConfig = {  fps: 15,  headless: true,  input: "",  cb: () => {},  output: "rrvideo-output.mp4",  rrwebPlayer: {},};class RRvideo {  private browser!: Browser;  private page!: Page;  private state: "idle" | "recording" | "closed" = "idle";  private config: RRvideoConfig;  constructor(config?: Partial & { 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,    };  }  public async init() {    try {        // 定义puppeteer 相关配置可以参考:https://zhuanlan.zhihu.com/p/624900686      this.browser = await puppeteer.launch({        headless: this.config.headless,      });        // 初始化时创建一个新页面      this.page = await this.browser.newPage();      await this.page.goto("about:blank");   // 页面开始时执行的方法      await this.page.exposeFunction("onReplayStart", () => {        this.startRecording();      });      // 页面结束时执行的方法      await this.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数据和配置      await this.page.setContent(getHtml(events, this.config.rrwebPlayer));    } catch (error) {      this.config.cb("", error);    }  }  private async startRecording() {    this.state = "recording";    let wrapperSelector = ".replayer-wrapper";    if (this.config.rrwebPlayer.width && this.config.rrwebPlayer.height) {      wrapperSelector = ".rr-player";    }    const wrapperEl = await this.page.$(wrapperSelector);    if (!wrapperEl) {      throw new Error("failed to get replayer element");    }        // start ffmpeg    const 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);    let processError: 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);    });  }  private async finishRecording() {    this.state = "closed";    await this.browser.close();  }}export function transformToVideo(  config: Partial & { input: string }): Promise {  return new Promise((resolve, reject) => {    const rrvideo = new RRvideo({      ...config,      cb(file, error) {        if (error) {          return reject(error);        }        resolve(file);      },    });    rrvideo.init();  });}
                      2.4.2、问题一:rrvideo默认转换超时时间为30秒,调整为无超时时间,超时时间通过脚本进行控制
                      // 在index.ts中的public async init()方法增加await this.page.setDefaultNavigationTimeout(0);

                      全方法

                        public async init() {    try {      this.browser = await puppeteer.launch({        headless: this.config.headless,      });      this.page = await this.browser.newPage();      // 增加设置超时时间为0  await this.page.setDefaultNavigationTimeout(0);      await this.page.goto("about:blank");      await this.page.exposeFunction("onReplayStart", () => {        this.startRecording();      });      await this.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"));      await this.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*1080await this.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}

                      全方法

                        public async init() {    try {      this.browser = await puppeteer.launch({        headless: this.config.headless,      });      this.page = await this.browser.newPage();      // 增加设置超时时间为0  await this.page.setDefaultNavigationTimeout(0);  // 设置puppeteer打开谷歌浏览器的大小  await this.page.setViewport({width: 1280,height: 720,deviceScaleFactor: 1});      await this.page.goto("about:blank");      await this.page.exposeFunction("onReplayStart", () => {        this.startRecording();      });      await this.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"));      await this.page.setContent(getHtml(events, this.config.rrwebPlayer));    } catch (error) {      this.config.cb("", error);    }  }
                      2.4.4、问题三:视频转换速度和清晰度问题

                      转换速度

                      // 在index.ts中的RRvideoConfig中,将fps由10改为15const defaultConfig: 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配置含义参考:

                      https://zhuanlan.zhihu.com/p/145312133

                      https://wenku.baidu.com/view/45b9f4a51a5f312b3169a45177232f60dccce749.html?wkts=1701760052153&bdQuery=ffmpeg+%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE-s

                      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": "rrvideo@0.2.1",  "_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": "yanzhen@smartx.com"  },  "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/下执行npm install# 若下载太慢,切换淘宝镜像(我太贴心了)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]# lltotal 44-rw-r--r--  1 root root   978 Oct 26  1985 README.md-rw-r--r--  1 root root   971 Oct 26  1985 README.zh_CN.mddrwxr-xr-x  2 root root  4096 Dec 13 16:42 builddrwxr-xr-x 72 root root  4096 Dec 14 15:21 node_modules-rw-r--r--  1 root root 22561 Dec 14 15:21 package-lock.json-rw-r--r--  1 root root  1307 Dec 14 15: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  0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1]  window.onReplayFinish());\n    \n  \n\n";}var defaultConfig = {    fps: 10,    headless: true,    input: "",    cb: function () { },    output: "rrvideo-output.mp4",    rrwebPlayer: {},};var RRvideo = /** @class */ (function () {    function RRvideo(config) {        this.state = "idle";        this.config = {            fps: (config === null || config === void 0 ? void 0 : config.fps) || defaultConfig.fps,            headless: (config === null || config === void 0 ? void 0 : config.headless) || defaultConfig.headless,            input: (config === null || config === void 0 ? void 0 : config.input) || defaultConfig.input,            cb: (config === null || config === void 0 ? void 0 : config.cb) || defaultConfig.cb,            output: (config === null || config === void 0 ? void 0 : config.output) || defaultConfig.output,            rrwebPlayer: (config === null || config === void 0 ? void 0 : config.rrwebPlayer) || defaultConfig.rrwebPlayer,        };    }    RRvideo.prototype.init = function () {        return __awaiter(this, void 0, void 0, function () {            var _a, _b, eventsPath, events, error_1;            var _this = this;            return __generator(this, function (_c) {                switch (_c.label) {                    case 0:                        _c.trys.push([0, 8, , 9]);                        _a = this;                        return [4 /*yield*/, puppeteer_1.default.launch({                                headless: this.config.headless,                                defaultViewport: {                                   width: 1920,                                   height: 1080,                                },                            })];                    case 1:                        _a.browser = _c.sent();                        _b = this;                        return [4 /*yield*/, this.browser.newPage()];                    case 2:                         _b.page = _c.sent();                        return [4 /*yield*/, this.page.setDefaultNavigationTimeout(0)];                    case 3:                         _c.sent();                         return [4 /*yield*/, this.page.goto("about:blank")];                    case 4:                        _c.sent();                        return [4 /*yield*/, this.page.exposeFunction("onReplayStart", function () {                                _this.startRecording();                            })];                    case 5:                        _c.sent();                        return [4 /*yield*/, this.page.exposeFunction("onReplayFinish", function () {                                _this.finishRecording();                            })];                    case 6:                        _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))];                    case 7:                        _c.sent();                        return [3 /*break*/, 9];                    case 8:                        error_1 = _c.sent();                        this.config.cb("", error_1);                        return [3 /*break*/, 9];                    case 9: return [2 /*return*/];                }            });        });    };    RRvideo.prototype.startRecording = function () {        return __awaiter(this, void 0, void 0, function () {            var wrapperSelector, wrapperEl, args, ffmpegProcess, processError, timer, outputPath;            var _this = this;            return __generator(this, function (_a) {                switch (_a.label) {                    case 0:                        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)];                    case 1:                        wrapperEl = _a.sent();                        if (!wrapperEl) {                            throw new Error("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, void 0, void 0, function () {                            var buffer, error_2;                            return __generator(this, function (_a) {                                switch (_a.label) {                                    case 0:                                        if (!(this.state === "recording" && !processError)) return [3 /*break*/, 5];                                        _a.label = 1;                                    case 1:                                        _a.trys.push([1, 3, , 4]);                                        return [4 /*yield*/, wrapperEl.screenshot({ encoding: "binary",                                            })];                                    case 2:                                        buffer = _a.sent();                                        ffmpegProcess.stdin.write(buffer);                                        return [3 /*break*/, 4];                                    case 3:                                        error_2 = _a.sent();                                        return [3 /*break*/, 4];                                    case 4: return [3 /*break*/, 6];                                    case 5:                                        clearInterval(timer);                                        if (this.state === "closed" && !processError) {                                            ffmpegProcess.stdin.end();                                        }                                        _a.label = 6;                                    case 6: 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, void 0, void 0, function () {            return __generator(this, function (_a) {                switch (_a.label) {                    case 0:                        this.state = "closed";                        return [4 /*yield*/, this.browser.close()];                    case 1:                        _a.sent();                        return [2 /*return*/];                }            });        });    };    return RRvideo;}());function transformToVideo(config) {    return new Promise(function (resolve, reject) {        var rrvideo = new RRvideo(__assign(__assign({}, config), { cb: function (file, error) {                if (error) {                    return reject(error);                }                resolve(file);            } }));        rrvideo.init();    });}exports.transformToVideo = transformToVideo;

上一篇:Spring MyBatis源码深度解析,揭秘工作原理与核心技术

下一篇:static方法怎么获取服务器地址?