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

效果预览

视频画面

网络请求

代码实现

ZLMRTCClient.js

当前使用的版本:
1.0.1Mon 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[url];
 if (cacheItem) {
 return cacheItem;
 } else {
 cacheItem = {};
 }
 console.log(PREFIX, '初始化', cacheItem);
 // 初始化
 fnInit(cacheItem);
 // 添加缓存信息
 cacheList[url] = 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[url] = 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 即可,画布会自适应父元素宽高。

作者:Frost-ZX原文地址:https://www.cnblogs.com/frost-zx/p/-/zlm-rtc-client-multi-video-pull-once

%s 个评论

要回复文章请先登录注册