使用 WebRtcStreamer 实现实时视频流播放

WebRtcStreamer 是一个基于 WebRTC 协议的轻量级开源工具,可以在浏览器中直接播放 RTSP 视频流。它利用 WebRTC 的强大功能,提供低延迟的视频流播放体验,非常适合实时监控和其他视频流应用场景。

本文将介绍如何在Vue.js项目中使用 WebRtcStreamer 实现实时视频流播放,并分享相关的代码示例。

注意:只支持H264格式

流媒体方式文章
使用 Vue 和 flv.js 实现流媒体视频播放:完整教程
VUE项目中优雅使用EasyPlayer实时播放摄像头多种格式视频使用版本信息为5.xxxx

实现步骤

  • 安装和配置 WebRtcStreamer 服务端
    要使用 WebRtcStreamer,需要先在服务器上部署其服务端。以下是基本的安装步骤:

  • 从 WebRtcStreamer 官方仓库 下载代码。
    在这里插入图片描述
    启动命令
    在这里插入图片描述
    或者双击exe程序
    在这里插入图片描述
    服务启动后,默认会监听 8000 端口,访问 http://<server_ip>:8000 可查看状态。
    在这里插入图片描述
    更改默认端口命令:webrtc-streamer.exe -o -H 0.0.0.0:9527

2.集成到vue中
webRtcStreamer.js 不需要在html文件中引入webRtcStreamer相关代码

/**
 * @constructor
 * @param {string} videoElement -  dom ID
 * @param {string} srvurl -  WebRTC 流媒体服务器的 URL(默认为当前页面地址)
 */
class WebRtcStreamer {
  constructor(videoElement, srvurl) {
    if (typeof videoElement === 'string') {
      this.videoElement = document.getElementById(videoElement);
    } else {
      this.videoElement = videoElement;
    }
    this.srvurl =
      srvurl || `${location.protocol}//${window.location.hostname}:${window.location.port}`;
    this.pc = null; // PeerConnection 实例

    // 媒体约束条件
    this.mediaConstraints = {
      offerToReceiveAudio: true,
      offerToReceiveVideo: true,
    };

    this.iceServers = null; // ICE 服务器配置
    this.earlyCandidates = []; // 提前收集的候选者
  }

  /**
   * HTTP 错误处理器
   * @param {Response} response - HTTP 响应
   * @throws {Error} 当响应不成功时抛出错误
   */
  _handleHttpErrors(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response;
  }

  /**
   * 连接 WebRTC 视频流到指定的 videoElement
   * @param {string} videourl - 视频流 URL
   * @param {string} audiourl - 音频流 URL
   * @param {string} options - WebRTC 通话的选项
   * @param {MediaStream} localstream - 本地流
   * @param {string} prefmime - 优先的 MIME 类型
   */
  connect(videourl, audiourl, options, localstream, prefmime) {
    this.disconnect();

    if (!this.iceServers) {
      console.log('获取 ICE 服务器配置...');

      fetch(`${this.srvurl}/api/getIceServers`)
        .then(this._handleHttpErrors)
        .then((response) => response.json())
        .then((response) =>
          this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime),
        )
        .catch((error) => this.onError(`获取 ICE 服务器错误: ${error}`));
    } else {
      this.onReceiveGetIceServers(
        this.iceServers,
        videourl,
        audiourl,
        options,
        localstream,
        prefmime,
      );
    }
  }

  /**
   * 断开 WebRTC 视频流,并清空 videoElement 的视频源
   */
  disconnect() {
    if (this.videoElement?.srcObject) {
      this.videoElement.srcObject.getTracks().forEach((track) => {
        track.stop();
        this.videoElement.srcObject.removeTrack(track);
      });
    }
    if (this.pc) {
      fetch(`${this.srvurl}/api/hangup?peerid=${this.pc.peerid}`)
        .then(this._handleHttpErrors)
        .catch((error) => this.onError(`hangup ${error}`));

      try {
        this.pc.close();
      } catch (e) {
        console.log(`Failure close peer connection: ${e}`);
      }
      this.pc = null;
    }
  }

  /**
   * 获取 ICE 服务器配置的回调
   * @param {Object} iceServers - ICE 服务器配置
   * @param {string} videourl - 视频流 URL
   * @param {string} audiourl - 音频流 URL
   * @param {string} options - WebRTC 通话的选项
   * @param {MediaStream} stream - 本地流
   * @param {string} prefmime - 优先的 MIME 类型
   */
  onReceiveGetIceServers(iceServers, videourl, audiourl, options, stream, prefmime) {
    this.iceServers = iceServers;
    this.pcConfig = iceServers || { iceServers: [] };
    try {
      this.createPeerConnection();

      let callurl = `${this.srvurl}/api/call?peerid=${this.pc.peerid}&url=${encodeURIComponent(
        videourl,
      )}`;
      if (audiourl) {
        callurl += `&audiourl=${encodeURIComponent(audiourl)}`;
      }
      if (options) {
        callurl += `&options=${encodeURIComponent(options)}`;
      }

      if (stream) {
        this.pc.addStream(stream);
      }

      this.earlyCandidates.length = 0;

      this.pc
        .createOffer(this.mediaConstraints)
        .then((sessionDescription) => {
          // console.log(`创建 Offer: ${JSON.stringify(sessionDescription)}`);

          if (prefmime !== undefined) {
            const [prefkind] = prefmime.split('/');
            const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs;
            const preferredCodecs = codecs.filter((codec) => codec.mimeType === prefmime);

            this.pc
              .getTransceivers()
              .filter((transceiver) => transceiver.receiver.track.kind === prefkind)
              .forEach((tcvr) => {
                if (tcvr.setCodecPreferences) {
                  tcvr.setCodecPreferences(preferredCodecs);
                }
              });
          }

          this.pc
            .setLocalDescription(sessionDescription)
            .then(() => {
              fetch(callurl, {
                method: 'POST',
                body: JSON.stringify(sessionDescription),
              })
                .then(this._handleHttpErrors)
                .then((response) => response.json())
                .then((response) => this.onReceiveCall(response))
                .catch((error) => this.onError(`调用错误: ${error}`));
            })
            .catch((error) => console.log(`setLocalDescription error: ${JSON.stringify(error)}`));
        })
        .catch((error) => console.log(`创建 Offer 失败: ${JSON.stringify(error)}`));
    } catch (e) {
      this.disconnect();
      alert(`连接错误: ${e}`);
    }
  }

  /**
   * 创建 PeerConnection 实例
   */

  createPeerConnection() {
    console.log('创建 PeerConnection...');
    this.pc = new RTCPeerConnection(this.pcConfig);
    this.pc.peerid = Math.random(); // 生成唯一的 peerid

    // 监听 ICE 候选者事件
    this.pc.onicecandidate = (evt) => this.onIceCandidate(evt);
    this.pc.onaddstream = (evt) => this.onAddStream(evt);
    this.pc.oniceconnectionstatechange = () => {
      if (this.videoElement) {
        if (this.pc.iceConnectionState === 'connected') {
          this.videoElement.style.opacity = '1.0';
        } else if (this.pc.iceConnectionState === 'disconnected') {
          this.videoElement.style.opacity = '0.25';
        } else if (['failed', 'closed'].includes(this.pc.iceConnectionState)) {
          this.videoElement.style.opacity = '0.5';
        } else if (this.pc.iceConnectionState === 'new') {
          this.getIceCandidate();
        }
      }
    };
    return this.pc;
  }

  onAddStream(event) {
    console.log(`Remote track added: ${JSON.stringify(event)}`);
    this.videoElement.srcObject = event.stream;
    const promise = this.videoElement.play();
    if (promise !== undefined) {
      promise.catch((error) => {
        console.warn(`error: ${error}`);
        this.videoElement.setAttribute('controls', true);
      });
    }
  }

  onIceCandidate(event) {
    if (event.candidate) {
      if (this.pc.currentRemoteDescription) {
        this.addIceCandidate(this.pc.peerid, event.candidate);
      } else {
        this.earlyCandidates.push(event.candidate);
      }
    } else {
      console.log('End of candidates.');
    }
  }

  /**
   * 添加 ICE 候选者到 PeerConnection
   * @param {RTCIceCandidate} candidate - ICE 候选者
   */
  addIceCandidate(peerid, candidate) {
    fetch(`${this.srvurl}/api/addIceCandidate?peerid=${peerid}`, {
      method: 'POST',
      body: JSON.stringify(candidate),
    })
      .then(this._handleHttpErrors)
      .catch((error) => this.onError(`addIceCandidate ${error}`));
  }

  /**
   * 处理 WebRTC 通话的响应
   * @param {Object} message - 来自服务器的响应消息
   */
  onReceiveCall(dataJson) {
    const descr = new RTCSessionDescription(dataJson);
    this.pc
      .setRemoteDescription(descr)
      .then(() => {
        while (this.earlyCandidates.length) {
          const candidate = this.earlyCandidates.shift();
          this.addIceCandidate(this.pc.peerid, candidate);
        }
        this.getIceCandidate();
      })
      .catch((error) => console.log(`设置描述文件失败: ${JSON.stringify(error)}`));
  }

  getIceCandidate() {
    fetch(`${this.srvurl}/api/getIceCandidate?peerid=${this.pc.peerid}`)
      .then(this._handleHttpErrors)
      .then((response) => response.json())
      .then((response) => this.onReceiveCandidate(response))
      .catch((error) => this.onError(`getIceCandidate ${error}`));
  }

  onReceiveCandidate(dataJson) {
    if (dataJson) {
      dataJson.forEach((candidateData) => {
        const candidate = new RTCIceCandidate(candidateData);
        this.pc
          .addIceCandidate(candidate)
          .catch((error) => console.log(`addIceCandidate error: ${JSON.stringify(error)}`));
      });
    }
  }

  /**
   * 错误处理器
   * @param {string} message - 错误信息
   */
  onError(status) {
    console.error(`WebRTC 错误: ${status}`);
  }
}

export default WebRtcStreamer;

组件中使用

<template>
  <div>
    <video id="video" controls muted autoplay></video>
    <button @click="startStream">开始播放</button>
    <button @click="stopStream">停止播放</button>
  </div>
</template>

<script>
import WebRtcStreamer from "@/utils/webRtcStreamer";

export default {
  name: "VideoStreamer",
  data() {
    return {
      webRtcServer: null,
    };
  },
  methods: {
    startStream() {
    const srvurl = "127.0.0.1:9527"
      this.webRtcServer = new WebRtcStreamer(
        "video",
        `${location.protocol}//${srvurl}`
      );
      const videoPath = "stream_name"; // 替换为你的流地址
      this.webRtcServer.connect(videoPath);
    },
    stopStream() {
      if (this.webRtcServer) {
        this.webRtcServer.disconnect(); // 销毁
      }
    },
  },
};
</script>

<style>
video {
  width: 100%;
  height: 100%;
  object-fit: fill;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/933902.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

本地无需公网可访问开源趣味艺术画板 paint-board

paint-board 一款用于绘画或涂鸦的工具&#xff0c;它非常轻量而且很有趣&#xff0c;集成了多种创意画笔和绘画功能&#xff0c;能够支持形状绘制、橡皮擦、自定义画板等操作&#xff0c;并可以将作品保存为图片。 第一步&#xff0c;本地部署安装 paint-board 1&#xff0c…

VideoConvertor.java ffmpeg.exe

VideoConvertor.java ffmpeg.exe 视频剪切原理 入点 和 出点 选中时间点&#xff0c;导出

ASP .NET Core 中的环境变量

在本文中&#xff0c;我们将通过组织一场小型音乐会&#xff08;当然是在代码中&#xff09;来了解 ASP .NET Core 中的环境变量。让我们从创建项目开始&#xff1a; dotnet new web --name Concert 并更新Program.cs&#xff1a; // replace this: app.MapGet("/"…

Robust Depth Enhancement via Polarization Prompt Fusion Tuning

paper&#xff1a;论文地址 code&#xff1a;github项目地址 今天给大家分享一篇2024CVPR上的文章&#xff0c;文章是用偏振做提示学习&#xff0c;做深度估计的。模型架构图如下 这篇博客不是讲这篇论文的内容&#xff0c;感兴趣的自己去看paper&#xff0c;主要是分享环境&…

NanoLog起步笔记-3-尝试解析log

nonolog起步笔记-3-尝试解析log 第一次解析sample中的nano二进制log在哪里compressedLog./decompressor decompress /tmp/logFile compressedLog是什么鬼下断分析 第一次解析 sample中的nano二进制log在哪里 如下图 手工执行的结果如下&#xff0c;不看代码&#xff0c;还真不…

Sqoop脚本编写(Mysql---->>hdfs)

目录 语法手册编写方式脚本文件类型文件编写.jar路径指定 执行效果执行方式效果 语法手册 参考博客 编写方式 脚本文件类型 只要是可读的文件即可&#xff08;.txt或者.sh等其他类型&#xff0c;不带文件后缀也可以&#xff0c;但二进制文件最好不要&#xff09; 文件编写…

一、测试工具LoadRunner Professional脚本编写-录制前设置

设置基于URL的脚本 原因:基于HTML的脚本会导致login接口不能正确录制 设置UTF-8 原因:不勾选此项会导致脚本中文变为乱码

day05-开发接口-学习记录和学习计划

1. 查询用户的课程学习记录 1.1 代码实现 Controller层&#xff1a; package com.tianji.learning.controller;import com.tianji.api.dto.leanring.LearningLessonDTO; import com.tianji.learning.service.ILearningLessonService; import com.tianji.learning.service.IL…

2022 年“泰迪杯”数据分析技能赛A 题竞赛作品的自动评判

2022 年“泰迪杯”数据分析技能赛A 题竞赛作品的自动评判 完整代码请私聊 博主 一、背景 在各类学科竞赛中&#xff0c;常常要求参赛者提交 Excel 或/和 PDF 格式的竞赛作品。 本赛题以某届数据分析竞赛作品的评阅为背景&#xff0c;要求参赛者根据给定的评分准则和标准答案&a…

AI Agent框架如何选择?LangGraph vs CrewAI vs OpenAI Swarm

介绍 由 LLMs经历了起起落落。从 2023 年 AutoGPT 和 BabyAGI 的病毒式演示到今天更精致的框架&#xff0c;AI Agent的概念——LLMs自主执行端到端任务的 LLM——既引起了人们的想象力&#xff0c;也引起了怀疑。 为什么重新引起人们的兴趣&#xff1f;LLMs 在过去 9 个月中进…

【OpenCV】平滑图像

二维卷积(图像滤波) 与一维信号一样&#xff0c;图像也可以通过各种低通滤波器&#xff08;LPF&#xff09;、高通滤波器&#xff08;HPF&#xff09;等进行过滤。LPF 有助于消除噪音、模糊图像等。HPF 滤波器有助于在图像中找到边缘。 opencv 提供了函数 **cv.filter2D()**&…

调试android 指纹遇到的坑

Android8以后版本 一、指纹服务不能自动 指纹服务fingerprintd(biometrics fingerprintservice)&#xff0c;可以手动起来&#xff0c;但是在init.rc中无法启动。 解决办法&#xff1a; 1.抓取开机时kernel log &#xff0c;确认我们的启动指纹服务的init.rc 文件有被init.c…

深度学习笔记之BERT(五)TinyBERT

深度学习笔记之TinyBERT 引言回顾&#xff1a;DistilBERT模型TinyBERT模型结构TinyBERT模型策略Transformer层蒸馏嵌入层蒸馏预测层蒸馏 TinyBERT模型的训练效果展示 引言 上一节介绍了 DistilBERT \text{DistilBERT} DistilBERT模型&#xff0c;本节将继续介绍优化性更强的知…

30.串联所有单词的子串 python

串联所有单词的子串 题目题目描述示例 1&#xff1a;示例 2&#xff1a;示例 3&#xff1a;提示&#xff1a;题目链接 题解解题思路python实现代码解释&#xff1a;提交结果 题目 题目描述 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的…

【LeetCode】498.对角线遍历

无论何时何地&#xff0c;我都认为对于一道编程题&#xff0c;思考解法的时间用于是实际动手解决问题的2倍&#xff01;如果敲键盘编码需要5min&#xff0c;那么思考解法的过程至少就需要10分钟。 1. 题目 2. 思想 其实这就是一道模拟题&#xff0c;难度中等。做这种题的关键就…

uniapp中父组件传参到子组件页面渲染不生效问题处理实战记录

上篇文件介绍了,父组件数据更新正常但是页面渲染不生效的问题,详情可以看下:uniapp中父组件数组更新后与页面渲染数组不一致实战记录 本文在此基础上由于新增需求衍生出新的问题.本文只记录一下解决思路. 下面说下新增需求方便理解场景: 商品信息设置中添加抽奖概率设置…

Flutter提示错误:无效的源发行版17

错误描述 Flutter从3.10.1 升级到3.19.4&#xff0c;在3.10.1的时候一切运行正常&#xff0c;但是当我将Flutter版本升级到3.19.4后&#xff0c;出现了下方的错误 FAILURE: Build failed with an exception.* What went wrong: Execution failed for task :device_info_plus:…

etcd的dbsize引起的集群故障

故障现象 k8s集群不能访问&#xff0c;具体表现kubectl命令不能使用。 思路 检查apiserver服务状态&#xff0c;检查etcd集群状态中errors列中存在一个alarm:NOSPACE的告警 解决&分析 具体表现 恢复使用第一&#xff0c;先尝试解除告警看能否恢复 etcdctl --endpoin…

Redis性能优化18招

Redis性能优化的18招 目录 前言选择合适的数据结构避免使用过大的key和value[使用Redis Pipeline](#使用Redis Pipeline)控制连接数量合理使用过期策略使用Redis集群充分利用内存优化使用Lua脚本监控与调优避免热点key使用压缩使用Geo位置功能控制数据的持久化尽量减少事务使…

Docker 安装 Yapi

Docker 安装系列 Docker已安装。 1、场景Yapi使用的MongoDB用户信息 1.1 创建自定义 Docker 网络 首先&#xff0c;创建一个自定义的 Docker 网络&#xff0c;以便 MongoDB 和 YApi 容器可以相互通信 [rootflexusx-328569 data]# docker network create yapi-networ…