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

登录界面:

聊天界面:

运行方法:

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、创建房间入口

  public Room getRoom(String roomName) {
log.debug("Searching for room {}", roomName);
Room room = rooms.get(roomName); if (room == null) {
log.debug("Room {} not existent. Will create now!", roomName);
room = new Room(roomName, kurento.createMediaPipeline());
rooms.put(roomName, room);
}
log.debug("Room {} found!", roomName);
return room;
}

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

2、创建用户实例入口

    public UserSession(final String name, String roomName, final WebSocketSession session,
MediaPipeline pipeline) { this.pipeline = pipeline;
this.name = name;
this.session = session;
this.roomName = roomName;
this.outgoingMedia = new WebRtcEndpoint.Builder(pipeline).build(); this.outgoingMedia.addIceCandidateFoundListener(event -> {
JsonObject response = new JsonObject();
response.addProperty("id", "iceCandidate");
response.addProperty("name", name);
response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
try {
synchronized (session) {
session.sendMessage(new TextMessage(response.toString()));
}
} catch (IOException e) {
log.debug(e.getMessage());
}
});
}

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

用户加入房间的代码:

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

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

3、SDP交换的入口

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

ws.onmessage = function (message) {
let parsedMessage = JSON.parse(message.data);
console.info('Received message: ' + message.data); switch (parsedMessage.id) {
case 'existingParticipants':
onExistingParticipants(parsedMessage);
break;
case 'newParticipantArrived':
onNewParticipant(parsedMessage);
break;
case 'participantLeft':
onParticipantLeft(parsedMessage);
break;
case 'receiveVideoAnswer':
receiveVideoResponse(parsedMessage);
break;
case 'iceCandidate':
participants[parsedMessage.name].rtcPeer.addIceCandidate(parsedMessage.candidate, function (error) {
if (error) {
console.error("Error adding candidate: " + error);
return;
}
});
break;
case 'joinFail':
alert(parsedMessage.data[0]);
window.location.reload();
break;
default:
console.error('Unrecognized message', parsedMessage);
}
}

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

function onExistingParticipants(msg) {
const constraints = {
audio: true,
video: {
mandatory: {
maxWidth: 320,
maxFrameRate: 15,
minFrameRate: 15
}
}
};
console.log(name + " registered in room " + room);
let participant = new Participant(name);
participants[name] = participant;
let video = participant.getVideoElement(); const options = {
localVideo: video,
mediaConstraints: constraints,
onicecandidate: participant.onIceCandidate.bind(participant)
};
participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options,
function (error) {
if (error) {
return console.error(error);
}
this.generateOffer(participant.offerToReceiveVideo.bind(participant));
}); msg.data.forEach(receiveVideo);
}

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

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

    @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
final JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class); final UserSession user = registry.getBySession(session); if (user != null) {
log.debug("Incoming message from user '{}': {}", user.getName(), jsonMessage);
} else {
log.debug("Incoming message from new user: {}", jsonMessage);
} switch (jsonMessage.get("id").getAsString()) {
case "joinRoom":
joinRoom(jsonMessage, session);
break;
case "receiveVideoFrom":
final String senderName = jsonMessage.get("sender").getAsString();
final UserSession sender = registry.getByName(senderName);
final String sdpOffer = jsonMessage.get("sdpOffer").getAsString();
user.receiveVideoFrom(sender, sdpOffer);
break;
case "leaveRoom":
leaveRoom(user);
break;
case "onIceCandidate":
JsonObject candidate = jsonMessage.get("candidate").getAsJsonObject(); if (user != null) {
IceCandidate cand = new IceCandidate(candidate.get("candidate").getAsString(),
candidate.get("sdpMid").getAsString(), candidate.get("sdpMLineIndex").getAsInt());
user.addCandidate(cand, jsonMessage.get("name").getAsString());
}
break;
default:
break;
}
}

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

    public void receiveVideoFrom(UserSession sender, String sdpOffer) throws IOException {
log.info("USER {}: connecting with {} in room {}", this.name, sender.getName(), this.roomName); log.trace("USER {}: SdpOffer for {} is {}", this.name, sender.getName(), sdpOffer); final String ipSdpAnswer = this.getEndpointForUser(sender).processOffer(sdpOffer);
final JsonObject scParams = new JsonObject();
scParams.addProperty("id", "receiveVideoAnswer");
scParams.addProperty("name", sender.getName());
scParams.addProperty("sdpAnswer", ipSdpAnswer); log.trace("USER {}: SdpAnswer for {} is {}", this.name, sender.getName(), ipSdpAnswer);
this.sendMessage(scParams);
log.debug("gather candidates");
this.getEndpointForUser(sender).gatherCandidates();
}

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

参考文章:

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

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

  1. 在Ubuntu上部署一个基于webrtc的多人视频聊天服务

    最近研究webrtc视频直播技术,网上找了些教程最终都不太能顺利跑起来的,可能是文章写的比较老,使用的一些开源组件已经更新了,有些配置已经不太一样了,所以按照以前的步骤会有问题.折腾了一阵终于跑起来了 ...

  2. 如何基于 ZEGO SDK 实现 Flutter 一对一音视频聊天应用?

    之前的文章发布了ZEGO SDK实现Android端音视频通话应用的开发教程,不少开发者反馈很实用,能不能也出一版Flutter的教程. 有求必应,这不小编来了- 我们封装了ZEGO Flutter ...

  3. WebRTC实现网页版多人视频聊天室

    因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果. 话说 WebRTC Real Time Communication 简称 RTC,是谷歌若干年前收购的一项技术 ...

  4. webrtc笔记(1): 基于coturn项目的stun/turn服务器搭建

    webrtc是google推出的基于浏览器的实时语音-视频通讯架构.其典型的应用场景为:浏览器之间端到端(p2p)实时视频对话,但由于网络环境的复杂性(比如:路由器/交换机/防火墙等),浏览器与浏览器 ...

  5. 如何基于 ZEGO SDK 实现 Android 一对一音视频聊天应用

    疫情期间,很多线下活动转为线上举行,实时音视频的需求剧增,在视频会议,在线教育,电商购物等众多场景成了"生活新常态". 本文将教你如何通过即构ZEGO sdk在Android端搭建 ...

  6. 如何基于 ZEGO SDK 实现 Windows 一对一音视频聊天应用

    互联网发展至今,实时视频和语音通话越来越被大众所依赖. 今天,我们将会继续介绍如何基于ZEGO SDK实现音视频通话功能,前两篇文章分别介绍了Android,Flutter平台的实现方式,感兴趣的小伙 ...

  7. 基于Kurento的WebRTC移动视频群聊技术方案

    说在前面的话:视频实时群聊天有三种架构: Mesh架构:终端之间互相连接,没有中心服务器,产生的问题,每个终端都要连接n-1个终端,每个终端的编码和网络压力都很大.群聊人数N不可能太大. Router ...

  8. webrtc笔记(4): kurento 部署

    kurento是一个开源的webrtc mcu服务器,按官方的文档,建议在ubtntu上安装,过程如下: 注:建议先切换到root身份,如果不是root身份登录的,下列命令,请自行加上sudo . 另 ...

  9. Winsock网络编程笔记(3)----基于UDP的server和client

    在上一篇随笔中,对Winsock中基于tcp面向连接的Server和Client通信进行了说明,但是,Winsock中,Server和Client间还可以通过无连接通信,也就是采用UDP协议.. 因此 ...

随机推荐

  1. 你想了解的「SpringCloud」都在这里

    前言: 之前我们已经了解了「什么是微服务?」,现在我们开始了解「微服务」关键字下比较热门的「Spring Cloud」... 一.传统架构发展史 部分引用自:从架构演进的角度聊聊Spring Clou ...

  2. 如何使用PHP的生成器yield处理大量数据业务

    官方解释yield yield生成器是php5.5之后出现的,官方文档这样解释:yield提供了一种更容易的方法来实现简单的迭代对象,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大 ...

  3. activiti5初识

    因工作需要,接手新的项目,其中用到了activiti实现的工作流,特意去大致学习下,特此记录下. 1.acticiti5框架说明及表结构介绍 Activiti5工作流引擎框架: 它实际上是一个java ...

  4. 内核态发生非法地址访问是否会panic

    https://mp.weixin.qq.com/s?__biz=MzAwMDUwNDgxOA==&mid=2652663676&idx=1&sn=b18ab57322594e ...

  5. 黄聪:微信小程序 服务器 TLS1.0 1TLS.2 配置详细教学!

    下载IISCrypto.exe 点击best 工具自动推荐选中 也可以定义勾选 选择配置完成 然后点击”apply“ 软件弹窗提醒你 手动重启服务器!!!重启服务器 搞定! 最后 https://ww ...

  6. .net core程序强制以管理员权限启动

    当我们编写windows程序的时候,很多时候需要程序默认以管理员权限运行,以前在.net 程序中直接新建一个app.manifest,设置requestedExecutionLevel 节点即可 &l ...

  7. C# 获取系统硬件可用信息

    如何获取系统可用磁盘和可用内存? 1 获取磁盘信息 这个有自带的 DriveInfo 可以用. DriveInfo 类 (System.IO) | Microsoft Docs 2 获取内存信息 参考 ...

  8. go-面向对象编程(下)

    面向对象编程思想-抽象 抽象的介绍 我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的 属性( 字段)和 行为( 方法)提取 出来,形成一个 物理模型(结构体).这种研究问题的方法称为抽象 ...

  9. Initialize a Property After Creating an Object创建对象后初始化属性 即如何设置对象的默认值(EF)

    In this lesson, you will learn how to set the default value for a particular property of a business ...

  10. 垃圾分类环保宣传 PPT模板

    模板来源:http://ppt.dede58.com/peixunyanjiang/26263.html