首页 > 其他分享 >通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次

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

时间:2024-05-30 12:11:09浏览次数:24  
标签:Canvas console url 视频流 canvas element 只拉取 cacheItem let

效果预览

视频画面

网络请求

代码实现

ZLMRTCClient.js

首先需要修改 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 即可,画布会自适应父元素宽高。

标签:Canvas,console,url,视频流,canvas,element,只拉取,cacheItem,let
From: https://www.cnblogs.com/frost-zx/p/-/zlm-rtc-client-multi-video-pull-once

相关文章

  • 功能实现:canvas图片缩放与移动
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width,initial-scale=1.0"/><title>canvas图片缩放与移动</ti......
  • uniapp_07_文本截取和canvas
    uniapp_07_文本截取和canvasuniapp的canvas文本截取获取图片颜色renderjs参考uniapp的canvasuniappcancas组件和api请看uniapp的官方文档,咕就不在这里写了,哎早知道当年就好好读书了,现在写个随笔都写不好,标点符号都不会用,果然没救了canvas组件和canvasApi与html......
  • 博客园美化:canvas炫酷背景
    话不多说,先上效果图:yysy,这个当背景确实酷炫!!!......
  • webgl和canvas的区别
    webgl和canvas的区别WebGL和Canvas的主要区别在于它们的渲染方式、功能复杂性、以及编程难度。12渲染方式:Canvas使用2D渲染上下文来绘制图形和图像,基于像素的绘图系统,通过JavaScript脚本控制渲染过程。而WebGL(WebGraphicsLibrary)是基于OpenGLES标......
  • html5新标签 画布 canvas 替代了 flash
    绘制矩形边框,和填充不同的是绘制使用的是strokeRect,和strokeStyle实现的 绘制路径绘制路径的作用是为了设置一个不规则的多边形状态路径都是闭合的,使用路径进行绘制的时候需要既定的步骤:需要设置路径的起点使用绘制命令画出路径封闭路径填充或者绘制已经封闭路......
  • 画布canvas基础 01
    1.什么是canvascanvas是用来绘制图形的.它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。<canvaswidth="500"height="500">当前的浏览器版本不支持,请升级浏览器</canvas>判断浏览器是否支持画布cnavascanvas的标签属性只有两个,width和heig......
  • WPF Canvas在Image 图像上绘图,自适应缩放.
    效果如图  实现了绘图,自适应缩放核心代码如下<Window.InputBindings><KeyBindingKey="Z"Modifiers="Ctrl"Command="{BindingUndoCommand}"/></Window.InputBindings><i:Interaction.Triggers>......
  • 采用flex布局,父组件width为百分比,解决子组件canvas画布width自适应问题
     创建EleResize.js文件(拷贝以下代码即可)varEleResize={ _handleResize:function(e){  varele=e.target||e.srcElement  vartrigger=ele.__resizeTrigger__  if(trigger){   varhandlers=trigger.__z_resizeListeners   if......
  • vidgear:处理实时视频流
    Github:https://github.com/abhiTronix/vidgear在当今数字化的时代,视频处理应用变得越来越普遍。无论是视频流分析、实时视频处理还是视频流转码,都需要强大的工具来实现。PythonVidgear库就是这样一个工具,它为开发人员提供了丰富的功能,用于处理实时视频流。本文将深入探讨Pyth......
  • 浅谈AI智能分析与视频流媒体能力下的自然灾害预防监测应用
    一、方案背景夏季是灾害多发季节,山洪、泥石流、洪涝、冰雹、飓风、地震等自然灾害每年都给国家经济带来巨大的损失。随着人工智能(AI)和视频流媒体技术的发展,这些技术在自然灾害预防和监测方面的应用正在变得越来越广泛和重要。AI智能分析技术能够处理复杂的数据,并且能够帮助人们......