- A+
所属分类:Web前端
效果预览
视频画面
网络请求
代码实现
ZLMRTCClient.js
当前使用的版本:
1.0.1
Mon Mar 27 2023 19:11:59 GMT+0800
首先需要修改 ZLMRTCClient.js 的代码,解决由于网络导致播放失败时无法触发 WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED
事件的问题。
修改前:
修改后:
修改内容:
// 添加 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 即可,画布会自适应父元素宽高。