找回密码
 立即注册
首页 业界区 业界 通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显 ...

通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次

阮蓄 2025-6-6 15:03:36
效果预览

视频画面
1.png

网络请求
2.png

代码实现

ZLMRTCClient.js

当前使用的版本:
1.0.1 Mon Mar 27 2023 19:11:59 GMT+0800
首先需要修改 ZLMRTCClient.js 的代码,解决由于网络导致播放失败时无法触发 WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED 事件的问题。
修改前:
3.png

修改后:
4.png

修改内容:
5.png

6.png
  1. // 添加 catch()
  2. axios({
  3. }).then(() => {
  4. }).catch(() => {
  5.   // 网络异常时触发事件
  6.   this.dispatch(Events$1.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, null);
  7. });
复制代码
video-preview.js
  1. // 2024-05-30 初始版本
  2. // 2024-06-06 优化视频是否存在调用检测方式
  3. // 2025-01-08 优化逻辑,减少定时器的使用
  4. import { v4 as uuidv4 } from 'uuid';
  5. /**
  6. * @typedef  CacheItem
  7. * @property {string}           id         缓存项唯一 ID
  8. * @property {HTMLElement|null} element    Video 元素
  9. * @property {boolean}          isStopped  是否为主动停止播放
  10. * @property {ZLMPlayer|null}   player     ZLM 播放器对象
  11. * @property {number}           timeCheck  最后一次检测关联画布的时间戳
  12. * @property {number}           timeResize 最后一次更新分辨率的时间戳
  13. * @property {number}           timeRender 最后一次渲染的时间戳
  14. * @property {boolean}          willStop   是否没有关联的画布,在下一次停止播放
  15. */
  16. /** @typedef {InstanceType<typeof ZLMRTCClient.Endpoint>} ZLMPlayer */
  17. /** 检测视频是否存在调用间隔 */
  18. const INTERVAL_CHECK_VIDEO = 10000;
  19. /** 画布渲染间隔 */
  20. const INTERVAL_RENDER = 100;
  21. /** 画布分辨率更新间隔 */
  22. const INTERVAL_RESIZE = 1000;
  23. /** 循环处理间隔 */
  24. const INTERVAL_TIME = 100;
  25. /** 模块名称 */
  26. const PREFIX = '[video-preview]';
  27. /** 重新播放间隔 */
  28. const RESTART_TIMEOUT = 2000;
  29. /** ZLM 客户端 */
  30. const ZLMRTCClient = window.ZLMRTCClient;
  31. /** 循环检测定时器 */
  32. let loopTimer = null;
  33. /**
  34. * @desc 缓存信息列表
  35. * @type {Record<string, CacheItem | null>}
  36. */
  37. export const cacheList = {};
  38. /**
  39. * @description 初始化播放器
  40. * @param {string} url 视频流地址
  41. */
  42. function initPlayer(url = '') {
  43.   try {
  44.     if (!url) {
  45.       throw new Error('缺少 url 参数');
  46.     }
  47.     /**
  48.      * @description 初始化 & 更新数据
  49.      * @param {CacheItem} cache
  50.      */
  51.     let fnInit = (cache) => {
  52.       // 创建 video 元素
  53.       let element = document.createElement('video');
  54.       // 开启自动播放
  55.       // 注:不能用 `setAttribute`,否则没效果
  56.       element.autoplay = true;
  57.       element.controls = false;
  58.       element.muted = true;
  59.       // 标记缓存 ID
  60.       element.setAttribute('data-video-id', cache.id);
  61.       // 添加到页面,否则无法播放
  62.       element.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0');
  63.       document.body.appendChild(element);
  64.       // 初始化播放器
  65.       let player = new ZLMRTCClient.Endpoint({
  66.         // video 标签
  67.         element: element,
  68.         // 是否打印日志
  69.         debug: false,
  70.         // 流地址
  71.         zlmsdpUrl: url,
  72.         // 功能开关
  73.         audioEnable: false,
  74.         simulcast: false,
  75.         useCamera: false,
  76.         videoEnable: true,
  77.         // 仅查看,不推流
  78.         recvOnly: true,
  79.         // 推流分辨率
  80.         resolution: { w: 1280, h: 720 },
  81.         // 文本收发
  82.         // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send
  83.         usedatachannel: false,
  84.       });
  85.       // // 监听事件:ICE 协商出错
  86.       // player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function () {
  87.       //   console.error(PREFIX, 'ICE 协商出错')
  88.       // });
  89.       // 监听事件:获取到了远端流,可以播放
  90.       player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function (event) {
  91.         console.log(PREFIX, '播放成功', event.streams);
  92.       });
  93.       // 监听事件:offer anwser 交换失败
  94.       player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, function (event) {
  95.         console.error(PREFIX, 'offer anwser 交换失败', event);
  96.         // 当前没有主动停止
  97.         if (!cache.isStopped) {
  98.           // 停止播放
  99.           stopPlayer(player, element);
  100.           // 重新播放
  101.           setTimeout(() => {
  102.             fnInit(cache);
  103.           }, RESTART_TIMEOUT);
  104.         }
  105.       });
  106.       // 监听事件:RTC 状态变化
  107.       player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, function (state) {
  108.         console.log(PREFIX, 'RTC 状态变化', state);
  109.         // 状态为已断开
  110.         if (state === 'disconnected' && !cache.isStopped) {
  111.           // 停止播放
  112.           stopPlayer(player, element);
  113.           // 重新播放
  114.           setTimeout(() => {
  115.             fnInit(cache);
  116.           }, RESTART_TIMEOUT);
  117.         }
  118.       });
  119.       cache.element = element;
  120.       cache.player = player;
  121.     };
  122.     let cacheItem = cacheList[url];
  123.     if (cacheItem) {
  124.       return cacheItem;
  125.     } else {
  126.       cacheItem = {
  127.         id: uuidv4(),
  128.         element: null,
  129.         isStopped: false,
  130.         player: null,
  131.         timeCheck: 0,
  132.         timeRender: 0,
  133.         timeResize: 0,
  134.         willStop: false,
  135.       };
  136.     }
  137.     console.log(PREFIX, '初始化', cacheItem);
  138.     // 初始化
  139.     fnInit(cacheItem);
  140.     // 添加缓存信息
  141.     cacheList[url] = cacheItem;
  142.     return cacheItem;
  143.   } catch (error) {
  144.     console.error(PREFIX, '初始化播放器失败:');
  145.     console.error(error);
  146.     return null;
  147.   }
  148. }
  149. /**
  150. * @description 停止播放
  151. * @param {ZLMPlayer}        player
  152. * @param {HTMLVideoElement} element
  153. */
  154. function stopPlayer(player, element) {
  155.   try {
  156.     if (player) {
  157.       console.debug(PREFIX, 'stopPlayer - 停止播放');
  158.       player.close();
  159.     }
  160.     if (element instanceof HTMLVideoElement) {
  161.       console.debug(PREFIX, 'stopPlayer - 移除元素');
  162.       element.remove();
  163.     }
  164.     return true;
  165.   } catch (error) {
  166.     console.error(PREFIX, '停止播放失败:');
  167.     console.error(error);
  168.     return false;
  169.   }
  170. }
  171. /**
  172. * @description 获取视频画面 canvas
  173. * @param {string} url
  174. */
  175. export function getVideoCanvas(url = '') {
  176.   try {
  177.     if (!url) {
  178.       throw new Error('缺少 url 参数');
  179.     }
  180.     let cacheItem = initPlayer(url);
  181.     let canvas = document.createElement('canvas');
  182.     if (cacheItem) {
  183.       // 标记缓存 ID
  184.       canvas.setAttribute('data-cache-id', cacheItem.id);
  185.     } else {
  186.       throw new Error('获取缓存数据失败');
  187.     }
  188.     // 背景填充
  189.     canvas.style.backgroundPosition = 'center center';
  190.     canvas.style.backgroundSize = '100% 100%';
  191.     return canvas;
  192.   } catch (error) {
  193.     console.error(PREFIX, '获取 canvas 失败:');
  194.     console.error(error);
  195.     return null;
  196.   }
  197. }
  198. /** 开始循环处理视频 */
  199. export function timerStart() {
  200.   timerStop();
  201.   loopTimer = setInterval(() => {
  202.     for (let url in cacheList) {
  203.       let cacheItem = cacheList[url];
  204.       let currTime = Date.now();
  205.       if (!cacheItem) {
  206.         continue;
  207.       }
  208.       let cacheId = cacheItem.id;
  209.       let videoElement = cacheItem.element;
  210.       /**
  211.        * @desc 画布元素列表
  212.        * @type {NodeListOf<HTMLCanvasElement>}
  213.        */
  214.       let canvasList = document.querySelectorAll(`[data-cache-id="${cacheId}"]`);
  215.       let foundCanvas = canvasList.length > 0;
  216.       // 渲染画面
  217.       if (currTime - cacheItem.timeRender >= INTERVAL_RENDER) {
  218.         cacheItem.timeRender = currTime;
  219.         canvasList.forEach((canvas) => {
  220.           let ctx = canvas.getContext('2d');
  221.           let cWidth = canvas.width;
  222.           let cHeight = canvas.height;
  223.           if (document.contains(videoElement)) {
  224.             ctx.drawImage(videoElement, 0, 0, cWidth, cHeight);
  225.           }
  226.           canvas.style.backgroundImage = '';
  227.         });
  228.       }
  229.       // 更新画布分辨率
  230.       if (currTime - cacheItem.timeResize >= INTERVAL_RESIZE) {
  231.         cacheItem.timeResize = currTime;
  232.         canvasList.forEach((canvas) => {
  233.           let parent = canvas.parentElement;
  234.           let rect = parent ? parent.getBoundingClientRect() : null;
  235.           if (rect) {
  236.             let cWidth = Math.round(canvas.width);
  237.             let cHeight = Math.round(canvas.height);
  238.             let rWidth = Math.round(rect.width);
  239.             let rHeight = Math.round(rect.height);
  240.             if (cWidth !== rWidth || cHeight !== rHeight) {
  241.               // 更新画布分辨率前将画面设置为背景,防止闪烁
  242.               canvas.style.backgroundImage = `url(${canvas.toDataURL('image/png')})`;
  243.               // 更新画布分辨率(将会自动清空画布内容)
  244.               canvas.width = rWidth;
  245.               canvas.height = rHeight;
  246.             }
  247.           }
  248.         });
  249.       }
  250.       // 检测是否存在与视频关联的画布
  251.       if (currTime - cacheItem.timeCheck >= INTERVAL_CHECK_VIDEO) {
  252.         cacheItem.timeCheck = currTime;
  253.         // 当前存在关联的画布,不处理
  254.         if (foundCanvas) {
  255.           cacheItem.willStop = false;
  256.           return;
  257.         }
  258.         // 若当前不存在关联的画布,检测上一次的查找结果
  259.         if (cacheItem.willStop) {
  260.           console.debug(PREFIX, '视频没有被调用,停止播放', { url });
  261.           cacheItem.isStopped = true;
  262.           stopPlayer(cacheItem.player, cacheItem.element);
  263.           cacheList[url] = null;
  264.         } else {
  265.           cacheItem.willStop = true;
  266.         }
  267.       }
  268.     }
  269.   }, INTERVAL_TIME);
  270. }
  271. /** 停止循环处理视频 */
  272. export function timerStop() {
  273.   if (loopTimer) {
  274.     clearInterval(loopTimer);
  275.     loopTimer = null;
  276.   }
  277. }
复制代码
使用时只需要调用 getVideoCanvas() 获取 canvas,然后插入到 DOM 即可,画布会自适应父元素宽高。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册