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.效果
新建房间
其他用户加入房间
点击共享,