Kurento应用开发指南(以Kurento 5.0为模板) 之四:示例教程 一对一视频呼叫

本文主要是介绍Kurento应用开发指南(以Kurento 5.0为模板) 之四:示例教程 一对一视频呼叫,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

5.3 教程四-一对一的视频呼叫

这个页面应用程序使用WebRTC技术实现了一个一对一的呼叫,换言话说,这个应用提供了一个简单的视频电话

5.3.1 运行示例程序

运行这个DEMO之前,你需要先安装Kurento Media Server.可以看前面的介绍。
另外,你还需要先安装好 JDK (at least version 7), Maven, Git, 和 Bower。
在Ubuntu上安装这些的命令如下:
sudo apt-get install curl
curl -sL https://deb.nodesource.com/setup | sudo bash -
sudo apt-get install -y nodejs
sudo npm install -g bower
启动应用程序之前,需要先下载源,并编译运行,命令如下:
git clone https://github.com/Kurento/kurento-tutorial-java.git
cd kurento-tutorial-java/kurento-one2one-call
mvn clean compile exec:java
默认地,这个应用程序部署在8080端口上,可以使用兼容WebRTC的浏览器打开URL http://localhost:8080


5.3.2 Understanding this example

下面的图片显示了在浏览上运行这个DEMO时截图。
这个应用程序(一个HTML页面)的接口是由两个HTML5视频标签组成的:
  一个用来显示本地流;
  另一个用来显示远端的流;
如果有两用户,A和B都使用这个应用程序,则媒体流的工作方式如下:
A的摄像头的流发送到Kurento Media Server,Kurento Media Server会将这个流发送给B;
同样地,B也会将流发送到Kurento Media Server,它再发给A。
这意味着,KMS提供了一个B2B (back-to-back) 的呼叫服务。
 
Figure 9.1: One to one video call screenshot


为了实现上述的工作方式,需要创建一个由两个WebRtc端点以B2B方式连接的媒体管道,媒体管道的示例图如下:
 
Figure 9.2: One to one video call Media Pipeline


客户端和服务端的通信是通过基于WebSocket上的JSON消息的信令协议实现的,客户端和服务端的工作时序如下:
1. 用户A在服务器上注册他的名字
2. 用户B在服务器注册他的名字
3. 用户A呼叫用户B
4. 用户B接受呼叫
5. 通信已建立,媒体在用户A与用户B之间流动
6. 其中一个用户结束这次通信
时序流程的细节如下图所示:


 
Figure 9.3: One to many one call signaling protocol
如图中所示,为了在浏览器和Kurento之间建立WebRTC连接,需要在客户端和服务端之间进行SDP交互。
特别是,SDP协商连接了浏览器的WebRtcPeer和服务端的WebRtcEndpoint。 
下面的章节描述了服务端和客户端的细节,以及DEMO是如何运行的。源码可以从GitHub上下载;


5.3.3 应用程序服务端逻辑

这个DEMO的服务端是使用Java的Spring Boot框架开发的。这个技术可以嵌入到Tomcat页面服务器中,从而简化开发流程。
Note: You can use whatever Java server side technology you prefer to build 
web applications with Kurento. For example, a pure Java EE application, SIP Servlets, 
Play, Vertex, etc. We have choose Spring Boot for convenience.


下面的图显示了服务端的类图。
这个DEMO的主类为One2OneCallApp, 如代码中所见,KurentoClient作为Spring Bean在类中进行了实例化。






 


Figure 9.4: Server-side class diagram of the one to one video call app


@Configuration
@EnableWebSocket
@EnableAutoConfiguration
public class One2OneCallApp implements WebSocketConfigurer {
     @Bean
     public CallHandler callHandler() {
          return new CallHandler();
     }


     @Bean
     public UserRegistry registry() {
          return new UserRegistry();
     }


     @Bean
     public KurentoClient kurentoClient() {
          return KurentoClient.create("ws://localhost:8888/kurento");
     }


     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
          registry.addHandler(callHandler(), "/call");
     }


     public static void main(String[] args) throws Exception {
          new SpringApplication(One2OneCallApp.class).run(args);
     }
}
这个页面应用程序使用了单页面应用程序架构(SPA:Single Page Application architecture ),
并使用了WebSocket来作为客户端与服务端通信的请求与响应。
特别地,主app类实现了WebSocketConfigurer接口来注册一个WebSocketHandler来处理WebSocket请求。


CallHandler类实现了TextWebSocketHandler,用来处理文本WebSocket的请求。
这个类的主要实现的方法就是handleTextMessage, 这个方法实现了对请求的动作: 
通过WebSocket返回对请求的响应。换句话说,它实现前面的时序图中的信令协议的服务端部分。


在设计的协议中,有三种类型的输入消息: 注册,呼叫, incomingCallResponse和stop。
这些消息对应的处理都在switch中。
public class CallHandler extends TextWebSocketHandler {
     private static final Logger log = LoggerFactory.getLogger(CallHandler.class);
     private static final Gson gson = new GsonBuilder().create();
     private ConcurrentHashMap<String, CallMediaPipeline> pipelines =
                    new ConcurrentHashMap<String, CallMediaPipeline>();


     @Autowired
     private KurentoClient kurento;


     @Autowired
     private UserRegistry registry;


     @Override
     public void handleTextMessage(WebSocketSession session, TextMessage message)
     throws Exception {
          JsonObject jsonMessage = gson.fromJson(message.getPayload(),
          JsonObject.class);
          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 "register":
          try {
               register(session, jsonMessage);
          } catch (Throwable t) {
               log.error(t.getMessage(), t);
               JsonObject response = new JsonObject();
               response.addProperty("id", "resgisterResponse");
               response.addProperty("response", "rejected");
               response.addProperty("message", t.getMessage());
               session.sendMessage(new TextMessage(response.toString()));
          }
     break;
     case "call":
          try {
               call(user, jsonMessage);
          } catch (Throwable t) {
               log.error(t.getMessage(), t);
               JsonObject response = new JsonObject();
               response.addProperty("id", "callResponse");
               response.addProperty("response", "rejected");
               response.addProperty("message", t.getMessage());
               session.sendMessage(new TextMessage(response.toString()));
          }
       ​  break;
     ​    case "incomingCallResponse":
     ​         incomingCallResponse(user, jsonMessage);
      ​   break;
      ​   case "stop":
     ​         stop(session);
     ​    break;
    ​     default:
     ​    break;
     }
}
private void register(WebSocketSession session, JsonObject jsonMessage)
     throws IOException {
          ...
}
private void call(UserSession caller, JsonObject jsonMessage)
throws IOException {
     ...
}
private void incomingCallResponse(UserSession callee, JsonObject jsonMessage)
throws IOException {
     ...
}
public void stop(WebSocketSession session) throws IOException {
...
}
@Override
public void afterConnectionClosed(WebSocketSession session,
               CloseStatus status) throws Exception {
     registry.removeBySession(session);
     }
}
在下面的代码片断中,我们可以看到注册方法,基本上,它包含了从注册信息中得到的名字属性,并检测它是否被注册过。
如果没有,则新用户被注册且有一个接受的消息发送给它;


private void register(WebSocketSession session, JsonObject jsonMessage)
throws IOException {
     String name = jsonMessage.getAsJsonPrimitive("name").getAsString();


     UserSession caller = new UserSession(session, name);
     String responseMsg = "accepted";
     if (name.isEmpty()) {
          responseMsg = "rejected: empty user name";
     } else if (registry.exists(name)) {
          responseMsg = "rejected: user '" + name + "' already registered";
     } else {
          registry.register(caller);
     }
     JsonObject response = new JsonObject();
     response.addProperty("id", "resgisterResponse");
     response.addProperty("response", responseMsg);
     caller.sendMessage(response);
}


在call方法中,服务端会检查在消息属性栏中的名字是否已注册,然后发送一个incomingCall消息给它。
或者,如果这个名字未注册,则会有一个callResponse消息发送给呼叫者以拒绝这次呼叫。


private void call(UserSession caller, JsonObject jsonMessage)
throws IOException {
     String to = jsonMessage.get("to").getAsString();
     String from = jsonMessage.get("from").getAsString();
     JsonObject response = new JsonObject();
     if (registry.exists(to)) {
          UserSession callee = registry.getByName(to);
          caller.setSdpOffer(jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString());
          caller.setCallingTo(to);
          response.addProperty("id", "incomingCall");
          response.addProperty("from", from);
          callee.sendMessage(response);
          callee.setCallingFrom(from);
     } else {
          response.addProperty("id", "callResponse");
          response.addProperty("response", "rejected: user '" + to+ "' is not registered");
          caller.sendMessage(response);
     }
}


stop方法结束这次呼叫。这个过程会被呼叫者和被叫者在通信中被调用。
结果是这两端会释放媒体管道并结束通信:
public void stop(WebSocketSession session) throws IOException {
     String sessionId = session.getId();
     if (pipelines.containsKey(sessionId)) {
          pipelines.get(sessionId).release();
          CallMediaPipeline pipeline = pipelines.remove(sessionId);
          pipeline.release();
          // Both users can stop the communication. A 'stopCommunication'
          // message will be sent to the other peer.
          UserSession stopperUser = registry.getBySession(session);
          UserSession stoppedUser = (stopperUser.getCallingFrom() != null) ? registry
               .getByName(stopperUser.getCallingFrom()) : registry
               .getByName(stopperUser.getCallingTo());
          JsonObject message = new JsonObject();
          message.addProperty("id", "stopCommunication");
          stoppedUser.sendMessage(message);
     }
}


在 incomingCallResponse方法中,如果被叫用户接受了这个呼叫,那么就会以B2B方式创建媒体元素并连接呼叫者与被叫者。
通常,服务端会创建一个 CallMediaPipeline对象,用来封装媒体管道的创建和管理。
然后,这个对象就用来在用户浏览器间进行媒体交互协商。


浏览器上WebRTC端点与Kurento Media Server的WebRtcEndpoint间的协商
是通过客户端生成的SDP(提交)与服务端生成的SDP(回答)实现的。
这个SDP的回答是由类CallMediaPipeline中Kurento Java Client生成的。
用于生成SDP的方法为generateSdpAnswerForCallee(calleeSdpOffer) 和 generateSdpAnswerForCaller(callerSdpOffer):


private void incomingCallResponse(UserSession callee, JsonObject jsonMessage)
throws IOException {
     String callResponse = jsonMessage.get("callResponse").getAsString();
     String from = jsonMessage.get("from").getAsString();
     UserSession calleer = registry.getByName(from);
     String to = calleer.getCallingTo();


     if ("accept".equals(callResponse)) {
          log.debug("Accepted call from '{}' to '{}'", from, to);
          CallMediaPipeline pipeline = null;
          try {
               pipeline = new CallMediaPipeline(kurento);
               pipelines.put(calleer.getSessionId(), pipeline);
               pipelines.put(callee.getSessionId(), pipeline);
               String calleeSdpOffer = jsonMessage.get("sdpOffer").getAsString();
               String calleeSdpAnswer = pipeline.generateSdpAnswerForCallee(calleeSdpOffer);
               String callerSdpOffer = registry.getByName(from).getSdpOffer();
               String callerSdpAnswer = pipeline.generateSdpAnswerForCaller(callerSdpOffer);
               JsonObject startCommunication = new JsonObject();
               startCommunication.addProperty("id", "startCommunication");
               startCommunication.addProperty("sdpAnswer", calleeSdpAnswer);
               callee.sendMessage(startCommunication);
               JsonObject response = new JsonObject();
               response.addProperty("id", "callResponse");
               response.addProperty("response", "accepted");
               response.addProperty("sdpAnswer", callerSdpAnswer);
               calleer.sendMessage(response);
          } catch (Throwable t) {
               log.error(t.getMessage(), t);
               if (pipeline != null) {
                         pipeline.release();
               }
               pipelines.remove(calleer.getSessionId());
               pipelines.remove(callee.getSessionId());
               JsonObject response = new JsonObject();
               response.addProperty("id", "callResponse");
               response.addProperty("response", "rejected");
               calleer.sendMessage(response);
               response = new JsonObject();
               response.addProperty("id", "stopCommunication");
               callee.sendMessage(response);
          }
     } else {
          JsonObject response = new JsonObject();
          response.addProperty("id", "callResponse");
          response.addProperty("response", "rejected");
          calleer.sendMessage(response);
     }
}


这个DEMO的媒体逻辑是在类CallMediaPipeline中实现的,如上图所见,媒体管道的组成很简单:
由两个WebRtcEndpoint直接相连组成。需要注意的WebRtcEndpoints需要做两次连接,每次连接一个方向的。
public class CallMediaPipeline {
     private MediaPipeline pipeline;
     private WebRtcEndpoint callerWebRtcEP;
     private WebRtcEndpoint calleeWebRtcEP;
     public CallMediaPipeline(KurentoClient kurento) {
          try {
               this.pipeline = kurento.createMediaPipeline();
               this.callerWebRtcEP = new WebRtcEndpoint.Builder(pipeline).build();
               this.calleeWebRtcEP = new WebRtcEndpoint.Builder(pipeline).build();
               this.callerWebRtcEP.connect(this.calleeWebRtcEP);
               this.calleeWebRtcEP.connect(this.callerWebRtcEP);
          } catch (Throwable t) {
               if(this.pipeline != null){
                   pipeline.release();
               }
          }
     }
     public String generateSdpAnswerForCaller(String sdpOffer) {
          return callerWebRtcEP.processOffer(sdpOffer);
     }


     public String generateSdpAnswerForCallee(String sdpOffer) {
          return calleeWebRtcEP.processOffer(sdpOffer);
     }
     public void release() {
          if (pipeline != null) {
               pipeline.release();
          }
     }
}


在这个类中,我们可以看到方法generateSdpAnswerForCaller 和 generateSdpAnswerForCallee的实现,
这些方法引导WebRtc端点创建合适的回答。


5.3.4 客户端

现在来看应用程序客户端的代码。为了调用前面提到的服务端的WebSocket服务,我们使用了JavaScript类WebSocket。
我们使用了特殊的Kurento JavaScript库,叫做kurento-utils.js来简化WebRTC的交互,
这个库依赖于adapter.js,它是一个JavaScript WebRTC设备,由Google维护,用来抽象浏览器之间的差异。
最后,这个应用程序还需要jquery.js.


这些库都链接到了index.html页面中,并都在index.js中被使用。
在下面的代码片断中,我们可以看到在path /call下WebSocket(变量ws)的创建,
然后,WebSocket的监听者onmessage被用来实现在客户端的JSON信令协议。
.
注意,在客户端有四个输入信息:resgisterResponse, callResponse,incomingCall, 和startCommunication,
用来实现通信中的各个步骤。
例如,在函数 call and incomingCall (for caller and callee respectively)中,
kurento-utils.js的函数WebRtcPeer.startSendRecv用来启动WebRTC通信。


var ws = new WebSocket('ws://' + location.host + '/call');
ws.onmessage = function(message) {
     var parsedMessage = JSON.parse(message.data);
     console.info('Received message: ' + message.data);


     switch (parsedMessage.id) {
     case 'resgisterResponse':
          resgisterResponse(parsedMessage);
     break;
     case 'callResponse':
          callResponse(parsedMessage);
     break;
     case 'incomingCall':
          incomingCall(parsedMessage);
     break;
     case 'startCommunication':
          startCommunication(parsedMessage);
     break;
     case 'stopCommunication':
          console.info("Communication ended by remote peer");
         stop(true);
     break;
     default:
          console.error('Unrecognized message', parsedMessage);
     }
}
function incomingCall(message) {
     //If bussy just reject without disturbing user
     if(callState != NO_CALL){
          var response = {
               id : 'incomingCallResponse',
               from : message.from,
               callResponse : 'reject',
               message : 'bussy'
          };
          return sendMessage(response);
     }
     setCallState(PROCESSING_CALL);
     if (confirm('User ' + message.from + ' is calling you. Do you accept the call?')) {
          showSpinner(videoInput, videoOutput);
           webRtcPeer = kurentoUtils.WebRtcPeer.startSendRecv(videoInput, videoOutput,
       ​  function(sdp, wp) {
          var response = {
               id : 'incomingCallResponse',
               from : message.from,
               callResponse : 'accept',
               sdpOffer : sdp
          };
          sendMessage(response);
     }, function(error){
          setCallState(NO_CALL);
     });
     } else {
          var response = {
               id : 'incomingCallResponse',
               from : message.from,
               callResponse : 'reject',
                    message : 'user declined'
          };
          sendMessage(response);
          stop();
     }
}


function call() {
     if(document.getElementById('peer').value == ''){
          window.alert("You must specify the peer name");
          return;
}
setCallState(PROCESSING_CALL);
showSpinner(videoInput, videoOutput);
kurentoUtils.WebRtcPeer.startSendRecv(videoInput, videoOutput, function(offerSdp, wp) {
     webRtcPeer = wp;
     console.log('Invoking SDP offer callback function');
     var message = {
               id : 'call',
               from : document.getElementById('name').value,
               to : document.getElementById('peer').value,
                sdpOffer : offerSdp
     };
     sendMessage(message);
}, function(error){
     console.log(error);
     setCallState(NO_CALL);
});
}


5.3.5 依赖库

This Java Spring application is implementad using Maven. 
The relevant part of the pom.xml is where Kurento dependencies are declared. 
As the following snippet shows, we need two dependencies: the Kurento Client Java dependency
(kurento-client) and the JavaScript Kurento utility library (kurento-utils) for the client-side:
<dependencies>
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-client</artifactId>
<version>[5.0.0,6.0.0)</version>
</dependency>
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-utils-js</artifactId>
<version>[5.0.0,6.0.0)</version>
</dependency>
</dependencies>
Kurento framework uses Semantic Versioning for releases. 
Notice that range [5.0.0,6.0.0) downloads the latest version of Kurento artefacts 
from Maven Central in version 5 (i.e. 5.x.x). Major versions are released when incompatible changes are made.
Note: We are in active development. You can find the latest version of Kurento Java Client at Maven Central.
Kurento Java Client has a minimum requirement of Java 7. 
To configure the application to use Java 7, we have to include the following properties in the properties section:
<maven.compiler.target>1.7</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>

这篇关于Kurento应用开发指南(以Kurento 5.0为模板) 之四:示例教程 一对一视频呼叫的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1093874

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,