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

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

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

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

 java

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上下载;

 node

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端点建立合适的回答。

 jquery

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);
});
}

 git

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>


 github

相关文章
相关标签/搜索