Room demo tutorial
本教程是使用Room API SDK開發(fā)多協(xié)作應(yīng)用程序的指南蚊惯,共有三個基本結(jié)構(gòu),也就是三個文件夾仔引,三個工程——kurento-room-server
, kurento-room-client-js
和kurento-room-demo
鄙皇。
代碼
一、服務(wù)器端代碼(server side code)
主類是KurentoRoomServerApp.java
其爵,同樣為一個springboot應(yīng)用類车吹,我們將會在這個類里用Spring bean的方式來實例化構(gòu)成服務(wù)器端的各個組件。
1.帶通知的房間管理(room management)
這里用了 Room SDK 來管理房間和用戶醋闭,我們選擇的API——NotificationRoomManager
類是一個通知風格的API窄驹,把它定義為一個Spring bean,以便以后依賴的注入证逻。
但首先我們要給NotificationRoomManager
的構(gòu)造器提供一個 UserNotificationService 的實例作為參數(shù)乐埠,代碼中為notificationService
,它是一個 JsonRpcNotificationService 類型的對象囚企,用于存儲JSON-RPC會話丈咐,以支持向客戶按發(fā)送響應(yīng)和通知。另一個參數(shù)為kmsManager
龙宏。下圖代碼先定義了兩個參數(shù)棵逊,再用兩個參數(shù)送到構(gòu)造器來定義 NotificationRoomManager
@Bean
@ConditionalOnMissingBean
public KurentoClientProvider kmsManager() {
JsonArray kmsUris = getPropertyJson(KMSS_URIS_PROPERTY, KMSS_URIS_DEFAULT, JsonArray.class);
List<String> 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 服務(wù)器庫來實現(xiàn)與客戶端的交互。
我們?yōu)檫M來的信息注冊了一個handler RoomJsonRpcHandler
(整個RoomJsonRpcHandler.java
就定義了這一個類)以便之后可以根據(jù)方法名字來處理請求银酗。這個類 RoomJsonRpcHandler
實現(xiàn)了之前提到的Websocket API辆影。
在向這個API的registerJsonRpcHandlers
方法中(一看到register應(yīng)該反應(yīng)過來這個方法在主類中,即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()
會在每次收到客戶端的請求時被觸發(fā)灭衷,再出發(fā)的時候所有Websocket交流會在一個會話內(nèi)完成次慢,并且JSON-RPC庫會給每個會話提供一個引用。一次請求-應(yīng)答交換稱為一次事務(wù)翔曲。
應(yīng)用會存儲每個用戶對應(yīng)的繪畫和事務(wù)迫像,從而notificationService
(主類中定義的)在從 Room SDK 中被調(diào)用的時候就可以向客戶端發(fā)相應(yīng)或服務(wù)器事件了。
public final void handleRequest(Transaction transaction, Request<JsonObject> 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
)會從用戶請求中提取出所需的參數(shù)傅蹂,并且調(diào)用部分roomManager
的必要部分纷闺。用的時候還是在handler里面用算凿,只是具體實現(xiàn)封裝到了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<JsonObject> 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.用戶響應(yīng)與事件
現(xiàn)在來到了notificationService
犁功,由上文知氓轰,他是一個JsonRpcNotificationService
類型的對象,為RoomJsonRpcHandler
類的私有成員浸卦。
這個類將所有用戶會話存儲為映射署鸡,從中可以獲取向一個房間請求做出響應(yīng)所需的事務(wù)對象。發(fā)送通知用了session對象的功能限嫌。
響應(yīng)一個特定請求時靴庆,對應(yīng)的事務(wù)對象被用完后會被移出內(nèi)存(getAndRemoveTransaction()
),再來相應(yīng)就是新的事務(wù)了怒医。發(fā)出錯誤指示回應(yīng)也是一樣炉抒。
@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);
}
}
發(fā)服務(wù)器響應(yīng)或者服務(wù)器事件時,我們需要用到session對象稚叹。session對象需要一直保留焰薄,直到close session()方法被調(diào)用。close session()被調(diào)用可以有兩種來源扒袖,可以是因為用戶的離開被roomhandler調(diào)用塞茅,也可以是因為網(wǎng)絡(luò)錯誤被websocket調(diào)用。
二季率、demo對服務(wù)器端的定制
這個demo替換和修改了一些服務(wù)器端代碼的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創(chuàng)建的KurentoClient
鞭光。
2.自定義用戶控制(user control)
自定義了DemoJsonRpcUserControl
來實現(xiàn)對于customRequest
這一附加的websocket請求類型的支持。
在這個類中我們重寫了customRequest()
方法蠢络,以實現(xiàn)切換FaceOverlayFilter
衰猛,它可以在發(fā)布者的頭上加一個帽子或者移除。他把濾鏡對象當作websocket session的一個屬性來存儲刹孔,方便了濾鏡的刪除:
@Override
public void customRequest(Transaction transaction, Request<JsonObject> 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應(yīng)用髓霞。
1.庫
- 在這個時候,我突然腦子一抽覺得畦戒,我應(yīng)該線跑一下demo再來看代碼啊方库。所以我去跑demo,沒想到障斋,很慘纵潦,又是一堆別人從來沒遇到過的錯徐鹤。改了,兩天半吧邀层,改了兩天半的bug返敬。你能想象官方demo還有bug嗎!A仍骸>⒃!而且秸谢,最主要的是凛澎,我也說了,這bug都是別人沒遇到過的估蹄,所以我去搜解決方案就很少塑煎,僅有的比較契合的也都是英文,剩下的那些臭蚁,中文的什么什么鬼的轧叽,完全看不下去。so刊棕,到這了炭晒,兩天半,我終于找到了一個看似可以采用的解決方案甥角,然后自定義得改了改网严,現(xiàn)在把電腦放著讓它自己慢慢解決maven依賴吧,也不知道是不是有效嗤无。等之后回來再看吧震束。雖然還不能確定是否有效,但起碼能做點什么了当犯,也不枉我搜了這么多網(wǎng)頁垢村,看了那么多英文文檔啊。哦嚎卫,對嘉栓,在這里表揚一下stack overflow。甚是得朕心