Room demo tutorial
本教程是使用Room API SDK开发多协作应用程序的指南,共有三个基本结构,也就是三个文件夹,三个工程——kurento-room-server
, kurento-room-client-js
和kurento-room-demo
。
代码
一、服务器端代码(server side code)
主类是KurentoRoomServerApp.java
,同样为一个springboot应用类,我们将会在这个类里用Spring bean的方式来实例化构成服务器端的各个组件。
1.带通知的房间管理(room management)
这里用了 Room SDK 来管理房间和用户,我们选择的API——NotificationRoomManager
类是一个通知风格的API,把它定义为一个Spring bean,以便以后依赖的注入。
但首先我们要给NotificationRoomManager
的构造器提供一个 UserNotificationService 的实例作为参数,代码中为notificationService
,它是一个 JsonRpcNotificationService 类型的对象,用于存储JSON-RPC会话,以支持向客户按发送响应和通知。另一个参数为kmsManager
。下图代码先定义了两个参数,再用两个参数送到构造器来定义 NotificationRoomManager
@Bean
@ConditionalOnMissingBean
public KurentoClientProvider kmsManager() {
JsonArray kmsUris = getPropertyJson(KMSS_URIS_PROPERTY, KMSS_URIS_DEFAULT, JsonArray.class);
List kmsWsUris = JsonUtils.toStringList(kmsUris);
if (kmsWsUris.isEmpty()) {
throw new IllegalArgumentException(KMSS_URIS_PROPERTY
+ " should contain at least one kms url");
}
String firstKmsWsUri = kmsWsUris.get(0);
if (firstKmsWsUri.equals("autodiscovery")) {
log.info("Using autodiscovery rules to locate KMS on every pipeline");
return new AutodiscoveryKurentoClientProvider();
} else {
log.info("Configuring Kurento Room Server to use first of the following kmss: " + kmsWsUris);
return new FixedOneKmsManager(firstKmsWsUri);
}
}
@Bean
@ConditionalOnMissingBean
public JsonRpcNotificationService notificationService() {
return new JsonRpcNotificationService();
}
@Bean
@ConditionalOnMissingBean
public NotificationRoomManager roomManager() {
return new NotificationRoomManager(notificationService(), kmsManager());
}
2.通信(signaling)
我们的demo用Kureto 提供的 JSON-RPC 服务器库来实现与客户端的交互。
我们为进来的信息注册了一个handler RoomJsonRpcHandler
(整个RoomJsonRpcHandler.java
就定义了这一个类)以便之后可以根据方法名字来处理请求。这个类 RoomJsonRpcHandler
实现了之前提到的Websocket API。
在向这个API的registerJsonRpcHandlers
方法中(一看到register应该反应过来这个方法在主类中,即KurentoRoomServerApp.java
)添加这个handler(RoomJsonRpcHandler
类实例化而来的roomHandler
)时,要指定路径。
@Bean
@ConditionalOnMissingBean
public RoomJsonRpcHandler roomHandler() {
return new RoomJsonRpcHandler(userControl(), notificationService());
}
@Override
public void registerJsonRpcHandlers(JsonRpcHandlerRegistry registry) {
registry.addHandler(roomHandler().withPingWatchdog(true), "/room");
}
回到RoomJsonRpcHandler.java
类中,RoomJsonRpcHandler
类的主方法handleRequest()
会在每次收到客户端的请求时被触发,再出发的时候所有Websocket交流会在一个会话内完成,并且JSON-RPC库会给每个会话提供一个引用。一次请求-应答交换称为一次事务。
应用会存储每个用户对应的绘画和事务,从而notificationService
(主类中定义的)在从 Room SDK 中被调用的时候就可以向客户端发相应或服务器事件了。
public final void handleRequest(Transaction transaction, Request request)
throws Exception {
String sessionId = null;
try {
sessionId = transaction.getSession().getSessionId();
} catch (Throwable e) {
log.warn("Error getting session id from transaction {}", transaction, e);
throw e;
}
updateThreadName(HANDLER_THREAD_NAME + "_" + sessionId);
log.debug("Session #{} - request: {}", sessionId, request);
notificationService.addTransaction(transaction, request);
ParticipantRequest participantRequest = new ParticipantRequest(sessionId,
Integer.toString(request.getId()));
transaction.startAsync();
switch (request.getMethod()) {
case ProtocolElements.JOINROOM_METHOD :
userControl.joinRoom(transaction, request, participantRequest);
break;
case ProtocolElements.PUBLISHVIDEO_METHOD :
userControl.publishVideo(transaction, request, participantRequest);
break;
case ProtocolElements.UNPUBLISHVIDEO_METHOD :
userControl.unpublishVideo(transaction, request, participantRequest);
break;
case ProtocolElements.RECEIVEVIDEO_METHOD :
userControl.receiveVideoFrom(transaction, request, participantRequest);
break;
case ProtocolElements.UNSUBSCRIBEFROMVIDEO_METHOD :
userControl.unsubscribeFromVideo(transaction, request, participantRequest);
break;
case ProtocolElements.ONICECANDIDATE_METHOD :
userControl.onIceCandidate(transaction, request, participantRequest);
break;
case ProtocolElements.LEAVEROOM_METHOD :
userControl.leaveRoom(transaction, request, participantRequest);
break;
case ProtocolElements.SENDMESSAGE_ROOM_METHOD :
userControl.sendMessage(transaction, request, participantRequest);
break;
case ProtocolElements.CUSTOMREQUEST_METHOD :
userControl.customRequest(transaction, request, participantRequest);
break;
default :
log.error("Unrecognized request {}", request);
break;
}
updateThreadName(HANDLER_THREAD_NAME);
}
3.管理用户请求(Manage user requests)
handler把对用户请求的处理委托给了另一个组件,JsonRpcUserControl 类的一个实例,代码中是userControl
,在主类中有定义
@Bean
@ConditionalOnMissingBean
public JsonRpcUserControl userControl() {
return new JsonRpcUserControl(roomManager());
}
这个对象(userControl
)会从用户请求中提取出所需的参数,并且调用部分roomManager
的必要部分。用的时候还是在handler里面用,只是具体实现封装到了userControl
里面:
switch (request.getMethod()) {
case ProtocolElements.JOINROOM_METHOD :
userControl.joinRoom(transaction, request, participantRequest);
break;
case ProtocolElements.PUBLISHVIDEO_METHOD :
userControl.publishVideo(transaction, request, participantRequest);
break;
case ProtocolElements.UNPUBLISHVIDEO_METHOD :
userControl.unpublishVideo(transaction, request, participantRequest);
break;
case ProtocolElements.RECEIVEVIDEO_METHOD :
userControl.receiveVideoFrom(transaction, request, participantRequest);
break;
case ProtocolElements.UNSUBSCRIBEFROMVIDEO_METHOD :
userControl.unsubscribeFromVideo(transaction, request, participantRequest);
break;
case ProtocolElements.ONICECANDIDATE_METHOD :
userControl.onIceCandidate(transaction, request, participantRequest);
break;
case ProtocolElements.LEAVEROOM_METHOD :
userControl.leaveRoom(transaction, request, participantRequest);
break;
case ProtocolElements.SENDMESSAGE_ROOM_METHOD :
userControl.sendMessage(transaction, request, participantRequest);
break;
case ProtocolElements.CUSTOMREQUEST_METHOD :
userControl.customRequest(transaction, request, participantRequest);
break;
default :
log.error("Unrecognized request {}", request);
break;
}
比如在joinRoom()请求中,它要干的事如下:
public void joinRoom(Transaction transaction, Request request,
ParticipantRequest participantRequest) throws IOException, InterruptedException,
ExecutionException {
String roomName = getStringParam(request, ProtocolElements.JOINROOM_ROOM_PARAM);
String userName = getStringParam(request, ProtocolElements.JOINROOM_USER_PARAM);
boolean dataChannels = false;
if (request.getParams().has(ProtocolElements.JOINROOM_DATACHANNELS_PARAM)) {
dataChannels = request.getParams().get(ProtocolElements.JOINROOM_DATACHANNELS_PARAM)
.getAsBoolean();
}
ParticipantSession participantSession = getParticipantSession(transaction);
participantSession.setParticipantName(userName);
participantSession.setRoomName(roomName);
participantSession.setDataChannels(dataChannels);
roomManager.joinRoom(userName, roomName, dataChannels, true, participantRequest);
}
4.用户响应与事件
现在来到了notificationService
,由上文知,他是一个JsonRpcNotificationService
类型的对象,为RoomJsonRpcHandler
类的私有成员。
这个类将所有用户会话存储为映射,从中可以获取向一个房间请求做出响应所需的事务对象。发送通知用了session对象的功能。
响应一个特定请求时,对应的事务对象被用完后会被移出内存(getAndRemoveTransaction()
),再来相应就是新的事务了。发出错误指示回应也是一样。
@Override
public void sendResponse(ParticipantRequest participantRequest, Object result) {
Transaction t = getAndRemoveTransaction(participantRequest);
if (t == null) {
log.error("No transaction found for {}, unable to send result {}", participantRequest, result);
return;
}
try {
t.sendResponse(result);
} catch (Exception e) {
log.error("Exception responding to user ({})", participantRequest, e);
}
}
发服务器响应或者服务器事件时,我们需要用到session对象。session对象需要一直保留,直到close session()方法被调用。close session()被调用可以有两种来源,可以是因为用户的离开被roomhandler调用,也可以是因为网络错误被websocket调用。
二、demo对服务器端的定制
这个demo替换和修改了一些服务器端代码的spring bean来进行了一些特殊订制。全部在KurentoRoomDemoApp
中完成,它先导入了原始server类然后做了修改:
import org.kurento.room.KurentoRoomServerApp;
...
public class KurentoRoomDemoApp {
...
public static void main(String[] args) throws Exception {
SpringApplication.run(KurentoRoomDemoApp.class, args);
}
}
1.自定义KurentoClientProvider
我们自定义了FixedNKmsManager
作为默认的provider接口,得以管理一系列由在配置文件中指定的URI创建的KurentoClient
。
2.自定义用户控制(user control)
自定义了DemoJsonRpcUserControl
来实现对于customRequest
这一附加的websocket请求类型的支持。
在这个类中我们重写了customRequest()
方法,以实现切换FaceOverlayFilter
,它可以在发布者的头上加一个帽子或者移除。他把滤镜对象当作websocket session的一个属性来存储,方便了滤镜的删除:
@Override
public void customRequest(Transaction transaction, Request request,
ParticipantRequest participantRequest) {
try {
if (request.getParams() == null
|| request.getParams().get(filterType.getCustomRequestParam()) == null) {
throw new RuntimeException(
"Request element '" + filterType.getCustomRequestParam() + "' is missing");
}
switch (filterType) {
case MARKER:
handleMarkerRequest(transaction, request, participantRequest);
break;
case HAT:
default:
handleHatRequest(transaction, request, participantRequest);
}
} catch (Exception e) {
log.error("Unable to handle custom request", e);
try {
transaction.sendError(e);
} catch (IOException e1) {
log.warn("Unable to send error response", e1);
}
}
}
3.依赖
手动删除了一些会造成冲突的依赖。
三、客户端代码
这部分描述一下kurento-room-demo中包含的AngularJS应用。
1.库
- 在这个时候,我突然脑子一抽觉得,我应该线跑一下demo再来看代码啊。所以我去跑demo,没想到,很惨,又是一堆别人从来没遇到过的错。改了,两天半吧,改了两天半的bug。你能想象官方demo还有bug吗!!!!而且,最主要的是,我也说了,这bug都是别人没遇到过的,所以我去搜解决方案就很少,仅有的比较契合的也都是英文,剩下的那些,中文的什么什么鬼的,完全看不下去。so,到这了,两天半,我终于找到了一个看似可以采用的解决方案,然后自定义得改了改,现在把电脑放着让它自己慢慢解决maven依赖吧,也不知道是不是有效。等之后回来再看吧。虽然还不能确定是否有效,但起码能做点什么了,也不枉我搜了这么多网页,看了那么多英文文档啊。哦,对,在这里表扬一下stack overflow。甚是得朕心