webrtc笔记(5): 基于kurento media server的多人视频聊天示例

忘是亡心i 2021-11-17 12:36 436阅读 0赞

这是kurento tutorial中的一个例子(groupCall),用于多人音视频通话,效果如下:

登录界面:

27612-20190714195504363-986228458.png

聊天界面:

27612-20190714195421231-494894219.png

运行方法:

1、本地用docker把kurento server跑起来

2、idea里启用这个项目

3、浏览器里输入https://localhost:8443/ 输入用户名、房间号,然后再开一个浏览器tab页,输入一个不同的用户名,房间号与第1个tab相同,正常情况下,这2个tab页就能聊上了,还可以再加更多tab模拟多人视频(注:docker容器性能有限,mac本上实测,越过4个人,就很不稳定了)

下面是该项目的一些代码和逻辑分析:

一、主要模型的类图如下:

点击查看原图

UserSession类:代表每个连接进来的用户会话信息。

Room类:即房间,1个房间可能有多个UserSession实例。

RoomManager类:房间管理,用于创建或销毁房间。

UserRegistry类:用户注册类,即管理用户。

二、主要代码逻辑:

1、创建房间入口

  1. public Room getRoom(String roomName) {
  2. log.debug("Searching for room {}", roomName);
  3. Room room = rooms.get(roomName);
  4. if (room == null) {
  5. log.debug("Room {} not existent. Will create now!", roomName);
  6. room = new Room(roomName, kurento.createMediaPipeline());
  7. rooms.put(roomName, room);
  8. }
  9. log.debug("Room {} found!", roomName);
  10. return room;
  11. }

注:第7行,每个房间实例创建时,都绑定了一个对应的MediaPipeline(用于隔离不同房间的媒体信息等)

2、创建用户实例入口

  1. public UserSession(final String name, String roomName, final WebSocketSession session,
  2. MediaPipeline pipeline) {
  3. this.pipeline = pipeline;
  4. this.name = name;
  5. this.session = session;
  6. this.roomName = roomName;
  7. this.outgoingMedia = new WebRtcEndpoint.Builder(pipeline).build();
  8. this.outgoingMedia.addIceCandidateFoundListener(event -> {
  9. JsonObject response = new JsonObject();
  10. response.addProperty("id", "iceCandidate");
  11. response.addProperty("name", name);
  12. response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
  13. try {
  14. synchronized (session) {
  15. session.sendMessage(new TextMessage(response.toString()));
  16. }
  17. } catch (IOException e) {
  18. log.debug(e.getMessage());
  19. }
  20. });
  21. }

UserSession的构造函数上,把房间实例的pipeline做为入参传进来,然后上行传输的WebRtcEndPoint实例outgoingMedia又跟pipeline绑定(第8行)。这样:”用户实例—pipeline实例—房间实例” 就串起来了。

用户加入房间的代码:

  1. public UserSession join(String userName, WebSocketSession session) throws IOException {
  2. log.info("ROOM {}: adding participant {}", this.name, userName);
  3. final UserSession participant = new UserSession(userName, this.name, session, this.pipeline);
  4. //示例工程上,没考虑“相同用户名”的人进入同1个房间的情况,这里加上了“用户名重名”检测
  5. if (participants.containsKey(userName)) {
  6. final JsonObject jsonFailMsg = new JsonObject();
  7. final JsonArray jsonFailArray = new JsonArray();
  8. jsonFailArray.add(userName + " exist!");
  9. jsonFailMsg.addProperty("id", "joinFail");
  10. jsonFailMsg.add("data", jsonFailArray);
  11. participant.sendMessage(jsonFailMsg);
  12. participant.close();
  13. return null;
  14. }
  15. joinRoom(participant);
  16. participants.put(participant.getName(), participant);
  17. sendParticipantNames(participant);
  18. return participant;
  19. }

原代码没考虑到用户名重名的问题,我加上了这段检测,倒数第2行代码,sendParticipantNames在加入成功后,给房间里的其它人发通知。

3、SDP交换的入口

kurento-group-call/src/main/resources/static/js/conferenceroom.js 中有一段监听websocket的代码:

  1. ws.onmessage = function (message) {
  2. let parsedMessage = JSON.parse(message.data);
  3. console.info('Received message: ' + message.data);
  4. switch (parsedMessage.id) {
  5. case 'existingParticipants':
  6. onExistingParticipants(parsedMessage);
  7. break;
  8. case 'newParticipantArrived':
  9. onNewParticipant(parsedMessage);
  10. break;
  11. case 'participantLeft':
  12. onParticipantLeft(parsedMessage);
  13. break;
  14. case 'receiveVideoAnswer':
  15. receiveVideoResponse(parsedMessage);
  16. break;
  17. case 'iceCandidate':
  18. participants[parsedMessage.name].rtcPeer.addIceCandidate(parsedMessage.candidate, function (error) {
  19. if (error) {
  20. console.error("Error adding candidate: " + error);
  21. return;
  22. }
  23. });
  24. break;
  25. case 'joinFail':
  26. alert(parsedMessage.data[0]);
  27. window.location.reload();
  28. break;
  29. default:
  30. console.error('Unrecognized message', parsedMessage);
  31. }
  32. }

服务端在刚才提到的sendParticipantNames后,会给js发送各种消息,existingParticipants(其它人加入)、newParticipantArrived(新人加入) 这二类消息,就会触发generateOffer,开始向服务端发送SDP

  1. function onExistingParticipants(msg) {
  2. const constraints = {
  3. audio: true,
  4. video: {
  5. mandatory: {
  6. maxWidth: 320,
  7. maxFrameRate: 15,
  8. minFrameRate: 15
  9. }
  10. }
  11. };
  12. console.log(name + " registered in room " + room);
  13. let participant = new Participant(name);
  14. participants[name] = participant;
  15. let video = participant.getVideoElement();
  16. const options = {
  17. localVideo: video,
  18. mediaConstraints: constraints,
  19. onicecandidate: participant.onIceCandidate.bind(participant)
  20. };
  21. participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options,
  22. function (error) {
  23. if (error) {
  24. return console.error(error);
  25. }
  26. this.generateOffer(participant.offerToReceiveVideo.bind(participant));
  27. });
  28. msg.data.forEach(receiveVideo);
  29. }

4、服务端回应各种websocket消息

org.kurento.tutorial.groupcall.CallHandler#handleTextMessage 信令处理的主要逻辑,就在这里:

  1. @Override
  2. public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  3. final JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);
  4. final UserSession user = registry.getBySession(session);
  5. if (user != null) {
  6. log.debug("Incoming message from user '{}': {}", user.getName(), jsonMessage);
  7. } else {
  8. log.debug("Incoming message from new user: {}", jsonMessage);
  9. }
  10. switch (jsonMessage.get("id").getAsString()) {
  11. case "joinRoom":
  12. joinRoom(jsonMessage, session);
  13. break;
  14. case "receiveVideoFrom":
  15. final String senderName = jsonMessage.get("sender").getAsString();
  16. final UserSession sender = registry.getByName(senderName);
  17. final String sdpOffer = jsonMessage.get("sdpOffer").getAsString();
  18. user.receiveVideoFrom(sender, sdpOffer);
  19. break;
  20. case "leaveRoom":
  21. leaveRoom(user);
  22. break;
  23. case "onIceCandidate":
  24. JsonObject candidate = jsonMessage.get("candidate").getAsJsonObject();
  25. if (user != null) {
  26. IceCandidate cand = new IceCandidate(candidate.get("candidate").getAsString(),
  27. candidate.get("sdpMid").getAsString(), candidate.get("sdpMLineIndex").getAsInt());
  28. user.addCandidate(cand, jsonMessage.get("name").getAsString());
  29. }
  30. break;
  31. default:
  32. break;
  33. }
  34. }

其中user.receiveVideoFrom方法,就会回应SDP

  1. public void receiveVideoFrom(UserSession sender, String sdpOffer) throws IOException {
  2. log.info("USER {}: connecting with {} in room {}", this.name, sender.getName(), this.roomName);
  3. log.trace("USER {}: SdpOffer for {} is {}", this.name, sender.getName(), sdpOffer);
  4. final String ipSdpAnswer = this.getEndpointForUser(sender).processOffer(sdpOffer);
  5. final JsonObject scParams = new JsonObject();
  6. scParams.addProperty("id", "receiveVideoAnswer");
  7. scParams.addProperty("name", sender.getName());
  8. scParams.addProperty("sdpAnswer", ipSdpAnswer);
  9. log.trace("USER {}: SdpAnswer for {} is {}", this.name, sender.getName(), ipSdpAnswer);
  10. this.sendMessage(scParams);
  11. log.debug("gather candidates");
  12. this.getEndpointForUser(sender).gatherCandidates();
  13. }

SDP和ICE信息交换完成,就开始视频通讯了。

参考文章:

https://doc-kurento.readthedocs.io/en/6.10.0/tutorials/java/tutorial-groupcall.html

转载于:https://www.cnblogs.com/yjmyzz/p/webrtc-groupcall-using-kurento.html

发表评论

表情:
评论列表 (有 0 条评论,436人围观)

还没有评论,来说两句吧...

相关阅读

    相关 基于webrtc视频研究(一)

    所周知,WebRTC非常适合点对点(即一对一)的音视频会话。然而,当我们的客户要求超越一对一,即一对多、多对一设置多对多的解决方案或者服务,那么问题就来了:“我们应该采用什么样