2019独角兽企业重金招聘Python工程师标准>>>
本教程是使用Room SDK开发多重应用程序的指南。 它是基于kurento-room-demo中的演示应用程序的开发,它依赖于kurento-room-sdk,kurento-room-server和kurento-room-client-js组件。
下图尝试解释这些组件的集成以及它们之间的通信。
Server端代码
房间服务器库项目的主类是Spring Boot应用程序类KurentoRoomServerApp。 在这个类中,我们将实例化构成服务器端的不同组件的 Spring bean。
此外,具有所有配置的类可以导入到其他Spring项目的应用程序类中(使用Spring的@Import注释或扩展服务器Spring Boot应用程序类)。
Room management
对于管理房间及其用户,服务器使用Room SDK库。 我们选择了通知风格的API,即NotificationRoomManager类。 我们必须将管理器定义为一个Spring bean,当需要时,它将作为依赖注入(使用@Autowired注释)。
但是,首先我们需要一个UserNotificationService实现,提供给NotificationRoomManager构造函数。 我们将使用JsonRpcNotificationService类型实现NotificationRoomManager接口,该实现将存储WebSocket会话以响应和通知发送回客户端。
我们还需要一个KurentoClientProvider接口的实现,我们将它命名为KMSManager:
@Bean
public NotificationRoomManager roomManager() {
return new NotificationRoomManager(userNotificationService, kmsManager());
}
Signaling
为了与客户端交互,我们的Demo程序将使用Kurento开发的JSON-RPC服务器库。 这个库用于Spring框架提供的WebSockets库进行通信传输协议。
我们为传入的JSON-RPC消息注册一个处理程序,以便我们可以根据其方法名称处理每个请求。 这个处理程序实现见前面描述的WebSocket API。
当在JsonRpcConfigurer API(由我们的Spring应用程序类实现)的方法registerJsonRpcHandlers(...)中添加处理程序时,指示请求路径。
处理程序类需要一些依赖,它们使用其构造函数,控制组件和用户通知服务(这些将在下面解释)传递。
@Bean
@ConditionalOnMissingBean
public RoomJsonRpcHandler roomHandler() {
return new RoomJsonRpcHandler(userControl(), notificationService());
}
@Override
public void registerJsonRpcHandlers(JsonRpcHandlerRegistry registry) {
registry.addHandler(roomHandler(), "/room");
}
处理程序的主要方法handleRequest(...)将针对来自客户端的每个请求进行调用。 所有与给定客户端的WebSocket通信都将在会话内部完成,当调用处理方法时,JSON-RPC库将提供引用。 请求-响应交换称为事务,也提供从其中获得WebSocket会话。
应用程序将存储与每个用户关联的会话和事务,以便当从Room SDK库调用时,我们的UserNotificationService实现将响应或服务器事件返回给客户端:
@Override
public final void handleRequest(Transaction transaction,
Request request) throws Exception {
...
notificationService.addTransaction(transaction, request);
sessionId = transaction.getSession().getSessionId();
ParticipantRequest participantRequest = new ParticipantRequest(sessionId,
Integer.toString(request.getId()));
...
transaction.startAsync();
switch (request.getMethod()) {
case JsonRpcProtocolElements.JOIN_ROOM_METHOD:
userControl.joinRoom(transaction, request, participantRequest);
break;
...
default:
log.error("Unrecognized request {}", request);
}
}
Manage user requests
处理程序将用户请求执行委派给不同的组件,即JsonRpcUserControl类的实例。 此对象将从请求中提取所需的参数,并从RoomManager调用必要的代码。
在joinRoom(...)请求的情况下,它将首先将用户和房间名称存储到会话中,以便以后更容易检索:
public void joinRoom(Transaction transaction, Request request,
ParticipantRequest participantRequest) throws ... {
String roomName = getStringParam(request,
JsonRpcProtocolElements.JOIN_ROOM_ROOM_PARAM);
String userName = getStringParam(request,
JsonRpcProtocolElements.JOIN_ROOM_USER_PARAM);
//store info in session
ParticipantSession participantSession = getParticipantSession(transaction);
participantSession.setParticipantName(userName);
participantSession.setRoomName(roomName);
roomManager.joinRoom(userName, roomName, participantRequest);
User responses and events
如前所述,通过为UserNotificationService API提供实现来创建NotificationRoomManager实例,在本例中,它将是类型为JsonRpcNotificationService的对象。
此类将所有打开的WebSocket会话存储在一个map中,从中获取将返回房间请求所需的事务对象。为了向客户端发送JSON-RPC事件(通知),它将使用Session对象的功能。
请注意,必须为NotificationRoomHandler(包含在Room SDK库中)的默认实现提供通知API(sendResponse,sendErrorResponse,sendNotification和closeSession)。房间应用程序的其他变体可以实现它们自己的NotificationRoomHandler,从而使得通知服务不必要。
在发送对给定请求的响应的情况下,将使用事务对象并从存储器中移除(不同的请求将意味着新的事务)。发送错误响应时会发生同样的情况:
@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", e);
}
}
要发送通知(或服务器事件),我们将使用会话对象。 在调用关闭会话方法(会议室处理程序,用户离开的结果,或直接从WebSocket处理程序,在连接超时或错误的情况下)之前,不能删除此选项:
@Override
public void sendNotification(final String participantId,
final String method, final Object params) {
SessionWrapper sw = sessions.get(participantId);
if (sw == null || sw.getSession() == null) {
log.error("No session found for id {}, unable to send notification {}: {}",
participantId, method, params);
return;
}
Session s = sw.getSession();
try {
s.sendNotification(method, params);
} catch (Exception e) {
log.error("Exception sending notification to user", e);
}
}
Dependencies
Kurento Spring应用程序使用Maven进行管理。 我们的服务器库在其pom.xml文件中有几个明确的依赖关系,Kurento Room SDK和Kurento JSON-RPC服务器用于实现服务器功能,而其他的用于测试:
org.kurento
kurento-room-sdk
org.kurento
kurento-jsonrpc-server
org.springframework.boot
spring-boot-starter-logging
org.kurento
kurento-room-test
test
org.kurento
kurento-room-client
test
org.mockito
mockito-core
test
Demo customization of the server-side
该演示通过扩展和替换一些Spring bean来向客房服务器添加一些定制。 这一切所有演示都是在新的Spring Boot应用程序类KurentoRoomDemoApp中完成的,它扩展了服务器的原始应用程序类:
public class KurentoRoomDemoApp extends KurentoRoomServerApp {
...
public static void main(String[] args) throws Exception {
SpringApplication.run(KurentoRoomDemoApp.class, args);
}
}
Custom KurentoClientProvider
作为代替提供者接口的默认实现,我们创建了类FixedNKmsManager,它允许维护一系列KurentoClient,每个都是由演示配置中指定的URI创建。
Custom user control
为了提供对附加WebSocket请求类型customRequest的支持,创建了一个扩展版本的JsonRpcUserControl,DemoJsonRpcUserControl。
此类覆盖了方法customRequest(...)以允许切换FaceOverlayFilter,该方法添加或删除用户头部的帽子。 它将过滤器对象存储为WebSocket会话中的一个属性,以便更容易删除它:
@Override
public void customRequest(Transaction transaction,
Request request, ParticipantRequest participantRequest) {
try {
if (request.getParams() == null
|| request.getParams().get(CUSTOM_REQUEST_HAT_PARAM) == null)
throw new RuntimeException("Request element '" + CUSTOM_REQUEST_HAT_PARAM
+ "' is missing");
boolean hatOn = request.getParams().get(CUSTOM_REQUEST_HAT_PARAM)
.getAsBoolean();
String pid = participantRequest.getParticipantId();
if (hatOn) {
if (transaction.getSession().getAttributes()
.containsKey(SESSION_ATTRIBUTE_HAT_FILTER))
throw new RuntimeException("Hat filter already on");
log.info("Applying face overlay filter to session {}", pid);
FaceOverlayFilter faceOverlayFilter = new FaceOverlayFilter.Builder(
roomManager.getPipeline(pid)).build();
faceOverlayFilter.setOverlayedImage(this.hatUrl,
this.offsetXPercent, this.offsetYPercent, this.widthPercent,
this.heightPercent);
//add the filter using the RoomManager and store it in the WebSocket session
roomManager.addMediaElement(pid, faceOverlayFilter);
transaction.getSession().getAttributes().put(SESSION_ATTRIBUTE_HAT_FILTER,
faceOverlayFilter);
} else {
if (!transaction.getSession().getAttributes()
.containsKey(SESSION_ATTRIBUTE_HAT_FILTER))
throw new RuntimeException("This user has no hat filter yet");
log.info("Removing face overlay filter from session {}", pid);
//remove the filter from the media server and from the session
roomManager.removeMediaElement(pid, (MediaElement)transaction.getSession()
.getAttributes().get(SESSION_ATTRIBUTE_HAT_FILTER));
transaction.getSession().getAttributes()
.remove(SESSION_ATTRIBUTE_HAT_FILTER);
}
transaction.sendResponse(new JsonObject());
} 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);
}
}
}
Dependencies
在它的pom.xml文件中,Kurento Room Server,Kurento Room Client JS(用于客户端库),Spring logging library和Kurento Room Test的测试实现有几个依赖关系。 我们不得不手动排除一些传递依赖,以避免冲突:
org.kurento
kurento-room-server
org.springframework.boot
spring-boot-starter-logging
org.apache.commons
commons-logging
org.kurento
kurento-room-client-js
org.kurento
kurento-room-test
test
org.springframework.boot
spring-boot-starter-log4j
Client 端代码
本节介绍kurento-room-demo包含的AngularJS应用程序的代码。AngularJS特定代码将不会解释,因为我们的目标是了解房间机制(读者不应担心,因为下面的指示也将为使用简单或传统的JavaScript开发的客户端应用程序服务)。
Libraries
包含下列必须的 JavaScript 文件:
-
jQuery: 是一个跨平台JavaScript库,旨在简化HTML的客户端脚本。
-
Adapter.js: 是由Google维护的WebRTC JavaScript实用程序库,用于抽象化浏览器差异。
-
EventEmitter: 为浏览器实现事件库。
-
kurento-jsonrpc: 是一个小的RPC库,我们将用于这个应用程序的信令面。
-
kurento-utils: 是一个Kurento实用程序库,旨在简化浏览器中的WebRTC管理。
-
KurentoRoom: 这个脚本是前面描述的库,它包含在kurento-room-client-js项目。
Init resources
为了加入一个房间,调用KurentoRoom的初始化函数,提供服务器的URI用于监听JSON-RPC请求。 在这种情况下,会议室服务器在请求路径/room上监听secure WebSocket连接:
var wsUri = 'wss://' + location.host + '/room';
你必须提供uername和room:
var kurento = KurentoRoom(wsUri, function (error, kurento) {...}
回调参数是我们订阅房间发出的事件的地方。
如果WebSocket初始化失败,错误对象将不为null,我们应该检查服务器的配置或状态。
否则,我们可以很好的创建一个Room和本地的Stream对象。 请注意,传递给本地流(音频,视频,数据)的选项的constraints暂时被忽略:
room = kurento.Room({
room: $scope.roomName,
user: $scope.userName
});
var localStream = kurento.Stream(room, {
audio: true,
video: true,
data: true
});
Webcam and mic access
选择何时加入会议室留给应用程序,之前我们必须首先获得对网络摄像头和麦克风的访问,然后才调用join方法。 这通过在本地流上调用init方法来完成:
localStream.init();
在其执行期间,将提示用户授予对系统上的媒体资源的访问权限。 根据响应,流对象将发出访问接受或访问被拒绝的事件。 应用程序必须注册这些事件才能继续连接操作:
localStream.addEventListener("access-denied", function () {
//alert of error and go back to login page
}
这里,当授予访问权限时,我们通过在room对象上调用connect继续联接操作:
localStream.addEventListener("access-accepted", function () {
//register for room-emitted events
room.connect();
}
Room events
作为连接调用的结果,开发人员通常应该注意房间发出的几种事件类型。
如果连接导致失败,则会生成错误室事件:
room.addEventListener("error-room", function (error) {
//alert the user and terminate
});
如果连接成功并且用户被接受为房间中的有效对等体,则将使用房间连接的事件。
下一段代码将包含对对象ServiceRoom和ServiceParticipant的引用,这些对象是由演示应用程序定义的Angular服务。 值得一提的是,ServiceParticipant使用流作为参与者:
room.addEventListener("room-connected", function (roomEvent) {
if (displayPublished ) { //demo cofig property
//display my video stream from the server (loopback)
localStream.subscribeToMyRemote();
}
localStream.publish(); //publish my local stream
//store a reference to the local WebRTC stream
ServiceRoom.setLocalStream(localStream.getWebRtcPeer());
//iterate over the streams which already exist in the room
//and add them as participants
var streams = roomEvent.streams;
for (var i = 0; i < streams.length; i++) {
ServiceParticipant.addParticipant(streams[i]);
}
}
由于我们刚刚指示我们的本地流在房间中发布,我们应该听相应的事件,并注册我们的本地流作为本地参与者在房间里。 此外,我们在演示中添加了一个选项,以显示我们不变的本地视频,以及通过媒体服务器传递的视频(如果配置为这样):
room.addEventListener("stream-published", function (streamEvent) {
//register local stream as the local participant
ServiceParticipant.addLocalParticipant(localStream);
//also display local loopback
if (mirrorLocal && localStream.displayMyRemote()) {
var localVideo = kurento.Stream(room, {
video: true,
id: "localStream"
});
localVideo.mirrorLocalStream(localStream.getWrStream());
ServiceParticipant.addLocalMirror(localVideo);
}
});
如果参与者决定发布她的媒体,我们应该知道它的流被添加到房间:
room.addEventListener("stream-added", function (streamEvent) {
ServiceParticipant.addParticipant(streamEvent.stream);
});
当流被移除时(当参与者离开房间时)必须采用相反的机制:
room.addEventListener("stream-removed", function (streamEvent) {
ServiceParticipant.removeParticipantByStream(streamEvent.stream);
});
另一个重要事件是由服务器端的介质错误触发的事件:
room.addEventListener("error-media", function (msg) {
//alert the user and terminate the room connection if deemed necessary
});
还有其他事件是从服务器发送的通知的直接后果,例如房间解散:
room.addEventListener("room-closed", function (msg) {
//alert the user and terminate
});
最后,客户端API允许我们向房间中的其他对等体发送消息:
room.addEventListener("newMessage", function (msg) {
ServiceParticipant.showMessage(msg.room, msg.user, msg.message);
});
Streams interface
在订阅新流之后,应用可以从流接口使用这两种方法中的一种或两种。
stream.playOnlyVideo(parentElement, thumbnailId):
此方法会将视频HTML标记附加到由parentElement参数指定的现有元素(可以是标识符,也可以直接是HTML标记)。视频元素将自动播放,没有播放控件。如果流是本地流,则视频将静音。
期望具有标识符thumbnailId的元素存在并且是可选择的。当WebRTC流可以分配给视频元素的src属性时,将显示此元素(jQuery .show()方法)。
stream.playThumbnail(thumbnailId):
在标识为thumbnailId的元素中创建一个div元素(类名称参与者)。来自流的视频将通过调用playOnlyVideo(parentElement,thumbnailId)作为parentElement在这个div(参与者)内播放。
使用流的全局ID,名称标签也将作为div元素中的文本字符串显示在参与者元素上。 name标签的样式由CSS类名指定。
缩略图的大小必须由应用程序定义。在房间演示中,缩略图以14%的宽度开始,直到房间中有7个以上的发布商为止(7 x 14%= 98%)。从这一点开始,将使用另一个公式来计算宽度,98%除以发布者的数量。