webrtc笔记(5): 基于kurento media server的多人视频聊天示例
这是kurento tutorial中的一个例子(groupCall),用于多人音视频通话,效果如下:
登录界面:

聊天界面:

运行方法:
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的多人视频聊天示例的更多相关文章
- 在Ubuntu上部署一个基于webrtc的多人视频聊天服务
最近研究webrtc视频直播技术,网上找了些教程最终都不太能顺利跑起来的,可能是文章写的比较老,使用的一些开源组件已经更新了,有些配置已经不太一样了,所以按照以前的步骤会有问题.折腾了一阵终于跑起来了 ...
- 如何基于 ZEGO SDK 实现 Flutter 一对一音视频聊天应用?
之前的文章发布了ZEGO SDK实现Android端音视频通话应用的开发教程,不少开发者反馈很实用,能不能也出一版Flutter的教程. 有求必应,这不小编来了- 我们封装了ZEGO Flutter ...
- WebRTC实现网页版多人视频聊天室
因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果. 话说 WebRTC Real Time Communication 简称 RTC,是谷歌若干年前收购的一项技术 ...
- webrtc笔记(1): 基于coturn项目的stun/turn服务器搭建
webrtc是google推出的基于浏览器的实时语音-视频通讯架构.其典型的应用场景为:浏览器之间端到端(p2p)实时视频对话,但由于网络环境的复杂性(比如:路由器/交换机/防火墙等),浏览器与浏览器 ...
- 如何基于 ZEGO SDK 实现 Android 一对一音视频聊天应用
疫情期间,很多线下活动转为线上举行,实时音视频的需求剧增,在视频会议,在线教育,电商购物等众多场景成了"生活新常态". 本文将教你如何通过即构ZEGO sdk在Android端搭建 ...
- 如何基于 ZEGO SDK 实现 Windows 一对一音视频聊天应用
互联网发展至今,实时视频和语音通话越来越被大众所依赖. 今天,我们将会继续介绍如何基于ZEGO SDK实现音视频通话功能,前两篇文章分别介绍了Android,Flutter平台的实现方式,感兴趣的小伙 ...
- 基于Kurento的WebRTC移动视频群聊技术方案
说在前面的话:视频实时群聊天有三种架构: Mesh架构:终端之间互相连接,没有中心服务器,产生的问题,每个终端都要连接n-1个终端,每个终端的编码和网络压力都很大.群聊人数N不可能太大. Router ...
- webrtc笔记(4): kurento 部署
kurento是一个开源的webrtc mcu服务器,按官方的文档,建议在ubtntu上安装,过程如下: 注:建议先切换到root身份,如果不是root身份登录的,下列命令,请自行加上sudo . 另 ...
- Winsock网络编程笔记(3)----基于UDP的server和client
在上一篇随笔中,对Winsock中基于tcp面向连接的Server和Client通信进行了说明,但是,Winsock中,Server和Client间还可以通过无连接通信,也就是采用UDP协议.. 因此 ...
随机推荐
- 《细说PHP》第四版 样章 第18章 数据库抽象层PDO 12
18.9 管理表books实例 在Web项目中,几乎所有模块都要和数据表打交道,而对表的管理无非就是增.删.改.查等操作,所以熟练掌握对表进行管理的这些常见操作是十分有必的.本例为了能更好地展示PD ...
- 【Linux命令】ldconfig动态链接库管理命令
ldconfig动态链接库管理命令,其目的为了让动态链接库为系统所共享. 作用: 默认搜寻/lilb和/usr/lib,以及配置文件/etc/ld.so.conf内所列的目录下的库文件. 搜索出可共享 ...
- JeeSite | 保存信息修改记录续
遗留问题 上篇文章中遗留了一个问题,就是为了要关联类属性与注释,注释与字典的地方使用了两个map来逐个添加了相关的信息,如下所示: Map<String, String> mapField ...
- 利用Python进行数据分析-Pandas(第一部分)
利用Python进行数据分析-Pandas: 在Pandas库中最重要的两个数据类型,分别是Series和DataFrame.如下的内容主要围绕这两个方面展开叙述! 在进行数据分析时,我们知道有两个基 ...
- Python 学习 第16篇:数据类型(字典和Json)
字典是键/值对构成的集合,字典通过大括号来创建,字典的键是字符串,而值可以是任何数据对象. 字典有两个重要的特征: 字典是无序的,字典项没有特定的顺序,只能通过键来获取值: 字典是可变的,支持原处修改 ...
- Java入门系列之字符串创建方式、判断相等(一)
前言 陆续从0开始学习Java出于多掌握一门语言以后的路也会更宽,.NET和Java兼顾,虽然路还很艰难,但事在人为.由于Java和C#语法相似,所以关于一些很基础的内容不会再重头讲,Java系列中所 ...
- docker registry 删除镜像 垃圾回收
操作步骤 通过环境变量修改默认配置,允许删除 获取image的sha值 进入registry容器中,执行垃圾回收 删除残留目录 #环境变量 REGISTRY_STORAGE_DELETE_ENABLE ...
- oracle学习笔记(十六) PL/SQL 异常和goto语句
PL/SQL 异常和goto语句 异常 预定义异常 oracle常见预定义异常: 错误号 异常错误信息名称 说明 ORA-0001 DUP_VAL_ON_INDEX 试图破坏一个唯一性限制 ORA-0 ...
- WPF ListBox 隐藏滑块
<ListBox ScrollViewer.VerticalScrollBarVisibility = "Disabled"; </ListBox>
- 资深程序员:学Python我推荐你用这几款编辑器
Python使用什么编辑比较好,Python编辑器推荐 各位 Pythoner 好啊!在这个烦躁的时代,相聚就是缘分,很高兴各位 Pythoner 能相聚于此,希望接下来的路,我们一起走下去,使用 P ...