Bootstrap

webrtc学习----前端推流拉流,局域网socket版,一对多

提示:局域网socket版,一对多

前言

‌‌‌‌‌WebRTC(Web Real-Time Communication)‌是一种实时通讯技术,允许网络应用或站点在不借助中间媒介的情况下,建立浏览器之间的点对点(Peer-to-Peer)连接,实现视频流、音频流或其他任意数据的传输。WebRTC的核心功能包括音视频的采集、编解码、网络传输和显示等‌

WebRTC的技术特点
‌1、实时通信‌:WebRTC专注于实时通信,包括音频、视频和其他数据传输‌。
‌2、点对点通信‌:WebRTC支持点对点通信,即两个浏览器之间直接建立连接,无需通过中间服务器‌。
‌3、多媒体引擎‌:WebRTC包含一个多媒体引擎,处理音频和视频流,并提供丰富的API和协议‌。
4‌、NAT穿越‌:WebRTC提供机制,使得在NAT(Network Address Translation)和防火墙等网络设备背后进行通信更为容易‌。
‌5、TURN服务器‌:当P2P连接无法建立时,WebRTC会利用TURN服务器进行数据中转,确保通信的稳定性‌

一、教程

webrtc文档

二、webrtc工作流程

// 推流拉流过程
/**
  * 推流端获取视频stream
  * 推流端生成offer  
  * 推流端通过offer设置推流LocalDescription
  * 推流端发送offer给(拉)流端
  * (拉)流端接收offer
  * (拉)流端通过offer设置(拉)流端RemoteDescription
  * (拉)流端生成answer
  * (拉)流端通过answer设置(拉)流端LocalDescription
  * (拉)流端发送answer给推流端
  * 推流端接收answer设置推流端RemoteDescription
  * 推流端发送candidate(video,audio各一次)
  * (拉)流端接收candidate
  * (拉)流端发送candidate(video,audio各一次)
  * 推流端接收candidate
  * **/

三、推流端

一个拉流RTCPeerConnection,对应一个推流RTCPeerConnection
X 个拉流RTCPeerConnection,对应X 个推流RTCPeerConnection

push.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>推流</title>
</head>
<body>
  <video id="webrtcVideo" autoplay></video>
  <script>
    const video = document.getElementById('webrtcVideo');
    // webscoket
    const ws = new WebSocket('ws://127.0.0.1:1990'); // 可换成局域网ip地址
    let videoStream;
    // 一个拉流RTCPeerConnection对应一个推流RTCPeerConnection,xx个拉流RTCPeerConnection,对应xx个推流RTCPeerConnection
    const pushPool = {};
    // rtc connection
    let pushRtcCon;
    // 打开摄像头,video标签播放视频流
    const getStream = async () => {
      if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)console.log('不支持:getUserMedia');
      const stream = await navigator.mediaDevices.getUserMedia({video:true});
      video.srcObject = stream;
      videoStream = stream;
    }
    getStream();
    // 开始推流
    const startPush = (pullId) => {
      if(!pushPool[pullId])pushPool[pullId] = pushRtcCon = new RTCPeerConnection();
      // rtc connection 添加track
      videoStream.getVideoTracks().forEach(track => {
        pushRtcCon.addTrack(track,videoStream);
      });
      // 监听icecandidate
      pushRtcCon.onicecandidate = (event)=>{
        if(event.candidate)ws.send(JSON.stringify({type:'candidate',candidate:event.candidate,id:pullId}))
      }
      // 创建offer
      pushRtcCon.createOffer().then(offer=>{
        console.log(offer)
        // 设置推流LocalDescription
        pushRtcCon.setLocalDescription(offer).then(()=>{ console.log('推流设置LocalDescription成功');});
        // offer信息发送给拉流
        ws.send(JSON.stringify({type:'offer',id:pullId,offer}))
      });
    }
    // 开启websocket服务
    ws.addEventListener('open',()=>{
      // 初始化推流通道
      ws.send(JSON.stringify({type:'push_init'}))
      console.log('websocket连接成功')
    });
    // 接收wenbscoket信息
    ws.addEventListener('message', (event) => {
      let data = JSON.parse(event.data);
      console.log(data)
      // 接收到拉流传来的answer 设置推流RemoteDescription
      if(data.type == 'answer')pushRtcCon.setRemoteDescription(data.answer).then(()=>{ console.log('推流设置RemoteDescription成功');});
      // 接收拉流candidate 推流rtc connection 添加IceCandidate
      if(data.type == 'candidate'&&data.candidate)pushRtcCon.addIceCandidate(data.candidate).then(()=>{ console.log('推流添加candidate成功');});
      // 接收拉流开启消息 开始推流
      if(data.type == 'pull_start')startPush(data.id);
    })
  </script>
</body>
</html>

四、拉流

pull.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <video id="pullVideo" autoplay preload muted></video>
  <div id="pullBtn">拉流</div>
  <script>
    const pullBtn = document.getElementById('pullBtn');
    // 开始拉流
    const startPll = () =>{
      let ws = new WebSocket('ws://127.0.0.1:1990'); // 可换成局域网ip地址
      const pullVideo = document.getElementById('pullVideo');
      let pullStrem;
      // 拉流rtc connection
      const pullRtcCon = new RTCPeerConnection();
      const pullID = new Date().getTime()+'io'+Math.round(Math.random()*10000);
      // 拉流监听icecandidate
      pullRtcCon.onicecandidate = (event)=>{
        // 接收到icecandidate  发送candidate给推流端
        if(event.candidate)ws.send(JSON.stringify({type:'candidate',candidate:event.candidate,num:1,id:pullID}))
      }
      // 监听track
      pullRtcCon.addEventListener('track' ,(event) => {
        pullStrem = event.streams[0];
        pullVideo.srcObject = event.streams[0];
      })
      // 打开webscoket
      ws.addEventListener('open',async ()=>{
        await ws.send(JSON.stringify({type:'pull_init',id:pullID}));
        // 通知推流端,开始推流
        ws.send(JSON.stringify({type:'pull_start',id:pullID}));
        console.log('websocket连接成功')
      });
      // 监听webscoket消息
      ws.addEventListener('message',(event)=>{
        let data = JSON.parse(event.data);
        // 接收到推流端offer
        console.log(data,'????')
        if(data.type == 'offer'){
          // 设置拉流端 RemoteDescription
          pullRtcCon.setRemoteDescription(data.offer).then(()=>{
            console.log('拉流设置RemoteDescription成功')
            // 创建answer
            pullRtcCon.createAnswer(data.offer).then((answer)=>{
              // 设置拉流的LocalDescription
              pullRtcCon.setLocalDescription(answer).then(()=>{
                console.log('拉流设置LocalDescription成功')
              });
              // 发送answer到推流端
              ws.send(JSON.stringify({type:'answer',answer,id:pullID}))
            });
          });
        }
        // 接收推流端candidate  拉流端添加IceCandidate
        if(data.type == 'candidate')pullRtcCon.addIceCandidate(data.candidate).then(()=>{ console.log('拉流添加candidate成功');});
      })
    }
    // 拉流按钮点击事件
    pullBtn.addEventListener('click',startPll)
  </script>
</body>
</html>

五、socket服务

安装依赖

npm init
npm install nodejs-websocket -S

index.js

const ws = require('nodejs-websocket');
const port = '1990';

// 推流通道  拉流通道
let wsPush,wsPull,pullPool={};
const server = ws.createServer((connection)=>{
  // websocket 连接接收数据
  connection.on('text',(msg)=>{
    let data = JSON.parse(msg);
    // 初始化推流websocket
    if(data.type == 'push_init')wsPush = connection;
    // 初始化拉流websocket
    if(data.type == 'pull_init')if(!pullPool[data.id]) pullPool[data.id] = connection;
    // 接收推流消息 发送给拉流
    if(connection == wsPush&&pullPool[data.id])pullPool[data.id].send(msg);
    // 接收拉流消息 发送给推流
    for(let key in pullPool){
      if(connection == pullPool[key]&&wsPush)wsPush.send(msg);
    }
  })
  // websocket 关闭
  connection.on('close',()=>{
    wsPush = null;wsPull = null;
    console.log('通道关闭')
  })
  // websocket 报错
  connection.on('err',(err)=>{
    wsPush = null;wsPull = null;
    console.log('通道报错:'+err)
  })
})
server.listen(port,console.log('ws启动成功,127.0.0.1:'+port));

六、效果

推流端
在这里插入图片描述
拉流端(点击拉流按钮)
在这里插入图片描述

七、备注

1、socket地址可换成局域网IP地址访问
2、pull来流请求地址可换成局域网IP地址访问

总结

踩坑路漫漫长@~@

;