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

news2024/12/12 2:26:04

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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql5.7和mysql8.0安装教程(超详细)

目录 一、简介 1.1 什么是数据库 1.2 什么是数据库管理系统&#xff08;DBMS&#xff09; 1.3 数据库的作用 二、安装MySQL 1.1 国内yum源安装MySQL5.7&#xff08;centos7&#xff09; &#xff08;1&#xff09;安装4个软件包 &#xff08;2&#xff09;找到4个软件包…

ALSA笔记

alsa笔记 ALSA(Advanced Linux Sound Architecture)简介 以上是android和linux系统的音频整体架构图,他们不同的区别主要是在用户空间,Linux通过ALSA-Lib来和ALSA交互,而android则是tingyAlsa,其位于aosp源码根目录的/external/tinyalsa下; 在Kernel层,Alsa向上封装的D…

哈希表实现

哈希概念 哈希&#xff08;hash&#xff09;又称散列&#xff0c;是一种组织数据的方式。从译名来看&#xff0c;有散乱排列的意思。本质就是通过哈希函数把关键字 Key 跟存储位置建立一个映射关系&#xff0c;查找时通过这个哈希函数计算出 Key 存储的位置&#xff0c;进行快…

web复习(四)

JavaScript编程 1.计算圆的面积。 &#xff08;1&#xff09;表单中设置2个文本框、1个按钮、1个重置按钮&#xff0c;其中圆的面积文本框设置为只读&#xff1b; &#xff08;2&#xff09;编写两个自定义函数&#xff0c;分别是计算圆的面积函数area&#xff08;radius&…

第六届地博会世界酒中国菜助力广州龙美地标美食公司推动地标发展

第六届知交会暨地博会&#xff1a;世界酒中国菜助力广州龙美地标美食公司推动地标产品创新发展 2024年12月9日至11日&#xff0c;第六届粤港澳大湾区知识产权交易博览会暨国际地理标志产品交易博览会在中新广州知识城盛大启幕。本届盛会吸引了全球众多知识产权领域的专业人士和…

【期末复习】编译原理

1. 语法描述 1.1. 上下文无关文法 1.2. 句子 & 句型 & 语言 推导出来的都是句型但是如果句型中只含有终结符&#xff0c;那就是句子所有的句子合起来&#xff0c;才是语言 1.3. 文法 文法就是推导的式子。 1.4. 文法二义性 1.5. 文法二义性证明——根据最左 \ 最右推…

AI绘画设计实战-Day2

Stable Diffusion 提示词前缀 FF,(masterpiece:1.2),best quality,highres,extremely detailed CG,perfect lighting,8k wallpaper,anime,comic,game CG, FF&#xff0c;&#xff08;杰作&#xff1a;1.2&#xff09;&#xff0c;最高质量&#xff0c;高分辨率&#xff0c;极其…

python数据分析之爬虫基础:requests详解

1、requests基本使用 1.1、requests介绍 requests是python中一个常用于发送HTTP请求的第三方库&#xff0c;它极大地简化了web服务交互的过程。它是唯一的一个非转基因的python HTTP库&#xff0c;人类可以安全享用。 1.2、requests库的安装 pip install -i https://pypi.tu…

鸿雁电器发力,能否抢占康养卫浴新蓝海?

经济下行&#xff0c;叠加房地产行业的调整以及数智化浪潮的强劲推动&#xff0c;建材行业正面临着前所未有的变革与机遇。为了更好地把握行业趋势&#xff0c;求新求变&#xff0c;12月9日&#xff0c;鸿雁电器在青山湖园区鸿雁物联网大厦17楼鸿鹄厅成功举办了第四届“智创变革…

Scratch教学作品 | 3D飞行模拟器——体验飞行的无限乐趣! ✈️

今天为大家推荐一款令人惊叹的Scratch作品——《3D飞行模拟器》&#xff01;由BamBozzle制作&#xff0c;这款游戏完全用Scratch构建&#xff0c;带你体验开放世界飞行的自由与乐趣。从起飞到降落&#xff0c;每一步都需要你的精准操作&#xff01;更棒的是&#xff0c;这款游戏…

Linux服务器运维管理面板之1panel

华子目录 安装1panel使用卸载浏览器登录 安装 网站&#xff1a;https://community.fit2cloud.com/#/products/1panel/downloads 解压 [rootdocker-node1 ~]# tar -zxf 1panel-v1.10.13-lts-linux-amd64.tar.gz[rootdocker-node1 ~]# cd 1panel-v1.10.13-lts-linux-amd64/ [ro…

SpringBoot【二】yaml、properties两配置文件介绍及使用

一、前言 续上一篇咱们已经搭建好了一个springboot框架雏形。但是很多初学的小伙伴私信bug菌说&#xff0c;在开发项目中&#xff0c;为啥.yaml的配置文件也能配置&#xff0c;SpringBoot 是提供了两种2 种全局的配置文件嘛&#xff0c;这两种配置有何区别&#xff0c;能否给大…

学习笔记063——通过使用 aspose-words 将 Word 转 PDF 时,遇到的字体改变以及乱码问题

文章目录 1、问题描述&#xff1a;2、解决方法&#xff1a; 1、问题描述&#xff1a; Java项目中&#xff0c;有个需要将word转pdf的需求。本人通过使用aspose-words来转换的。在Windows中&#xff0c;转换是完全正常的。但是当部署到服务器时&#xff0c;会出现转换生成的pdf…

Linux下redis环境的搭建

1.redis的下载 redis官网下载redis的linux压缩包&#xff0c;官网地址:Redis下载 网盘链接&#xff1a; 通过网盘分享的文件&#xff1a;redis-5.0.4.tar.gz 链接: https://pan.baidu.com/s/1cz3ifYrDcHWZXmT1fNzBrQ?pwdehgj 提取码: ehgj 2.redis安装与配置 将包上传到 /…

帝可得-运营管理App

运营管理App Android模拟器 本项目的App客户端部分已经由前端团队进行开发完成&#xff0c;并且以apk的方式提供出来&#xff0c;供我们测试使用&#xff0c;如果要运行apk&#xff0c;需要先安装安卓的模拟器。 可以选择国内的安卓模拟器产品&#xff0c;比如&#xff1a;网…

用 Python 从零开始创建神经网络(十六):二元 Logistic 回归

二元 Logistic 回归 引言1. Sigmoid 激活函数2. Sigmoid 函数导数3. Sigmoid 函数代码4. 二元交叉熵损失&#xff08;Binary Cross-Entropy Loss&#xff09;5. 二元交叉熵损失导数&#xff08;Binary Cross-Entropy Loss Derivative&#xff09;6. 二进制交叉熵代码&#xff0…

高质量阅读微信小程序ssm+论文源码调试讲解

第2章 开发环境与技术 高质量阅读微信小程序的编码实现需要搭建一定的环境和使用相应的技术&#xff0c;接下来的内容就是对高质量阅读微信小程序用到的技术和工具进行介绍。 2.1 MYSQL数据库 本课题所开发的应用程序在数据操作方面是不可预知的&#xff0c;是经常变动的&…

AI 学习框架:开启智能未来的钥匙

一、热门人工智能学习框架概述 人工智能学习框架在当今的科技发展中占据着至关重要的地位&#xff0c;它为开发者提供了强大的工具&#xff0c;有力地推动了人工智能的发展&#xff0c;同时也极大地降低了开发的难度。 人工智能学习框架是帮助开发者和研究人员快速构建、训练…

go-blueprint create exit status 1

1. 异常信息 2024/12/06 10:59:19 Could not initialize go.mod in new project exit status 1 2024/12/06 10:59:19 Problem creating files for project. exit status 1 Error: exit status 12. 排查思路 手动进行go mod init查看手动的报错解决报错 3. 解决问题 发现是GO11…

Python利用海龟画图turtle库做一个篮球比赛计时画面

Python利用海龟画图turtle库做一个篮球比赛计时画面&#xff0c;代码如下 import turtle import time import random r random.random() g random.random() b random.random() turtle.speed(0) for j in range(1,2,1):for i in range(1,60,1):print(i)time.sleep(0.1)turtl…