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

  • 通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次已关闭评论
  • 58 次浏览
  • A+
所属分类:Web前端
摘要

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


效果预览

视频画面

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

网络请求

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

代码实现

ZLMRTCClient.js

当前使用的版本:
1.0.1 Mon Mar 27 2023 19:11:59 GMT+0800

首先需要修改 ZLMRTCClient.js 的代码,解决由于网络导致播放失败时无法触发 WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED 事件的问题。

修改前:

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

修改后:

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

修改内容:

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

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

// 添加 catch() axios({ }).then(() => { }).catch(() => {   // 网络异常时触发事件   this.dispatch(Events$1.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, null); }); 

video-preview.js

// 2024-05-30 // 初始版本  /**  * @typedef  CacheItem  * @property {HTMLElement | null} element  * @property {ZLMPlayer | null} player  * @property {number} usedAt  */  /** @typedef {InstanceType<typeof ZLMRTCClient.Endpoint>} ZLMPlayer */  /** 画布渲染间隔 */ const INTERVAL_RENDER = 100;  /** 画布分辨率更新间隔 */ const INTERVAL_RESIZE = 1000;  /** 检测画布是否在页面上间隔 */ const INTERVAL_WATCH_CANVAS = 1000;  /** 检测视频是否存在调用间隔 */ const INTERVAL_WATCH_VIDEO = 20000;  /** 模块名称 */ const PREFIX = '[video-preview]';  /** 重新播放间隔 */ const RESTART_TIMEOUT = 2000;  /** ZLM 客户端 */ const ZLMRTCClient = window.ZLMRTCClient;  /**  * @desc 缓存信息列表  * @type {Record<string, CacheItem | null>}  */ export const cacheList = {};  /**  * @description 初始化播放器  * @param {string} url 视频流地址  */ function initPlayer(url = '') {   try {      if (!url) {       throw new Error('缺少 url 参数');     }      /** 是否主动停止播放 */     let isStoped = false;      /**      * @description 初始化 & 更新数据      * @param {CacheItem} cache      */     let fnInit = (cache) => {        let element = document.createElement('video');        // 开启自动播放       // 注:不能用 `setAttribute`,否则没效果       element.autoplay = true;       element.controls = false;       element.muted = true;        // 添加到页面,否则无法播放       element.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0');       document.body.appendChild(element);        let player = new ZLMRTCClient.Endpoint({         // video 标签         element: element,         // 是否打印日志         debug: false,         // 流地址         zlmsdpUrl: url,         // 功能开关         audioEnable: false,         simulcast: false,         useCamera: false,         videoEnable: true,         // 仅查看,不推流         recvOnly: true,         // 推流分辨率         resolution: { w: 1280, h: 720 },         // 文本收发         // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send         usedatachannel: false,       });        // // 监听事件:ICE 协商出错       // player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function () {       //   console.error(PREFIX, 'ICE 协商出错')       // });        // 监听事件:获取到了远端流,可以播放       player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function (event) {         console.log(PREFIX, '播放成功', event.streams);       });        // 监听事件:offer anwser 交换失败       player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, function (event) {          console.error(PREFIX, 'offer anwser 交换失败', event);          // 当前没有主动停止         if (!isStoped) {           // 停止播放           stopPlayer(player, element);           // 重新播放           setTimeout(() => {             fnInit(cache);           }, RESTART_TIMEOUT);         }        });        // 监听事件:RTC 状态变化       player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, function (state) {          console.log(PREFIX, 'RTC 状态变化', state);          // 状态为已断开         if (state === 'disconnected' && !isStoped) {           // 停止播放           stopPlayer(player, element);           // 重新播放           setTimeout(() => {             fnInit(cache);           }, RESTART_TIMEOUT);         }        });        cache.element = element;       cache.player = player;       cache.usedAt = Date.now();      };      let cacheItem = cacheList
; if (cacheItem) { return cacheItem; } else { cacheItem = {}; } console.log(PREFIX, '初始化', cacheItem); // 初始化 fnInit(cacheItem); // 添加缓存信息 cacheList
= cacheItem; // 监听调用情况 let watchTimer = setInterval(() => { let currTime = Date.now(); let lastTime = cacheItem.usedAt; // 一段时间内没有被调用,停止播放 if (currTime - lastTime > INTERVAL_WATCH_VIDEO) { console.debug(PREFIX, '视频没有被调用,停止播放', { url }); isStoped = true; stopPlayer(cacheItem.player, cacheItem.element); cacheList
= null; clearInterval(watchTimer); } }, INTERVAL_WATCH_VIDEO); return cacheItem; } catch (error) { console.error(PREFIX, '初始化播放器失败:'); console.error(error); return null; } } /** * @description 停止播放 * @param {ZLMPlayer} player * @param {HTMLVideoElement} element */ function stopPlayer(player, element) { try { if (player) { console.debug(PREFIX, 'stopPlayer - 停止播放'); player.close(); } if (element instanceof HTMLVideoElement) { console.debug(PREFIX, 'stopPlayer - 移除元素'); element.remove(); } return true; } catch (error) { console.error(PREFIX, '停止播放失败:'); console.error(error); return false; } } /** * @description 获取视频画面 canvas * @param {string} url */ export function getVideoCanvas(url = '') { try { if (!url) { throw new Error('缺少 url 参数'); } let cacheItem = initPlayer(url); let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); // 背景填充 canvas.style.backgroundPosition = 'center center'; canvas.style.backgroundSize = '100% 100%'; /** 更新画布分辨率 */ let fnResize = () => { let parent = canvas.parentElement; let rect = parent ? parent.getBoundingClientRect() : null; if (rect) { let cWidth = Math.round(canvas.width); let cHeight = Math.round(canvas.height); let rWidth = Math.round(rect.width); let rHeight = Math.round(rect.height); if (cWidth !== rWidth || cHeight !== rHeight) { // 更新画布分辨率前将画面设置为背景,防止闪烁 canvas.style.backgroundImage = `url(${canvas.toDataURL('image/png')})`; // 更新画布分辨率(将会自动清空画布内容) canvas.width = rWidth; canvas.height = rHeight; } } }; if (!cacheItem) { throw new Error('获取缓存数据失败'); } // 渲染画面 let renderTimer = setInterval(() => { // 注: // 每次渲染都重新获取,防止重连后获取不到新创建的 video 元素 let video = cacheItem.element; let cWidth = canvas.width; let cHeight = canvas.height; if (document.contains(video)) { ctx.drawImage(video, 0, 0, cWidth, cHeight); } canvas.style.backgroundImage = ''; cacheItem.usedAt = Date.now(); }, INTERVAL_RENDER); // 更新分辨率 let resizeTimer = setInterval(fnResize, INTERVAL_RESIZE); // 监听元素 let watchTimer = setInterval(() => { if (!document.contains(canvas)) { console.debug(PREFIX, '画布已被移除,停止渲染画面', { url }); clearInterval(renderTimer); clearInterval(resizeTimer); clearInterval(watchTimer); } }, INTERVAL_WATCH_CANVAS); // 初始化分辨率 setTimeout(fnResize, 0); return canvas; } catch (error) { console.error(PREFIX, '获取 canvas 失败:'); console.error(error); return null; } }

使用时只需要调用 getVideoCanvas() 获取 canvas,然后插入到 DOM 即可,画布会自适应父元素宽高。