【前后端的那些事】webrtc入门demo(代码)

news2024/11/18 4:24:06

文章目录

  • 前端代码
    • api
    • vue界面
  • 后端
    • model
    • websocket
    • config
    • resource

龙年到了,先祝福各位龙年快乐,事业有成!

最近在搞webrtc,想到【前后端的那些事】好久都没有更新了,所以打算先把最近编写的小demo发出来。

p2p webrtc的demo在编写的时需要编写人员以不同的客户端角度出发编写代码,因此对编码造成一定的障碍,详细的介绍文章不是特别好写,所以我打算先把demo代码先分享出来,后续再进一步整理

效果

在这里插入图片描述

前端代码

api

/src/api/webrtc.ts

export const SIGNAL_TYPE_JOIN = "join";
export const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
export const SIGNAL_TYPE_LEAVE = "leave";
export const SIGNAL_TYPE_NEW_PEER = "new-peer";
export const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
export const SIGNAL_TYPE_OFFER = "offer";
export const SIGNAL_TYPE_ANSWER = "answer";
export const SIGNAL_TYPE_CANDIDATE = "candidate";

export class Message {
  userId: string;
  roomId: string;
  remoteUserId: string;
  data: any;
  cmd: string;

  constructor() {
    this.roomId = "1";
  }
}

export default {
  SIGNAL_TYPE_JOIN,
  SIGNAL_TYPE_RESP_JOIN,
  SIGNAL_TYPE_LEAVE,
  SIGNAL_TYPE_NEW_PEER,
  SIGNAL_TYPE_PEER_LEAVE,
  SIGNAL_TYPE_OFFER,
  SIGNAL_TYPE_ANSWER,
  SIGNAL_TYPE_CANDIDATE
}

vue界面

/src/views/welecome/index.vue

<script setup lang="ts">
import { ref, onMounted } from "vue";
import {
  Message,
  SIGNAL_TYPE_JOIN,
  SIGNAL_TYPE_NEW_PEER,
  SIGNAL_TYPE_RESP_JOIN,
  SIGNAL_TYPE_OFFER,
  SIGNAL_TYPE_ANSWER,
  SIGNAL_TYPE_CANDIDATE
} from "@/api/webrtc";

// 链接websocket
const userId = ref<string>(Math.random().toString(36).substr(2));
const remoteUserId = ref<string>();
const ws = new WebSocket("ws://localhost:1000/ws/" + userId.value);

const localVideo = ref<HTMLVideoElement>();
const localStream = ref<MediaStream>();

const remoteVideo = ref<HTMLVideoElement>();
const remoteStream = ref<MediaStream>();

const pc = ref<RTCPeerConnection>();

onMounted(() => {
  localVideo.value = document.querySelector("#localVideo");
  remoteVideo.value = document.querySelector("#remoteVideo");
})

ws.onopen = (ev: Event) => {
  console.log("连接成功 userId = " + userId.value);
}

ws.onmessage = (ev: MessageEvent) => {
  const data = JSON.parse(ev.data);
  if (data.cmd === SIGNAL_TYPE_NEW_PEER) {
    handleNewPeer(data);
  } else if (data.cmd === SIGNAL_TYPE_RESP_JOIN) {
    handleRespJoin(data);
  } else if (data.cmd === SIGNAL_TYPE_OFFER) {
    handleRemoteOffer(data);
  } else if (data.cmd === SIGNAL_TYPE_ANSWER) {
    handleRemoteAnswer(data);
  } else if (data.cmd === SIGNAL_TYPE_CANDIDATE) {
    handleRemoteCandidate(data);
  }
}

ws.onclose = (ev) => {
  console.log("连接关闭 userId = " + userId.value);
}

const handleRemoteCandidate = (msg : Message) => {
  console.log("handleRemoteCandidate...");
  // 保存远程cadidate
  pc.value.addIceCandidate(msg.data);
}

/**
 * 处理远端发送来的answer 
 */
const handleRemoteAnswer = (msg : Message) => {
  console.log("handleRemoteAnswer...");
  // 保存远端发送的answer(offer)
  pc.value.setRemoteDescription(msg.data);
}

/**
 * 处理对端发送过来的offer, 并且发送answer(offer) 
 * @param msg 
 */
const handleRemoteOffer = async (msg : Message) => {
  console.log("handleRemoteOffer...");

  // 存储对端的offer
  pc.value.setRemoteDescription(msg.data);
  // 创建自己的offer(answer)
  const answer = await pc.value.createAnswer();
  // 保存本地offer
  pc.value.setLocalDescription(answer);
  // 转发answer
  const answerMsg = new Message();
  answerMsg.userId = userId.value;
  answerMsg.remoteUserId = remoteUserId.value;
  answerMsg.cmd = SIGNAL_TYPE_ANSWER;
  answerMsg.data = answer;
  console.log("发送answer...");
  ws.send(JSON.stringify(answerMsg));
}

/**   
 * 创建offer,设置本地offer并且发送给对端 
 */
const handleNewPeer = async (msg : Message) => {
  console.log("handleNewPeer...");
  // 存储对端用户id
  remoteUserId.value = msg.remoteUserId;
  // todo:
  // 创建offer
  const offer = await pc.value.createOffer()
  // 本地存储offer
  pc.value.setLocalDescription(offer);
  // 转发offer
  const offerMsg = new Message();
  offerMsg.userId = userId.value;
  offerMsg.remoteUserId = remoteUserId.value;
  offerMsg.data = offer;
  offerMsg.cmd = SIGNAL_TYPE_OFFER;
  console.log("发送offer...");
  ws.send(JSON.stringify(offerMsg));
}

const handleRespJoin = (msg: Message) => {
  console.log("handleRespJoin...");
  console.log(msg);
  remoteUserId.value = msg.remoteUserId;
}

const join = async () => {
  // 初始化视频流
  console.log(navigator.mediaDevices);
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true
  })
  localVideo.value!.srcObject = stream
  localVideo.value!.play()
  localStream.value = stream;

  // 创建pc
  createPeerConn();

  // 加入房间
  doJoin();
}

const doJoin = () => {
  // 创建信息对象
  const message = new Message();
  message.cmd = SIGNAL_TYPE_JOIN;
  message.userId = userId.value;
  const msg = JSON.stringify(message);

  // send message
  ws.send(msg);
}

/**
 * 创建peerConnection
 */
const createPeerConn = () => {
  pc.value = new RTCPeerConnection();

  // 将本地流的控制权交给pc
  // const tracks = localStream.value.getTracks()
  // for (const track of tracks) {
  //   pc.value.addTrack(track); 
  // } 
  localStream.value.getTracks().forEach(track => {
    pc.value.addTrack(track, localStream.value);
  });


  pc.value.onicecandidate = (event : RTCPeerConnectionIceEvent) => {
    if (event.candidate) {
      // 发送candidate
      const msg = new Message();
      msg.data = event.candidate;
      msg.userId = userId.value;
      msg.remoteUserId = remoteUserId.value;
      msg.cmd = SIGNAL_TYPE_CANDIDATE;
      console.log("onicecandidate...");
      console.log(msg);
      ws.send(JSON.stringify(msg));
    } else {
      console.log('candidate is null');
    }
  }

  pc.value.ontrack = (event: RTCTrackEvent) => {
    console.log("handleRemoteStream add...");
    // 添加远程的stream
    remoteVideo.value.srcObject = event.streams[0];
    remoteStream.value = event.streams[0];
  }

  pc.value.onconnectionstatechange = () => {
    if(pc != null) {
        console.info("ConnectionState -> " + pc.value.connectionState);
    }
  };

  pc.value.oniceconnectionstatechange = () => {
    if(pc != null) {
        console.info("IceConnectionState -> " + pc.value.iceConnectionState);
  }
}
}
</script>

<template>
  <el-button @click="join">加入</el-button>
  <div id="videos">
    <video id="localVideo" autoplay muted playsinline>本地窗口</video>
    <video id="remoteVideo" autoplay playsinline>远端窗口</video>
  </div>
</template>

后端

model

Client.java

import lombok.Data;

import javax.websocket.Session;

@Data
public class Client {
    private String userId;
    private String roomId;
    private Session session;
}

Message.java

import lombok.Data;

@Data
public class Message {
    private String userId;
    private String remoteUserId;
    private Object data;
    private String roomId;
    private String cmd;

    @Override
    public String toString() {
        return "Message{" +
                "userId='" + userId + '\'' +
                ", remoteUserId='" + remoteUserId + '\'' +
                ", roomId='" + roomId + '\'' +
                ", cmd='" + cmd + '\'' +
                '}';
    }
}

Constant.java

public interface Constant {
     String SIGNAL_TYPE_JOIN = "join";
     String SIGNAL_TYPE_RESP_JOIN = "resp-join";
     String SIGNAL_TYPE_LEAVE = "leave";
     String SIGNAL_TYPE_NEW_PEER = "new-peer";
     String SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
     String SIGNAL_TYPE_OFFER = "offer";
     String SIGNAL_TYPE_ANSWER = "answer";
     String SIGNAL_TYPE_CANDIDATE = "candidate";
}

websocket

WebSocket.java

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fgbg.webrtc.model.Client;
import com.fgbg.webrtc.model.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static com.fgbg.webrtc.model.Constant.*;

@Component
@Slf4j
@ServerEndpoint("/ws/{userId}")
public class WebSocket {
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Client client;

    // 存储用户
    private static Map<String, Client> clientMap = new ConcurrentHashMap<>();

    // 存储房间
    private static Map<String, Set<String>> roomMap = new ConcurrentHashMap<>();

    // 为了简化逻辑, 只有一个房间->1号房间
    static {
        roomMap.put("1", new HashSet<String>());
    }

    private ObjectMapper objectMapper = new ObjectMapper();

    @OnOpen
    public void onOpen(Session session, @PathParam(value="userId")String userId) {
        log.info("userId = " + userId + " 加入房间1");
        Client client = new Client();
        client.setRoomId("1");
        client.setSession(session);
        client.setUserId(userId);
        this.client = client;
        clientMap.put(userId, client);
    }

    @OnClose
    public void onClose() {
        String userId = client.getUserId();
        clientMap.remove(userId);
        roomMap.get("1").remove(userId);
        log.info("userId = " + userId + " 退出房间1");
    }

    @OnMessage
    public void onMessage(String message) throws JsonProcessingException {
        // 反序列化message
        log.info("userId = " + client.getUserId() + " 收到消息");
        Message msg = objectMapper.readValue(message, Message.class);
        switch (msg.getCmd()) {
            case SIGNAL_TYPE_JOIN:
                handleJoin(message, msg);
                break;
            case SIGNAL_TYPE_OFFER:
                handleOffer(message, msg);
                break;
            case SIGNAL_TYPE_ANSWER:
                handleAnswer(message, msg);
                break;
            case SIGNAL_TYPE_CANDIDATE:
                handleCandidate(message, msg);
                break;
        }
    }

    /**
     * 转发candidate
     * @param message
     * @param msg
     */
    private void handleCandidate(String message, Message msg) throws JsonProcessingException {
        System.out.println("handleCandidate msg = " + msg);
        String remoteId = msg.getRemoteUserId();
        sendMsgByUserId(msg, remoteId);
    }

    /**
     * 转发answer
     * @param message
     * @param msg
     */
    private void handleAnswer(String message, Message msg) throws JsonProcessingException {
        System.out.println("handleAnswer msg = " + msg);
        String remoteId = msg.getRemoteUserId();
        sendMsgByUserId(msg, remoteId);
    }

    /**
     * 转发offer
     * @param message
     * @param msg
     */
    private void handleOffer(String message, Message msg) throws JsonProcessingException {
        System.out.println("handleOffer msg = " + msg);
        String remoteId = msg.getRemoteUserId();
        sendMsgByUserId(msg, remoteId);
    }

    /**
     * 处理加入房间逻辑
     * @param message
     * @param msg
     */
    private void handleJoin(String message, Message msg) throws JsonProcessingException {
        String roomId = msg.getRoomId();
        String userId = msg.getUserId();

        System.out.println("userId = " + msg.getUserId() + " join 房间" + roomId);
        // 添加到房间内
        Set<String> room = roomMap.get(roomId);
        room.add(userId);

        if (room.size() == 2) {
            String remoteId = null;
            for (String id : room) {
                if (!id.equals(userId)) {
                    remoteId = id;
                }
            }
            // 通知两个客户端
            // resp-join
            Message respJoinMsg = new Message();
            respJoinMsg.setUserId(userId);
            respJoinMsg.setRemoteUserId(remoteId);
            respJoinMsg.setCmd(SIGNAL_TYPE_RESP_JOIN);
            sendMsgByUserId(respJoinMsg, userId);

            // new-peer
            Message newPeerMsg = new Message();
            newPeerMsg.setUserId(remoteId);
            newPeerMsg.setRemoteUserId(userId);
            newPeerMsg.setCmd(SIGNAL_TYPE_NEW_PEER);
            sendMsgByUserId(newPeerMsg, remoteId);

        }else if (room.size() > 2) {
            log.error("房间号" + roomId + " 人数过多");
            return;
        }
    }

    /**
     * 根据远端用户id, 转发信息
     */
    private void sendMsgByUserId(Message msg, String remoteId) throws JsonProcessingException {
        Client client = clientMap.get(remoteId);
        client.getSession().getAsyncRemote().sendText(objectMapper.writeValueAsString(msg));
        System.out.println("信息转发: " + msg);
    }
}

config

WebSocketConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    /**
     * 	注入ServerEndpointExporter,
     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    
}

resource

application.yml

server:
  port: 1000

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

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

相关文章

Android 移动应用开发 创建第一个Android项目

文章目录 一、创建第一个Android项目1.1 准备好Android Studio1.2 运行程序1.3 程序结构是什么app下的结构res - 子目录&#xff08;所有图片、布局、字AndroidManifest.xml 有四大组件&#xff0c;程序添加权限声明 Project下的结构 二、开发android时&#xff0c;部分库下载异…

【Dubbo源码二:Dubbo服务导出】

入口 Dubbo服务导出的入口&#xff1a;服务导出是在DubboBootstrapApplicationListener在监听到ApplicationContextEvent的ContextRefreshedEvent事件后&#xff0c;会触发dubboBootstrap.start(), 在这个方法中最后会导出Dubbo服务 DubboBootstrapApplicationListener Dub…

【北邮鲁鹏老师计算机视觉课程笔记】03 edge 边缘检测

【北邮鲁鹏老师计算机视觉课程笔记】03 1 边缘检测 有几种边缘&#xff1f; ①实体上的边缘 ②深度上的边缘 ③符号的边缘 ④阴影产生的边缘 不同任务关注的边缘不一样 2 边缘的性质 边缘在信号突变的地方 在数学上如何寻找信号突变的地方&#xff1f;导数 用近似的方法 可以…

【DDD】学习笔记-领域模型与函数范式

函数范式 REA 的 Ken Scambler 认为函数范式的主要特征为&#xff1a;模块化&#xff08;Modularity&#xff09;、抽象化&#xff08;Abstraction&#xff09;和可组合&#xff08;Composability&#xff09;&#xff0c;这三个特征可以帮助我们编写简单的程序。 通常&#…

电商网站基础布局——以小兔鲜为例

项目准备 /* base.css */ /* 內减模式 */ * {margin: 0;padding: 0;box-sizing: border-box; }/* 设置网页统一的字体大小、行高、字体系列相关属性 */ body {font: 16px/1.5 "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei","Hiragino Sans…

编码安全风险是什么,如何进行有效的防护

2011年6月28日晚20时左右&#xff0c;新浪微博突然爆发XSS&#xff0c;大批用户中招&#xff0c;被XSS攻击的用户点击恶意链接后并自动关注一位名为HELLOSAMY的用户&#xff0c;之后开始自动转发微博和私信好友来继续传播恶意地址。不少认证用户中招&#xff0c;也导致该XSS被更…

【深蓝学院】移动机器人运动规划--第4章 动力学约束下的运动规划--笔记

0. Outline 1. Introduction 什么是kinodynamic&#xff1f; 运动学&#xff08;Kinematics&#xff09;和动力学&#xff08;Dynamics&#xff09;都是力学的分支&#xff0c;涉及物体的运动&#xff0c;但它们研究的焦点不同。 运动学专注于描述物体的运动&#xff0c;而…

反应式编程

反应式编程 前言1 反应式编程概览2 初识 Reactor2.1 绘制反应式流图2.2 添加 Reactor 依赖 3.使用常见的反应式操作3.1 创建反应式类型3.2 组合反应式类型3.3 转换和过滤反应式流3.4 在反应式类型上执行逻辑操作 总结 前言 你有过订阅报纸或者杂志的经历吗?互联网的确从传统的…

第66讲管理员登录功能实现

项目样式初始化 放assets目录下&#xff1b; border.css charset "utf-8"; .border, .border-top, .border-right, .border-bottom, .border-left, .border-topbottom, .border-rightleft, .border-topleft, .border-rightbottom, .border-topright, .border-botto…

WWW 万维网

万维网概述 万维网 WWW (World Wide Web) 并非某种特殊的计算机网络。 万维网是一个大规模的、联机式的信息储藏所。 万维网用链接的方法能非常方便地从互联网上的一个站点访问另一个站点&#xff0c;从而主动地按需获取丰富的信息。 这种访问方式称为“链接”。 万维网是分…

线上编程答疑解惑回顾,初学编程中文编程在线屏幕共享演示

线上编程答疑解惑回顾&#xff0c;初学编程中文编程在线屏幕共享演示 一、学编程过程中有不懂的怎么办&#xff1f; 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载——常用工具下载——编…

Python入门知识点分享——(二十)继承和方法重写

今天是大年三十&#xff0c;祝大家龙年大吉&#xff0c;当然无论何时何地&#xff0c;我们都不要忘记继续学习。今天介绍的是继承和方法重写这两种面向对象编程特点。继承机制指的是&#xff0c;一个类&#xff08;我们称其为子类或派生类&#xff09;可以使用另一个类&#xf…

无心剑中译佚名《春回大地》

The Coming of Spring 春回大地 I am coming, little maiden, With the pleasant sunshine laden, With the honey for the bee, With the blossom for the tree. 我来啦&#xff0c;小姑娘 满载着欣悦的阳光 蜂儿有蜜酿 树儿有花绽放 Every little stream is bright, All …

【Leetcode】LCP 30. 魔塔游戏

文章目录 题目思路代码结果 题目 题目链接 小扣当前位于魔塔游戏第一层&#xff0c;共有 N 个房间&#xff0c;编号为 0 ~ N-1。每个房间的补血道具/怪物对于血量影响记于数组 nums&#xff0c;其中正数表示道具补血数值&#xff0c;即血量增加对应数值&#xff1b;负数表示怪…

Apache Zeppelin 整合 Spark 和 Hudi

一 环境信息 1.1 组件版本 组件版本Spark3.2.3Hudi0.14.0Zeppelin0.11.0-SNAPSHOT 1.2 环境准备 Zeppelin 整合 Spark 参考&#xff1a;Apache Zeppelin 一文打尽Hudi0.14.0编译参考&#xff1a;Hudi0.14.0 最新编译 二 整合 Spark 和 Hudi 2.1 配置 %spark.confSPARK_H…

moduleID的使用

整个平台上有很多相同的功能&#xff0c;但是需要不同的内容。例如各个模块自己的首页上有滚动新闻、有友好链接等等。为了公用这些功能&#xff0c;平台引入了moduleID的解决方案。 在前端的配置文件中&#xff0c;配置了模块号&#xff1a; 前端页面请求滚动新闻时&#xff0…

Sam Altman计划筹集5至7万亿美元;OPPO发布AI时代新功能

&#x1f989; AI新闻 &#x1f680; Sam Altman计划筹集5至7万亿美元&#xff0c;建立全球芯片帝国 摘要&#xff1a;Sam Altman宣布计划筹集5至7万亿美元来建立全球芯片帝国&#xff0c;以满足日益增长的AI基础设施需求。他已在全球寻求资金&#xff0c;包括中东土豪。此外…

Flume拦截器使用-实现分表、解决零点漂移等

1.场景分析 使用flume做数据传输时&#xff0c;可能遇到将一个数据流中的多张表分别保存到各自位置的问题&#xff0c;同时由于采集时间和数据实际发生时间存在差异&#xff0c;因此需要根据数据实际发生时间进行分区保存。 鉴于此&#xff0c;需要设计flume拦截器配置conf文件…

Java 内存区域介绍

&#xff08;1&#xff09;程序计数器 程序计数器主要有两个作用&#xff1a; 字节码解释器通过改变程序计数器来依次读取指令&#xff0c;从而实现代码的流程控制&#xff0c;如&#xff1a;顺序执行、选择、循环、异常处理。 在多线程的情况下&#xff0c;程序计数器用于记录…

【开源】JAVA+Vue.js实现计算机机房作业管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 登录注册模块2.2 课程管理模块2.3 课时管理模块2.4 学生作业模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 课程表3.2.2 课时表3.2.3 学生作业表 四、系统展示五、核心代码5.1 查询课程数据5.2 新增课时5.3 提交作…