通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次
效果预览
视频画面
网络请求
代码实现
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 初始版本
// 2024-06-06 优化视频是否存在调用检测方式
import { v4 as uuidv4 } from 'uuid';
/**
* @typedef CacheItem
* @property {string} id
* @property {HTMLElement | null} element
* @property {ZLMPlayer | null} player
*/
/** @typedef {InstanceType<typeof ZLMRTCClient.Endpoint>} ZLMPlayer */
/** 画布渲染间隔 */
const INTERVAL_RENDER = 100;
/** 画布分辨率更新间隔 */
const INTERVAL_RESIZE = 1000;
/** 检测画布是否在页面上间隔 */
const INTERVAL_WATCH_CANVAS = 1000;
/** 检测视频是否存在调用间隔 */
const INTERVAL_WATCH_VIDEO = 10000;
/** 模块名称 */
const PREFIX = '[video-preview]';
/** 重新播放间隔 */
const RESTART_TIMEOUT = 2000;
/** ZLM 客户端 */
const ZLMRTCClient = window.ZLMRTCClient;
/**
* @desc 缓存信息列表
* @type {Record<string, CacheItem | null>}
*/
export const cacheList = {};
/**
* @desc RTSP -> WebRTC 地址缓存
* @type {Record<string, string>}
*/
export const urlCaches = {};
/**
* @description 初始化播放器
* @param {string} url 视频流地址
*/
function initPlayer(url = '') {
try {
if (!url) {
throw new Error('缺少 url 参数');
}
/** 是否主动停止播放 */
let isStoped = false;
/** 是否找不到关联的画布,在下一次查找时停止播放 */
let willStop = 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;
};
let cacheItem = cacheList[url];
if (cacheItem) {
return cacheItem;
} else {
cacheItem = {
id: uuidv4(),
element: null,
player: null,
};
}
console.log(PREFIX, '初始化', cacheItem);
// 初始化
fnInit(cacheItem);
// 添加缓存信息
cacheList[url] = cacheItem;
// 监听调用情况
let watchTimer = setInterval(() => {
let cacheId = cacheItem.id;
let canvasList = document.querySelectorAll(`[data-cache-id="${cacheId}"]`);
// 没有关联的画布,停止播放
if (willStop) {
console.debug(PREFIX, '视频没有被调用,停止播放', { url });
isStoped = true;
stopPlayer(cacheItem.player, cacheItem.element);
cacheList[url] = null;
clearInterval(watchTimer);
return;
}
if (canvasList.length > 0) {
willStop = false;
} else {
willStop = true;
}
}, 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');
if (cacheItem) {
// 标记缓存 ID
canvas.setAttribute('data-cache-id', cacheItem.id);
} else {
throw new Error('获取缓存数据失败');
}
// 背景填充
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;
}
}
};
// 渲染画面
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 = '';
}, 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 即可,画布会自适应父元素宽高。