这是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. Ubuntu Idea 快捷键 Ctrl+Alt+S 无法使用解决

    Idea 里习惯了用 Ctrl+Alt+S 打开设置界面,在 Ubuntu 下会因为快捷键冲突无法使用 系统快捷键 打开系统设置中的快捷键设置,按 Backspace 键禁用 Fcitx 如果你的输入 ...

  2. C语言程序设计100例之(1):鸡兔同笼

    例1   鸡兔同笼 [问题描述] 一个笼子里面关了鸡和兔子(鸡有2 只脚,兔子有4 只脚,没有例外).已知笼子里面脚的总数a,问笼子里面至少有多少只动物,至多有多少只动物? [输入数据] 第1 行是测 ...

  3. virtualbox FAIL(0x80004005) VirtualBox VT-x is not available (VERR_VMX_NO_VMX)

    virtualbox启动虚拟机报错: FAIL(0x80004005) VirtualBox VT-x is not available (VERR_VMX_NO_VMX),无法创建新任务 这是win ...

  4. hive引擎的选择:tez和spark

    背景 mr引擎在hive 2中将被弃用.官方推荐使用tez或spark等引擎. 选择 tez 使用有向无环图.内存式计算. spark 可以同时作为批式和流式的处理引擎,减少学习成本. 问题& ...

  5. wpf listview images

    <ListView x:Name="lv"> <ListView.ItemsPanel> <ItemsPanelTemplate> <St ...

  6. Java的23种设计模式,详细讲解(三)

    本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...

  7. C# Spire简单实现导出word(去水印)

    今天老姐打电话,说:下个月一号要换到其他岗位上,到时需要对word操作,小弟我随口答应,这个简单,我给你开发一款小程序,你直接在我程序上录入一些数据,我给你导出到word中. 利用中午空闲时间,百度了 ...

  8. JS基础语法---数组

    数组: 一组有序的数据 数组的作用: 可以一次性存储多个数据 数组的定义: 1. 通过构造函数创建数组   语法: var 数组名=new Array(); var array=new Array() ...

  9. 教你如何添加Xcode 9.3配置包?(安装流程可供其他版本安装参考)

    1.准备好你想要的Xcode版本的安装包 ,这里以Xcode 9.3为例.                        →                   2.打开Xcode开发工具的安装路径 ...

  10. Android中使用WebView实现全屏切换播放网页视频

    首先写布局文件activity_main.xml: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/an ...