翼度科技»论坛 编程开发 JavaScript 查看内容

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

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
效果预览

视频画面

网络请求

代码实现

ZLMRTCClient.js

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

修改后:

修改内容:

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

来源:https://www.cnblogs.com/frost-zx/p/-/zlm-rtc-client-multi-video-pull-once
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具