使用netty 实现webrtc 协议

使用netty 实现webrtc 协议

第一步 获取媒体信息

在针对 Web 进行开发时,WebRTC 标准提供了用于访问连接到计算机或智能手机的摄像头和麦克风的 API。这些设备通常称为“媒体设备”,可通过实现 MediaDevices 接口的 navigator.mediaDevices 对象使用 JavaScript 进行访问。最常见的用法是通过函数 getUserMedia(),该函数会返回一个针对匹配的媒体设备解析为 MediaStream 的 promise。

<video id="localVideo" autoplay></video>
const localVideo = document.getElementById("localVideo");
const constraints = {
    //获取图像
    'video': true,
    //获取音频
    'audio': true
}
navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
        console.log('Got MediaStream:');
    	//将获取到的媒体流和 localVideo 进行绑定显示
    	localVideo.srcObject = localStream;
    })
    .catch(error => {
        console.error('Error accessing media devices.', error);
    });

这样就可以在网页中看到摄像头捕获的内容了。

注意 navigator.mediaDevices.getUserMedia 这个可以设置很多属性,这里只是获取摄像头和麦克风,有兴趣可以去官网看看 https://webrtc.org/getting-started/media-capture-and-constraints?hl=zh-cn

第二步 建立对等连接

媒体协商

一对一通信中,发起方发送的 SDP 称为Offer(提议),接收方发送的 SDP 称为Answer(应答)。

每端保持两个描述:描述本身的本地描述LocalDescription,描述呼叫的远端的远程描述RemoteDescription

当通信双方 RTCPeerConnection 对象创建完成后,就可以进行媒体协商了,大致过程如下:

webrtc 的连接建立过程大概如下

  1. 发起方创建 offer ,保存为本地描述,发送给信令服务器,由信令服务器转发给接收方
  2. 接收方接收 offer ,保存远端描述,创建 answer ,发送给信令服务器,由信令服务器转发给发起方
  3. 发起方接收 answer,保存为远端描述。
  4. 整个媒体协商过程处理完毕。这样设置了本地和远程会话说明之后,他们就会了解远程对等方的功能。这并不意味着对等设备之间的连接已准备就绪。还需要收集 ice 候选项,相互交换后才能成功建立连接

这里就是使用 Netty 来与浏览器建立 websocket 连接作为信令服务器。

后端完整代码:

主要就是实现 websocket 连接,和处理转发 offeranswerice 候选项

使用 WebSocketServerProtocolHandler 可以很容易实现 websocket 的建立主要就是转发内容

用户需要先与服务器建立连接,并绑定信息,服务器将用户信息与channel保存下来,然后使用这些信息来转发 offer answer ice等内容,其实这里netty作为信令服务器就只需要对消息进行转发就行了。主要还是前端代码。

public class Test {
	public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boss = new NioEventLoopGroup(2);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try{
            ChannelFuture sync = new ServerBootstrap()
                    .channel(NioServerSocketChannel.class)
                    .group(boss, worker)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new HttpServerCodec());
                            pipeline.addLast(new  WebSocketServerProtocolHandler("/webrtc"));
                            pipeline.addLast(new WebRTCHandler());
                        }
                    }).bind(new InetSocketAddress("localhost",8080)).sync();

            ChannelFuture channelFuture = sync.channel().closeFuture().sync();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
        }
}
//主要是 WebRTCHandler
class WebRTCHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    private static ConcurrentMap<String,Channel> channelConcurrentMap = new ConcurrentHashMap<>();
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 处理WebSocket消息
        if (frame instanceof TextWebSocketFrame) {
            // 处理文本消息
            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
            String text = textFrame.text();
            JSONObject jsonObject = JSON.parseObject(text);
            //处理offer
            if (jsonObject.get("type").equals("offer")){
                System.out.println("处理 offer");
                String remote = (String) jsonObject.get("remote");
                extracted(text, remote);
            }
            //处理answer
            else if (jsonObject.get("type").equals("answer")){
                String remote = (String) jsonObject.get("remote");
                extracted(text, remote);
            }
            else if (jsonObject.get("type").equals("bind")){
                channelConcurrentMap.put((String) jsonObject.get("id"),ctx.channel());
                ctx.channel().writeAndFlush(JSON.toJSONString(new JSONObject().put("data","成功")));
            }  else if (jsonObject.get("type").equals("send")){
                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(new JSONObject().put("data","ok"))));
            }else if (jsonObject.get("type").equals("iceCandidate")){
                System.out.println("处理 answer");
                String remote = (String) jsonObject.get("remote");
                extracted(text, remote);
            }
        }
    }
    private void extracted(String text, String remote) {
        //将offer 发送给对方
        Channel channel = channelConcurrentMap.get(remote);
        channel.writeAndFlush(new TextWebSocketFrame(text));
    }
}

前端:

注意,这里需要双方都点击 bind 按钮,也就是与netty建立连接后,先点击发起方的 start 按钮,再点击 接收方的 start 按钮,才能正常运行

发起方

这里将各个部分的代码分开展示,后面接收端的完整代码

html 代码

  <video id="localVideo" autoplay></video>
  <video id="remoteVideo" autoplay></video>
  <button id="startButton">Start</button>
  <button id="stopButton">Stop</button>
  <button id="bind">bind</button>

获取网页元素,定义常量

    const localVideo = document.getElementById("localVideo");
    const remoteVideo = document.getElementById("remoteVideo");
    const startButton = document.getElementById("startButton");
    const stopButton = document.getElementById("stopButton");
    const bindButton = document.getElementById("bind");
    const sendButton = document.getElementById("sendButton");

    let localStream;
    let remoteStream;

    startButton.addEventListener("click", start);
    stopButton.addEventListener("click", stop);
    bindButton.addEventListener("click", bind);  
	let socket;

先建立websocket连接,并发送绑定消息,主要 webrtc 协议并不是只能依靠 websocket 协议,只是这里使用 socket 协议实现的。后面发送消息的时候,都会带上对方的 id 服务器用于选择用户发送。


    function bind() {
      socket = new WebSocket("ws://localhost:8080/webrtc");
      socket.onopen = () => {
        //发送一个绑定消息,后端服务器用于区分channel
        console.log("bind 1");
        socket.send(JSON.stringify({ type: "bind", id: "1" }));
      };
      socket.onmessage = async (event) => {
        //接收offer创建answer并发送
        console.log("接收到消息" + event);
        const data = JSON.parse(event.data);
        console.log("data " + data);
      };
    }

开始进行 webrtc 连接,下面这些 addEventListener 都是用于监听事件都写在前面,他们只有在特定的条件下才会触发,并不影响执行顺序,还是双方交换 offer,answer,ice 后才会建立连接

    async function start() {
      localStream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
      });
	 //将捕捉到的流,与本地 video 进行绑定,就可以看到本地画面了
      localVideo.srcObject = localStream;
      const configuration = {
      };
      const peerConnection = new RTCPeerConnection(configuration);

      // 要收集 ICE 候选对象,只需为 icecandidate 事件添加监听器即可。针对该监听器发出的 RTCPeerConnectionIceEvent 将包		含 candidate 属性,该属性表示应发送到远程对等端的新候选音频
      peerConnection.addEventListener("icecandidate", (event) => {
        console.log('触发 ' + event)
        if (event.candidate) {
            //如果收集到了候选项就发送给对方
          console.log("candidate" + event.candidate);
          socket.send(
            JSON.stringify({
              type: "iceCandidate",
              iceCandidate: event.candidate,
              remote: "2",
            })
          );
        } else {
          /* 在此次协商中,没有更多的候选了 */
          console.log("没有候选项");
        }
      });
      //监听是否连接成功 connectionstatechange 事件
      peerConnection.addEventListener('connectionstatechange', event => {
        if (peerConnection.connectionState === 'connected') {
          console.log("连接已建立!")
        }
      });
      peerConnection.addEventListener("icecandidateerror", (event) => {
        console.log('触发err ' + event)
      });

      //监听 track 事件,RTCTrackEvent 包含一个 MediaStream 对象数组,这些对象与对等项的相应本地数据流具有相同的 				MediaStream.id 值,说白了也就是对方的音频流信息
      peerConnection.addEventListener('track', async (event) => {
        const [remoteStream] = event.streams;
        //将接收到的流信息在 video 上面绑定进行显示
        remoteVideo.srcObject = remoteStream;
      });

      //将本地流添加到  peerConnection 会开始收集 Ice候选项
      localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));


      //创建offer
      const offer = await peerConnection.createOffer();
      //保存自己的 本地说明
      await peerConnection.setLocalDescription(offer);

      //发送offer给服务器
      socket.send(
        JSON.stringify({ type: "offer", offer: offer, remote: "2" })
      );
      console.log("发送" + offer);

      socket.onmessage = async (event) => {
        //接收offer创建answer并发送
        console.log("接收到消息" + event);
        const data = JSON.parse(event.data);
        console.log("state " + peerConnection.connectionState)
        console.log("data " + data);
        if (data.type === "answer") {
          console.log("answer " + data.answer.sdp);
          const remoteDesc = new RTCSessionDescription(data.answer);
          //保存远端描述
          await peerConnection.setRemoteDescription(remoteDesc);
          //双方相互交换描述后,获取 iec 候选, 进行交换, 等ice候选项交换完后连接就建立了
        } else if (data.type === "iceCandidate") {
          // 处理ICE候选
          await peerConnection.addIceCandidate(data.iceCandidate);
        }
      };
    }

接收端

<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC Video Chat</title>
  </head>
  <body>
    <video id="localVideo" autoplay></video>
    <video id="remoteVideo" autoplay></video>
    <button id="startButton">Start</button>
    <button id="stopButton">Stop</button>
    <button id="bind"">bind</button>
    <script>
      const localVideo = document.getElementById("localVideo");
      const remoteVideo = document.getElementById("remoteVideo");
      const startButton = document.getElementById("startButton");
      const stopButton = document.getElementById("stopButton");
      const bindButton = document.getElementById("bind");

      let localStream;
      let remoteStream;

      startButton.addEventListener("click", start);
      stopButton.addEventListener("click", stop);
      bindButton.addEventListener("click", bind);

      var socket;
      let offer;

      function bind(){
            socket = new WebSocket("ws://localhost:8080/webrtc");
            socket.onopen = () => {
                  //发送一个绑定消息,后端服务器用于区分channel
                  console.log("bind 2")
                socket.send(JSON.stringify({ type: "bind", id: "2" }));
        };

        socket.onmessage = async (event) => {
            //接收offer创建answer并发送
          const data = JSON.parse(event.data);
          if (data.type === "offer") {
            console.log("offer:"+data.offer.sdp)
            offer = data.offer;
            }else if (data.type === "candidate") {
            // 处理ICE候选
            console.log("data.iceCandidate "+data.iceCandidate)
            await peerConnection.addIceCandidate(data.iceCandidate);
          }
        };

      }
      async function start() {
        localStream = await navigator.mediaDevices.getUserMedia({
          video: true,
          audio: true,
        });
        localVideo.srcObject = localStream;
        const configuration = {
        
      };
        const peerConnection = new RTCPeerConnection(configuration);

        peerConnection.addEventListener('icecandidate', event => {
          if (event.candidate) {
            console.log(event.candidate)
            //发送自己的
            socket.send(JSON.stringify({ type: "iceCandidate", iceCandidate: event.candidate, remote:"1" }));
            }
         });
        peerConnection.addEventListener('track', async (event) => {
        const [remoteStream] = event.streams;
        remoteVideo.srcObject = remoteStream;
      });
        //将本地流添加到  peerConnection
         localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
          // 处理offer,创建answer
          //保存远端 信息
        peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
        //创建answer
        const answer = await peerConnection.createAnswer();
        console.log("answer "+answer)
        //保存本地信息 
        await peerConnection.setLocalDescription(answer);
        //返回answer发送
        socket.send(JSON.stringify({ type: "answer", answer: answer, remote:"1" }));
      }

      function stop() {
        // 停止本地视频流
        localStream.getTracks().forEach((track) => track.stop());
      }
    </script>
  </body>
</html>

运行效果

在这里插入图片描述

参考文章:https://zhuanlan.zhihu.com/p/662875601

webrtc 网站:https://webrtc.org/?hl=zh-cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值