Kurento Room Demo 教程 (开发)

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

本教程是使用Room SDK开发多重应用程序的指南。 它是基于kurento-room-demo中的演示应用程序的开发,它依赖于kurento-room-sdk,kurento-room-server和kurento-room-client-js组件。

下图尝试解释这些组件的集成以及它们之间的通信。

Kurento Room Demo 教程 (开发)_第1张图片

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%除以发布者的数量。

转载于:https://my.oschina.net/997155658/blog/841915

你可能感兴趣的:(Kurento Room Demo 教程 (开发))