SRS流媒体服务(四)WebRTC实现实时视频通话和低延时互动直播

CentOS版本号:7.9
在这里插入图片描述
SRS版本号:4.0.215
在这里插入图片描述
服务器IP:192.168.5.104
在这里插入图片描述
注意需要开启端口号:1935、1985、8000(UDP端口)、8080。
在这里插入图片描述
注意需要开启服务:http
在这里插入图片描述

文章目录

  • webRTC介绍
    • getUserMedia
    • RTCPeerConnection
    • RTCDataChannel
  • SRS流媒体服务器
    • 推流API:POST /rtc/v1/publish/
    • 拉流API:POST /rtc/v1/play/
    • SRS服务配置开启RTC服务
  • 创建web应用
    • 编写代码,vue3项目
    • 运行代码

webRTC介绍

WebRTC是一个由Google发起的实时通讯解决方案,其中包含视频音频采集,编解码,数据传输,音视频展示等功能,我们可以通过技术快速地构建出一个音视频通讯应用。 虽然其名为WebRTC,但是实际上它不光支持Web之间的音视频通讯,还支持Android以及IOS端,此外由于该项目是开源的,我们也可以通过编译C++代码,从而达到全平台的互通。

getUserMedia

为一个 RTC 连接获取设备的摄像头与 (或) 麦克风权限,并为此 RTC 连接接入设备的摄像头与 (或) 麦克风的信号。

RTCPeerConnection

用于配置音频或视频聊天。

RTCDataChannel

用于设置两个浏览器之间的端到端 (en-US) 数据连接。

SRS流媒体服务器

SRS(Simple Realtime Server)是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181。
SRS服务自带一个简单的信令服务器,用于webRTC交换SDP,提供了2个接口分别用于发布端(推流)和播放端(拉流)进行SDP交换

推流API:POST /rtc/v1/publish/

使用WebRTC推流到SRS时,需要先调用API交换SDP。例如:

POST /rtc/v1/publish/

Body in JSON:

{
  "api": "https://d.ossrs.net/rtc/v1/publish/"
  "streamurl": "webrtc://d.ossrs.net/live/3abd9f34",
  "sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}

服务器响应对应的SDP如下:

{
  "code": 0
  "sdp": "v=0\r\n......\r\na=candidate:1 1 udp 2130706431 172.18.0.4 8000 typ host generation 0\r\n"
  "sessionid": "186tj710:hMub"
}

假如部署SRS服务的服务器IP地址为:192.168.5.104,SRS的http_api监听端口为1985,则推流端交换SDP的API请求地址为:http://192.168.5.104:1985/rtc/v1/publish/,发送post请求

POST /rtc/v1/publish/

Body in JSON:

{
  "api": "http://192.168.5.104:1985/rtc/v1/publish/"
  "streamurl": "webrtc://192.168.5.104:8000/live/stream",
  "sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}

拉流API:POST /rtc/v1/play/

拉流或播放时,需要调用另外的API,请求格式和publish一样。例如:

POST /rtc/v1/play/

Body in JSON:

{
  "api": "https://d.ossrs.net/rtc/v1/play/"
  "streamurl": "webrtc://d.ossrs.net/live/3abd9f34",
  "sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}

服务器响应对应的SDP如下:

{
  "code": 0
  "sdp": "v=0\r\n......\r\na=candidate:1 1 udp 2130706431 172.18.0.4 8000 typ host generation 0\r\n"
  "sessionid": "186tj710:hMub"
}

SRS服务配置开启RTC服务

在这里插入图片描述

创建web应用

编写代码,vue3项目

<template>
    <div id="box">
        <!-- 设置自动播放,否则不会显示视频流画面 -->
        <video id="video" autoplay></video>
        <div id="btn">
            <button ref="button_one" @click="publish">开始直播</button>
            <button ref="button_two" @click="close" >停止直播</button>
            <button ref="button_three" @click="stopAudio" >关闭声音</button>
            <button ref="button_four" @click="startAudio" >开启声音</button>
            <button ref="button_five" @click="play" >播放直播</button>
        </div>
        
        <video id="video2" autoplay></video>
    </div>
</template>

<script setup>
import { onMounted,ref } from 'vue';
// 定义全局属性
let videoStream = null;
let videoElement = null;
// 全局的RTCPeerConnection
let pc = null;
// 全局音频轨道,用于RTCRtpSender发送和停止对应轨道
let audioTrack = null;
// 全局的RTCRtpSender
let audioSender = null;
// 获取按钮元素
let button_one = ref(null);
let button_two = ref(null);
let button_three = ref(null);
let button_four = ref(null);
let button_five = ref(null);

const publish = async()=>{
        if(pc!==null&& pc!==undefined){
            console.log("已开始推流");
            return ;
        }
            

        var httpURL = "http://192.168.5.104:1985/rtc/v1/publish/";
        var webRTCURL = "webRTC://192.168.5.104/live/1";
        var constraints = {
                audio: {
                    echoCancellation : true,    // 回声消除
                    noiseSuppression : true,    // 降噪
                    autoGainControl  : true     // 自动增益
                },
                video: {
                    frameRate   : { min : 30 },                // 最小帧率
                    width       : { min : 640, ideal : 1080}, // 宽度   
                    height      : { min : 360, ideal : 720},  // 高度  
                    aspectRadio : 16/9                        // 宽高比
                }
}
        // 通过摄像头、麦克风获取音视频流
        videoStream = await navigator.mediaDevices.getUserMedia(constraints);
        // 获取video元素
        videoElement = document.querySelector("#video")
        //video播放流数据
        videoElement.srcObject = videoStream;
        // 静音
        videoElement.volume=0;
        // 创建RTC连接对象
         pc = new RTCPeerConnection();
        
        // RTCPeerConnection方法addTransceiver()创建一个新的RTCRtpTransceiver,并将其添加到与RTCPeerConnection关联的收发器集中。
        // 每个收发器代表一个双向流,RTCRtpSender和RTCRtpReceiver都与之相关联。
        // 注意添加顺序为audio、video,后续RTCPeerConnection创建offer时SDP的m线顺序遵循此顺序创建,SRS自带的信令服务器响应的SDP中m线总是先audio后video。
        // 若本端SDP和远端SDP中的m线顺序不一直,则设置远端描述时会异常,显示offer中的m线与answer中的m线顺序不匹配
        pc.addTransceiver("audio", {direction: "recvonly"});
        pc.addTransceiver("video", {direction: "recvonly"});
        // 遍历getUserMedia()获取到的流数据,拿到其中的音频轨道和视频轨道,加入到RTCPeerConnection连接的音频轨道和视频轨道中
        videoStream.getTracks().forEach((track)=>{
            pc.addTrack(track);
        });
        // 创建本端offer
        var offer = await pc.createOffer();
        // 设置本端
        await pc.setLocalDescription(offer);
        var data = {
            "api": httpURL,
            "streamurl":webRTCURL,
            "sdp":offer.sdp
        }
        // SDP交换,请求SRS自带的信令服务器
        httpApi(httpURL,data).then(async(data)=>{
            console.log("answer",data);
            // 设置远端描述,开始连接
            await pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: data.sdp}));
                button_one.value.disabled=true;
                button_two.value.disabled=false;
                button_three.value.disabled=false;
                button_five.value.disabled=false;

        }).catch((data)=>{
            if(data.code===400){
                console.log("SDP交换失败");
            }
        });
        
    
}

const play = async()=>{
    var httpURL = "http://192.168.5.104:1985/rtc/v1/play/";
    var webRTCURL = "webRTC://192.168.5.104/live/1";
    // 创建RTCPeerConnection连接对象
    var pc = new RTCPeerConnection();
    // 创建媒体流对象
    var stream = new MediaStream();
    // 获取播放流的容器video
    var videoElement2 = document.querySelector("#video2");
    // 监听流
    pc.ontrack = (event)=>{
        // 监听到的流加入MediaStream对象中让video播放
        stream.addTrack(event.track);
        videoElement2.srcObject = stream;
    }
    // RTCPeerConnection方法addTransceiver()创建一个新的RTCRtpTransceiver,并将其添加到与RTCPeerConnection关联的收发器集中。
    // 每个收发器代表一个双向流,RTCRtpSender和RTCRtpReceiver都与之相关联。
    // 注意添加顺序为audio、video,后续RTCPeerConnection创建offer时SDP的m线顺序遵循此顺序创建,SRS自带的信令服务器响应的SDP中m线总是先audio后video。
    // 若本端SDP和远端SDP中的m线顺序不一直,则设置远端描述时会异常,显示offer中的m线与answer中的m线顺序不匹配
    pc.addTransceiver("audio", {direction: "recvonly"});
    pc.addTransceiver("video", {direction: "recvonly"});

    var offer =await pc.createOffer();
    await pc.setLocalDescription(offer)
    var data = {
            "api": httpURL,
            "streamurl":webRTCURL,
            "sdp":offer.sdp
    }
    // SDP交换,请求SRS自带的信令服务器
    httpApi(httpURL,data).then(async(data)=>{
            console.log("answer",data);
            // 设置远端描述,开始连接
            await pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: data.sdp}));
            button_five.value.disabled=true;

    }).catch((data)=>{
            if(data.code===400){
                console.log("SDP交换失败");
            }
    });
}

// 关闭连接
const close = ()=>{
    if(pc!==null&&pc!==undefined){
        pc.close();
        pc = null;
        button_one.value.disabled=false;
        button_two.value.disabled=true;
        button_three.value.disabled=true;
        button_four.value.disabled=true;
        button_five.value.disabled=true;
    }
}
// 关闭音频
const stopAudio = ()=>{
    if(pc!==null&&pc!==undefined){
        // RTCPeerConnection方法getSenders()返回RTCRtpSender对象的数组,
        // 每个对象代表负责传输一个轨道的数据的RTP发送器。
        // sender对象提供了检查和控制音轨数据的编码和传输的方法和属性。
        pc.getSenders().forEach((sender)=>{
            if(sender.track!==null&&sender.track.kind==="audio"){
                // 拿到音频轨道
                audioTrack = sender.track;
                // 拿到音频轨道发送者对象RTCRtpSender
                audioSender = sender;
                // RTCRtpSender的replaceTrack()可以在无需重新媒体协商的情况下用另一个媒体轨道更换当前正在发送轨道
                // 参数为空则将当前正在发送的轨道停止,比如关闭音频,再次开启时将音频轨道作为参数传入
                audioSender.replaceTrack(null);
                button_three.value.disabled=true;
                button_four.value.disabled=false;
            }
        });
    }
}
// 开启音频
const startAudio = ()=>{
    console.log(audioSender);
    if(pc!==null&&pc!==undefined){
       if(audioSender.track===null){
        audioSender.replaceTrack(audioTrack);
        button_three.value.disabled=false;
        button_four.value.disabled=true;
       }
    }
}

const httpApi = (httpURL,data)=>{
    var promise = new Promise((resolve,reject)=>{
        var xhr = new XMLHttpRequest();
        xhr.open('POST', httpURL, true);
        xhr.setRequestHeader('Content-type', 'application/json');
        xhr.send(JSON.stringify(data));
        xhr.onload = ()=>{
                if (xhr.readyState !== xhr.DONE) reject(xhr);
                if (xhr.status !== 200 && xhr.status !== 201) reject(xhr) ;
                var data = JSON.parse(xhr.responseText);
                if(data.code===0){
                    resolve(data);
                }else{
                    reject(data)
                }
            }
    });
    return promise;
}

onMounted(()=>{
    button_one.value.disabled=false;
    button_two.value.disabled=true;
    button_three.value.disabled=true;
    button_four.value.disabled=true;
    button_five.value.disabled=true;
});

</script>

<style lang="scss">
*{
    margin: 0;
    padding: 0;
    border: 0;
    box-sizing: border-box;
}
#box{
    width: 100%;
    text-align: center;
}
video{
    background-color: black;
    width: 500px;
    height: 400px;
    object-fit: cover;
}
#btn{
    width: 80%;
    height: 100px;
    display: flex;
    margin:10px 10%;
}
button{
    flex: 1;
    height: 100px;
    background-color: aqua;
    border-radius: 20px;
    margin-left: 10px;
}
button:nth-child(1){
    margin-left: 0;
}
</style>

运行代码

在这里插入图片描述
2个客户端双向推拉流即可实现实时视频通话

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

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

相关文章

SpringBoot复习:(42)WebServerCustomizer的customize方法是在哪里被调用的?

ServletWebServletAutoConfiguration类定义如下&#xff1a; 可以看到其中通过Import注解导入了其内部类BeanPostProcessorRegister。 BeanPostProcessor中定义的registerBeanDefinition方法会被Spring容器调用。 registerBeanDefinitions方法调用了RegistrySyntheticBeanIf…

尚硅谷css3笔记

目录 一、新增长度单位 二、新增盒子属性 1.border-box 怪异盒模型 2.resize 调整盒子大小 3.box-shadow 盒子阴影 案例&#xff1a;鼠标悬浮盒子上时&#xff0c;盒子有一个过度的阴影效果 三、新增背景属性 1.background-origin 设置背景图的原点 2.background-clip 设置背…

案例18 基于Spring Boot+MyBatis的图书信息维护案例

一、案例需求 基于Spring BootMyBatis实现图书信息的新增、修改、删除、查询功能&#xff0c;并实现MySQL数据库的操作。 MySQL数据库创建图书表&#xff08;t_book&#xff09;&#xff0c;图书表有主键、图书名称、图书类别、作者、出版社、简介信息。 二、数据初始化 创建…

回归预测 | MATLAB实现GRU门控循环单元多输入多输出

回归预测 | MATLAB实现GRU门控循环单元多输入多输出 目录 回归预测 | MATLAB实现GRU门控循环单元多输入多输出预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 MATLAB实现GRU门控循环单元多输入多输出&#xff0c;数据为多输入多输出预测数据&#xff0c;输入10个…

fastApi基础

1、fastApi简介 官方文档&#xff1a;https://fastapi.tiangolo.com/ 源码&#xff1a; https://github.com/tiangolo/fastapi 2、环境准备 安装python 安装pycharm 安装fastAPI 安装 uvicorn 查看已经安装的第三方库&#xff1a;pip list 查看pip 配置信息&#xff1a;pip co…

【Apollo】赋能移动性:阿波罗自动驾驶系统的影响

前言 Apollo (阿波罗)是一个开放的、完整的、安全的平台&#xff0c;将帮助汽车行业及自动驾驶领域的合作伙伴结合车辆和硬件系统&#xff0c;快速搭建一套属于自己的自动驾驶系统。 开放能力、共享资源、加速创新、持续共赢是 Apollo 开放平台的口号。百度把自己所拥有的强大、…

以 Java NIO 的角度理解 Netty

文章目录 前言Java NIO 工作原理Selector 的创建ServerSocketChannel 的创建ServerSocketChannel 注册 Selector对事件的处理总结 前言 上篇文章《Netty 入门指南》主要涵盖了 Netty 的入门知识&#xff0c;包括 Netty 的发展历程、核心功能与组件&#xff0c;并且通过实例演示…

数据结构:栈和队列(超详细)

目录 ​编辑 栈&#xff1a; 栈的概念及结构&#xff1a; 栈的实现&#xff1a; 队列&#xff1a; 队列的概念及结构&#xff1a; 队列的实现&#xff1a; 扩展知识&#xff1a; 以上就是个人学习线性表的个人见解和学习的解析&#xff0c;欢迎各位大佬在评论区探讨&#…

UDP数据报结构分析(面试重点)

在传输层中有UDP和TCP两个重要的协议&#xff0c;下面将针对UDP数据报的结构进行分析 UDP结构图示 UDP报头结构的分析 UDP报头有4个属性&#xff0c;分别是源端口&#xff0c;目的端口&#xff0c;UDP报文长度&#xff0c;校验和&#xff0c;它们都占16位2个字节&#xff0c;所…

pycorrector一键式文本纠错工具,整合了BERT、MacBERT、ELECTRA、ERNIE等多种模型,让您立即享受纠错的便利和效果

pycorrector&#xff1a;一键式文本纠错工具&#xff0c;整合了Kenlm、ConvSeq2Seq、BERT、MacBERT、ELECTRA、ERNIE、Transformer、T5等多种模型&#xff0c;让您立即享受纠错的便利和效果 pycorrector: 中文文本纠错工具。支持中文音似、形似、语法错误纠正&#xff0c;pytho…

kaggle注册不显示验证码

edge浏览器 1.点击浏览器右上角三个点 2.点击扩展 3.点击管理扩展 4.点击获取Microsoft Edge扩展&#xff0c;在左上角输入Head Editor 5.输入https://www.azurezeng.com/static/HE-GoogleRedirect.json 6.下载后&#xff0c;点保存 成功&#xff01;

Git分布式版本控制系统基础概念

前言 我们在大学毕业写论文的时候碰到过如下的现象&#xff1a; <<毕业论文第一版.doc>> <<毕业论文第二版.doc>> <<毕业论文第三版.doc>> <<毕业论文最终版.doc>> <<毕业论文完结版.doc>> 你的论文会不断地修改…

JVM - 垃圾收集器

目录 垃圾收集器 串行垃圾收集器 并行垃圾收集器 什么是 吞吐量优先 什么是 响应时间优先 &#xff1f; CMS&#xff08;并发&#xff09;垃圾收集器 G1 垃圾收集器 垃圾收集器 垃圾收集器大概可以分为&#xff1a; 串行垃圾收集器并行垃圾收集器CMS&#xff08;并发&a…

解构软件开发中的破窗效应

目录 一、前言 二、解构破窗效应 三、如何解构&#xff1f; 一、前言 “一个房子如果窗户破了&#xff0c;没有人去修补&#xff0c;隔不久&#xff0c;其它的窗户也会莫名其妙地被人打破&#xff1b;一面墙&#xff0c;如果出现一些涂鸦没有被清洗掉&#xff0c;很快的&#x…

【Unity每日一记】资源加载相关你掌握多少?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

iview中table表头内容换行展示

如图效果图展示表头内容换行展示&#xff0c;代码如下&#xff1a; 在data中定义表头column Columns:[ {title: this.labelFn(Name, Name),key: name,align: center,}, ]在methods中定义方法 labelFn (name, str) {// 在需要换行的地方加入换行符 \n &#xff0c;在搭配最底…

mkv视频格式怎么转换为mp4?

mkv视频格式怎么转换为mp4&#xff1f;实际上&#xff0c;我们将MKV格式的文件转换成 MP4格式之后&#xff0c;能够在很大程度上提高原文件的利用率&#xff0c;也保证了文件的兼容性。很多时候&#xff0c;由于格式限制问题&#xff0c;文件在某些设备和软件上无法正常播放。所…

【内网穿透】如何实现在外web浏览器远程访问jupyter notebook服务器

文章目录 前言1. Python环境安装2. Jupyter 安装3. 启动Jupyter Notebook4. 远程访问4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5. 固定公网地址 前言 Jupyter Notebook&#xff0c;它是一个交互式的数据科学和计算环境&#xff0c;支持多种编程语言&#xff0c;如…

设计模式之构建器(Builder)C++实现

1、构建器提出 在软件功能开发中&#xff0c;有时面临“一个复杂对象”的创建工作&#xff0c;该对象的每个功能接口由于需求的变化&#xff0c;会使每个功能接口发生变化&#xff0c;但是该对象使用每个功能实现一个接口的流程是稳定的。构建器就是解决该类现象的。构建就是定…

Java课题笔记~ ServletContext

单个Servlet的配置对象 web.xml <servlet><servlet-name>FirstServlet</servlet-name><servlet-class>com.ambow.test.FirstServlet</servlet-class><init-param><param-name>charset</param-name><param-value>utf-8&…