基于react webRtc和nestJs实现屏幕多人共享

1.简介

WebRtc一直是作为实时通信的一个重要手段,无论是语音还是视频用WebRtc来实现效果都很不错,最近用到浏览器的屏幕共享功能发现也是返回的一个视频流,所以就想着尝试实现一下多人共享屏幕

2.流程

Rtc通信前连接交互,都是通过offer和answer来实现的,大致流程如下图
在这里插入图片描述

3.前端

3.1 WebSocket初始化

前端初始化WebSocket,主要还是将房间和Socket信息链接起来,和保证后端广播WS信息的时候主机不会收到自己的信息重复动作

class RoomSocket {
  _Socket: Socket;
  private isConnected: boolean = false;
  private roomId?: string;
  private user: userProp;
  constructor(roomId: string) {
    this.roomId = roomId;
    this.user = userMock;
    this._Socket = io("http://localhost:3002", {
      autoConnect: false,
      query: {
        roomId: roomId,
        userId: this.user.id
      },
    });
  }
  /**
   * @Date: 2024-07-16 15:01:17
   * @LastEditors: Jmelon66
   * @Description:
   * @param {object} eventObj
   */
  setOnEventsByObject(eventObj: { [index: string]: Function }) {
    const { _Socket } = this;
    for (const key in eventObj) {
      const item = eventObj[key];
      _Socket.on(key, (args) => this.onEventsFunction(args, item));
    }
  }
  onEventsFunction(args: any, callback: Function) {
    if (args.broadcast && args.userId === this.user.id) {
      return;
    }
    callback(args);
  }
  //   socket emit msg
  sendMsg<T>(msgObj: msgBody<T>, callback?: any) {
    const { _Socket, user } = this;
    if (!msgObj?.userId) {
      msgObj.userId = user.id;
    }
    if (callback) _Socket.emit(msgObj.name, msgObj, callback);
    else _Socket.emit(msgObj.name, msgObj);
  }
}

将Rtc进行封装成独立块,并监听ICE候选

export class PerUserRTCconnection extends rtcOption {
  private belongUserId: string;
  private outPutStream: MediaStream = new MediaStream();
  private peerConnection: RTCPeerConnection;
  private _SocketServer: RoomSocket;
  private eventsProp?: RtcManageEventsProp;
  private currentTracks: Array<RTCTrackEvent> = [];
  private isHaveOffer: Boolean = false;
  private isHaveICEOffer: Boolean = false;
  constructor(
    belongUserId: string,
    _SocketServer: RoomSocket,
    eventsProp?: RtcManageEventsProp
  ) {
    super();
    this._SocketServer = _SocketServer;
    this.belongUserId = belongUserId;
    this.eventsProp = eventsProp;
    const peerConnection = new RTCPeerConnection(undefined);
    peerConnection.ontrack = this.onTrackEvent.bind(this);
    peerConnection.onicecandidate = this.onicecandidateEvent.bind(this);
    peerConnection.onconnectionstatechange = (state: any) => {
      console.log(state);
    };
    this.peerConnection = peerConnection;
    console.log("init peer events OK");
  }
  onicecandidateEvent(candidateInfo: RTCPeerConnectionIceEvent) {
    console.log(candidateInfo);
    if (!candidateInfo.candidate) {
      return false;
    }
    const { _SocketServer, belongUserId } = this;
    _SocketServer.sendMsg({
      name: "SingleICEOffer",
      data: { candidate: candidateInfo.candidate, belongUserId },
    });
  }
  onTrackEvent(track: RTCTrackEvent) {
    // console.log(track);
    this.currentTracks.push(track);
    // this.eventsProp?.onTrackSuccess &&
    //   this.eventsProp.onTrackSuccess(this.belongUserId, track);
  }
  getTrack(): RTCTrackEvent | undefined {
    return this.currentTracks[0];
  }
  setICEAnswer(data: any) {
    console.log(data);
  }
  getICEAnswer(data: any) {
    console.log(data);
  }
  async setICEOffer(message: any) {
    console.log(message);
    const { peerConnection } = this;
    //添加候选人信息
    const candidate = new RTCIceCandidate(message);
    await peerConnection.addIceCandidate(candidate);
    return true;
  }
  getICEOffer(data: any) {
    console.log(data);
  }
  async setAnswer(answer: any) {
    const { peerConnection } = this;
    await peerConnection.setRemoteDescription(
      new RTCSessionDescription(answer)
    );
    this.Answer = answer;
    console.log(peerConnection.connectionState);
    return true;
  }
  async getAnswer() {
    const { peerConnection } = this;
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
    this.Answer = answer;
    return answer;
  }
  async setOffer(offer: any) {
    const { peerConnection } = this;
    await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
    this.Offer = offer;
    return true;
  }
  async getOffer() {
    const { peerConnection } = this;
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
    this.Offer = offer;
    return offer;
  }

  addMediaStream(stream: MediaStream) {
    const { peerConnection } = this;
    const tracks = stream.getTracks();
    for (const track of tracks) peerConnection.addTrack(track, stream);
    console.log("add stream");
  }
  getOutPutStream(): MediaStream {
    return this.outPutStream;
  }
  close() {
    this.peerConnection.close();
  }
}

建立完链接后前面的onicecandidate 会收到信息,将流加入offer传输,ontrack也会收到信息,通过回调把流回传,在页面下进行显示,这里用ReactPlayer显示

export interface ShareScreenProps {
  srcObj?: MediaStream;
  onRef?: RefObject<any>;
}
.....
<ReactPlayer
      url={props.srcObj}
      muted={true}
      id="videoPanel"
      playing={true}
      playsinline={true}
      width="100%"
      height="calc(100% - 40px)"
      onError={(...args) => console.log(args)}
      controls={false}
    ></ReactPlayer>

3.2 房间信息同步

由于是多人的,所以在进入到同一个房间的时候就要将信息同步出去
主机建立房间时


  /**
   * @Date: 2024-08-18 10:56:45
   * @LastEditors: Jmelon66 961255554@qq.com
   * @Description: 初始化房间信息
   */
  async initRoom() {
    this.user.role = "admin";
    const { roomId } = this;
    const { _SocketServer } = this;

    this.hostUserId = this.user.id;
    // await this.tryCature();
    _SocketServer.sendMsg(
      {
        name: "CreateRoom",
        data: {
          roomId,
        },
      },
      (res: any) => {
        console.log(res);
      }
    );
  }
  async joinRoom() {
    this.user.role = "user";
    const { roomId, _SocketServer } = this;
    const res = await getRoomHostById({ id: roomId });
    this.hostUserId = res.data;
    console.log("Join room");
    _SocketServer.sendMsg(
      {
        name: "JoinRoom",
        data: {
          roomId,
        },
      },
      (res: any) => {
        console.log(res);
      }
    );
    this._RtcManage && this._RtcManage.setRole("user");
  }

用户加入房间的同时,后台广播信息,获取其他用户offer


  @SubscribeMessage('JoinRoom')
  ReadyJoinRoom(client, param) {
    const { socket_user_map, user_socket_map, socket_room_map } = this;
    const { userId, roomId } = param;
    const socId = client.id;
    socket_user_map.set(socId, userId);
    socket_room_map.set(socId, roomId);
    user_socket_map.set(userId, socId);
    this.socket
      .to(roomId)
      .emit('getOffer', { name: 'getOffer', data: { userId } });
    client.join(roomId);
  }

主机用户收到信息发送offer并存入媒体流,同时将信息存入map中


  async getOffer(data: any) {
    const { UserOfferMap, _SocketServer } = this;
    const { userId } = data.data;
    let nPeer = UserOfferMap.get(userId);
    if (!nPeer) {
      nPeer = new PerUserRTCconnection(userId, this._SocketServer);
      UserOfferMap.set(userId, nPeer);
    }

    if (this.events?.onBeforeConnect) {
      const mediaS = await this.events.onBeforeConnect(userId);
      if (mediaS instanceof MediaStream) nPeer.addMediaStream(mediaS);
    }
    const offer = await nPeer.getOffer();
    _SocketServer.sendMsg({
      name: "SingleOffer",
      data: {
        offer,
        toUserId: userId,
      },
    });
  }

其他用户收到信息,设置offer并发送answer


  async setOffer(data: any) {
    const { userId, offer } = data.data;
    const { UserOfferMap, _SocketServer } = this;
    let nPeer = UserOfferMap.get(userId);
    if (!nPeer) {
      nPeer = new PerUserRTCconnection(userId, this._SocketServer, this.events);
      UserOfferMap.set(userId, nPeer);
    }
    if (this.events?.onBeforeConnect) {
      const mediaS = await this.events.onBeforeConnect(userId);
      if (mediaS instanceof MediaStream) nPeer.addMediaStream(mediaS);
    }
    await nPeer.setOffer(offer);
    const answer = await nPeer.getAnswer();
    console.log("setOffer Ok");
    _SocketServer.sendMsg({
      name: "SingleAnswer",
      data: {
        answer,
        toUserId: userId,
      },
    });
  }

主机设置answer


  async setAnswer(data: any) {
    const { userId, answer } = data.data;
    const { UserOfferMap } = this;
    let nPeer = UserOfferMap.get(userId);
    if (!nPeer) {
      return false;
    }
    await nPeer.setAnswer(answer);
  }

3.3 开始共享

主机触发共享事件,获取屏幕媒体流


  async tryCature(): Promise<MediaStream | Boolean> {
    let captureStream = null;
    const displayMediaOptions: MediaStreamConstraints = {
      video: true,
      audio: true,
    };
    try {
      captureStream = await navigator.mediaDevices.getDisplayMedia(
        displayMediaOptions
      );
      this._captureStream = captureStream;
      this.handleSuccess<MediaStream>(
        EventCode.captureShareScreen,
        EventType.success,
        captureStream
      );
      return true;
    } catch (err) {
      console.error("Error: " + err);
      return false;
    }
  }

加入offer中在传出去,同步信息应该是对已加入房间的用户进行同步,所以不用重建rtc连接

async sendOffer() {
    const { UserOfferMap, _SocketServer } = this;
    UserOfferMap.forEach(async (nPeer, userId) => {
      if (this.events?.onBeforeConnect) {
        const mediaS = await this.events.onBeforeConnect(userId);
        if (mediaS instanceof MediaStream) nPeer.addMediaStream(mediaS);
      }
      const offer = await nPeer.getOffer();
      _SocketServer.sendMsg({
        name: "SingleOffer",
        data: {
          offer,
          toUserId: userId,
        },
      });
    });
  }

4.效果

新建房间
在这里插入图片描述

其他用户加入房间
在这里插入图片描述
点击共享,

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MElon66

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值