Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7888567
  • 博文数量: 701
  • 博客积分: 2150
  • 博客等级: 上尉
  • 技术积分: 13233
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-29 16:28
个人简介

天行健,君子以自强不息!

文章分类

全部博文(701)

文章存档

2019年(2)

2018年(12)

2017年(76)

2016年(120)

2015年(178)

2014年(129)

2013年(123)

2012年(61)

分类: Android平台

2015-11-13 11:10:07

kurento-hello-world V6.0 源码分析

 

一、Web页面

后台服务用命令行启动:

   $ mvn clean compile exec:java

启动成功后,在chorme浏览器的地址栏输入:

  

即可看到如下页面


 

二、系统分析

2.1 示例程序的框架

这是一个Web应用程序,遵从客户-服务端的架构,主要分成三个部分:

?  页面客户端,包括HTML页面,页面按键调用的JavaScript函数,以及支持前两者的Web服务器----Tomcat

?  应用程序服务端,使用Java EE应用程序服务框架,调用Kurento Java Client API

Kurento Media Server, 是真正进行媒体处理的多媒体服务器。

 

这三者之间的通信都是使用WebSocket+JSON实现。

 

2.2. 系统通信的时序图

系统通信的时序图如下:

三、 页面客户端

3.1. Web服务器的实现

这个页面应用程序的Web服务器使用了基于Spring BootTomcat.

Web服务的pom.xml中相关的设置如下:

. . .

    8081

 

   

    org.kurento.tutorial.helloworld.HelloWorldApp

   

        

            org.springframework.boot

            spring-boot-starter-web

       

        . . .

   

. . .

 

在上面的配置中,

spring-boot-starter-web 告诉spring boot,我们要启动web应用;

org.kurento.tutorial.helloworld.HelloWorldApp 告诉 Spring Boot Maven在启动Tomcat后要执行的主类所在java文件位置;

 

3.2 处理页面请求的主类

接着看主类所在的Java文件

  “src/main/java/org/kurento/tutorial/helloworld/HelloWorldApp.java”

代码如下:

 

package org.kurento.tutorial.helloworld;

. . .

@Configuration

@EnableWebSocket

@EnableAutoConfiguration

public class HelloWorldApp implements WebSocketConfigurer {

    final static String DEFAULT_KMS_WS_URI = "ws://localhost:8888/kurento";

    @Bean  

    public HelloWorldHandler handler() {

        return new HelloWorldHandler();

    }

    

    @Bean

    public KurentoClient kurentoClient() {

        return KurentoClient.create(System.getProperty("kms.ws.uri", DEFAULT_KMS_WS_URI));

    }

    

    @Override

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(handler(), "/helloworld");

    }

   

    public static void main(String[] args) throws Exception {

        new SpringApplication(HelloWorldApp.class).run(args);

    }

}

其中,

@EnableAutoConfiguration 注解告诉Spring Boot根据添加的jar依赖猜测你想如何配置Spring

由于 spring-boot-starter-web 添加了TomcatSpring MVC,所以auto-configuration将假定你正在开发一个web应用并相应地对Spring进行设置。

 

这个Java程序最后部分是main方法public static void main(String[] args) throws Exception {

这只是一个标准的方法,它遵循Java对于一个应用程序入口点的约定。

我们的main方法通过调用run,将业务委托给了Spring BootSpringApplication类。SpringApplication将引导我们的应用,启动Spring,相应地启动被自动配置的Tomcat web服务器。

我们需要将 HelloWorldApp.class作为参数传递给run方法来告诉SpringApplication谁是主要的Spring组件。为了暴露任何的命令行参数,args数组也会被传递过去。

主类HelloWorldApp 实现了接口WebSocketConfigurer,

并通过注册函数

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(handler(), "/helloworld");

    }

注册了一个 WebSocketHandler, :

@Bean

public HelloWorldHandler handler() {

return new HelloWorldHandler();

}

HelloWorldHandler类实现了TextWebSocketHandler来处理文本WebSocket的请求,它就是WebSocket的服务端。

通过这个类来处理对路径 “/helloworld” 下的WebSocket请求。

我们可以看到,在对应页面客户端 src/main/resources/static/js/index.js中, WebSocket客户端的路径设置如下:

    var  ws = new WebSocket('ws://' + location.host + '/helloworld');

 

3.3 页面客户端代码分析

为了和服务端的WebSocket服务通信,我们需要在客户端页面中使用 JavaScript WebSocket

我们还需要使用 Kurento JavaScript 库,叫做 kurento-util.js, 来简化和服务端的WebRTC交互。
这个库依赖于 adapter.js, 它是谷歌开发JavaScript WebRTC库,它抽象并封装了各个浏览器间的差异,能方便页面程序开发人员的开发。

页面客户端还需要 jquery.js

 

这些库的都在 src/main/resources/static/index.html 而在中被链接:

. . .

 

 

. . .

src/main/resources/static/js/index.js中被使用。

 

NOTE】换句话说,我们在开发自己的应用时,可以不需要使用基于spring boot + tomcat的框架,
可以直接使用自己的Web服务框架,如Nginx,  python+tornado等,链接上这些JavaScript库也能正常工作。

 

>>> 首先,

需要创建一个对路径”/helloworld”通信的WebSocket:

 

var ws = new WebSocket('ws://' + location.host + '/helloworld');

 

当点击页面的start按键后,将调用src/main/resources/static/js/index.jsstart()函数,开始WebRTC通信:

function start() {

console.log('Starting video call ...');

 

// Disable start button

setState(I_AM_STARTING);

showSpinner(videoInput, videoOutput);

console.log('Creating WebRtcPeer and generating local sdp offer ...');

var options = {

localVideo : videoInput,

remoteVideo : videoOutput,

onicecandidate : onIceCandidate

}

webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options,

function(error) {

if (error)

return console.error(error);

webRtcPeer.generateOffer(onOffer);

});

}

 

function onOffer(error, offerSdp) {

    if (error)

        return console.error('Error generating the offer');

    console.info('Invoking SDP offer callback function ' + location.host);

    var message = {

        id : 'start',

        sdpOffer : offerSdp

    }

    sendMessage(message);

}

 

function onIceCandidate(candidate) {

    console.log('Local candidate' + JSON.stringify(candidate));

 

    var message = {

        id : 'onIceCandidate',

        candidate : candidate

    };

    sendMessage(message);

}

function sendMessage(message) {

    var jsonMessage = JSON.stringify(message);

    console.log('Senging message: ' + jsonMessage);

    ws.send(jsonMessage);

}

function showSpinner() {

    for (var i = 0; i < arguments.length; i++) {

        arguments[i].poster = './img/transparent-1px.png';

        arguments[i].style.background = "center transparent url('./img/spinner.gif') no-repeat";

    }

}

 

其中,kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv()是js/kurento-utils.js定义的函数,它抽象了WebRTC的内部细节(PeerConnectiongetUserStream)

并且,它使用HTML视频标签 videoInput ---- 显示视频摄像头(本地流)和视频标签videoOupup----显示远端由Kurento Media Server提供的流,来启动一个全双工的WebRTC通信。

在这个函数中,将会调用generateOffer()函数。

这个函数(generateOffer())会根据参数offeSDP生成客户端SDP请求,然后调用函数onOffer()。

onOffer()函数中,会通过WebSocketSDPid信息发送到WebSocket信令服务端;

 

>>> 然后,

WebSocket的监听函数onmessage() 用来处理从WebSocket服务端发送到客户端的JSON信令消息。

ws.onmessage = function(message) {

    var parsedMessage = JSON.parse(message.data);

    console.info('Received message: ' + message.data);

 

    switch (parsedMessage.id) {

    case 'startResponse':

        startResponse(parsedMessage);

        break;

    case 'error':

        if (state == I_AM_STARTING) {

            setState(I_CAN_START);

        }

        onError('Error message from server: ' + parsedMessage.message);

        break;

    case 'iceCandidate':

        webRtcPeer.addIceCandidate(parsedMessage.candidate, function(error) {

            if (error)

                return console.error('Error adding candidate: ' + error);

        });

        break;     

    default:   

        if (state == I_AM_STARTING) {

            setState(I_CAN_START);

        }          

        onError('Unrecognized message', parsedMessage);                                  

    }

}

从服务端发送来的消息有三种,startResponse, erroriceCandidate,每种消息都有对应的处理函数。

function startResponse(message) {

    setState(I_CAN_STOP);

    console.log('SDP answer received from server. Processing ...');

 

    webRtcPeer.processAnswer(message.sdpAnswer, function(error) {

        if (error)

            return console.error(error);

    });

}

function onError(error) {

    console.error(error);

}

>>> 最后,

用户点击页面stop按键后,将会调用src/main/resources/static/js/index.js中的stop()函数,

function stop() {

    console.log('Stopping video call ...');

    setState(I_CAN_START);

    if (webRtcPeer) {

        webRtcPeer.dispose();

        webRtcPeer = null;

 

        var message = {

            id : 'stop'

        }

        sendMessage(message);

    }

    hideSpinner(videoInput, videoOutput);

}

它会通过WebSocket客户端,发送停止消息到WebSocket服务端,结束服务。

 

四、应用程序服务端

如前所述,由页面客户端通过WebSocket发送的信令消息会被应用程序服务端的

 src/main/java/org/kurento/tutorial/helloworld/HelloWorldHandler.javaHelloWorldHandler类处理。

 

同时,他还需要创建一个和Kurento Media Server进行WebSocket通信的客户端:

src/main/java/org/kurento/tutorial/helloworld/HelloWorldApp.java

public class HelloWorldApp implements WebSocketConfigurer {

    final static String DEFAULT_KMS_WS_URI = "ws://localhost:8888/kurento";

   

    @Bean

    public HelloWorldHandler handler() {

        return new HelloWorldHandler();

    }

   

    @Bean

    public KurentoClient kurentoClient() {

        return KurentoClient.create(System.getProperty("kms.ws.uri", DEFAULT_KMS_WS_URI));

    }

   

    @Override

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(handler(), "/helloworld");

    }

   

    public static void main(String[] args) throws Exception {

        new SpringApplication(HelloWorldApp.class).run(args);

    }

}

当页面客户端通过websocket发送过来信令后,会在HelloWorldHandler类的 handlerTextMessage方法中进行分别处理:

    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);

 

        log.debug("Incoming message: {}", jsonMessage);

   

        switch (jsonMessage.get("id").getAsString()) {

        case "start":

            start(session, jsonMessage);

            break;

        case "stop": {

            UserSession user = users.remove(session.getId());

            if (user != null) {

                user.release();

            }

            break;

        }

        case "onIceCandidate": {

            JsonObject jsonCandidate = jsonMessage.get("candidate").getAsJsonObject();

            UserSession user = users.get(session.getId());                                

            if (user != null) {

                IceCandidate candidate = new IceCandidate(jsonCandidate.get("candidate").getAsString(),

                        jsonCandidate.get("sdpMid").getAsString(), jsonCandidate.get("sdpMLineIndex").getAsInt());

                user.addCandidate(candidate);

            }

            break;

        }

        default:

            sendError(session, "Invalid message with id " + jsonMessage.get("id").getAsString());

            break;

        }

    }

>>>首先,

当页面客户端发送了”start”信令后,将调用start方法进行处理:

import org.kurento.client.EventListener;

import org.kurento.client.IceCandidate;

import org.kurento.client.KurentoClient;

import org.kurento.client.MediaPipeline;

import org.kurento.client.OnIceCandidateEvent;

import org.kurento.client.WebRtcEndpoint;

 

public class HelloWorldHandler extends TextWebSocketHandler {

    @Autowired

private KurentoClient kurento;

 

private final ConcurrentHashMap users =

new ConcurrentHashMap();

 

    private void start(final WebSocketSession session, JsonObject jsonMessage) {

        try {

            // 1. Media logic (webRtcEndpoint in loopback)

            MediaPipeline pipeline = kurento.createMediaPipeline();

            WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();

            webRtcEndpoint.connect(webRtcEndpoint);

 

            // 2. Store user session

            UserSession user = new UserSession();

            user.setMediaPipeline(pipeline);

            user.setWebRtcEndpoint(webRtcEndpoint);

            users.put(session.getId(), user);

 

            // 3. SDP negotiation

            String sdpOffer = jsonMessage.get("sdpOffer").getAsString();

            String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);

 

            JsonObject response = new JsonObject();

            response.addProperty("id", "startResponse");

            response.addProperty("sdpAnswer", sdpAnswer);

 

            synchronized (session) {

                session.sendMessage(new TextMessage(response.toString()));

            }

 

            // 4. Gather ICE candidates

            webRtcEndpoint.addOnIceCandidateListener(new EventListener() {

                @Override

                public void onEvent(OnIceCandidateEvent event) {

                    JsonObject response = new JsonObject();

                    response.addProperty("id", "iceCandidate");

                    response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));

                    try {

                        synchronized (session) {

                            session.sendMessage(new TextMessage(response.toString()));

                        }

                    } catch (IOException e) {

                        log.error(e.getMessage());

                    }

                }

            });

            webRtcEndpoint.gatherCandidates();

        } catch (Throwable t) {

            sendError(session, t.getMessage());

        }

}

start()方法执行了下列动作:

1. 配置媒体处理逻辑

在这部分中,应用程序服务端配置了Kurento 如何处理媒体。

首先,使用KurentoClient对象kurento创建一个MediaPipeline对象,通过它,我们可以再创建我们想要的媒体元件并连接。

在这里,我们创建了一个WebRtcEndpoint媒体元件实例来接收WebRTC流并把它原样发回给客户端。

2. 存储用户session

首先要创建用户session,

它的定义 src/main/java/org/kurento/tutorial/helloworld/UserSession.java如下:

/**

 * User session.

 *

 * @author David Fernandez (d.fernandezlop@gmail.com)

 * @since 6.0.0

 */

public class UserSession {

    private WebRtcEndpoint webRtcEndpoint;

    private MediaPipeline mediaPipeline;

 

    public UserSession() {

    }

 

    public WebRtcEndpoint getWebRtcEndpoint() {

        return webRtcEndpoint;

    }

 

    public void setWebRtcEndpoint(WebRtcEndpoint webRtcEndpoint) {

        this.webRtcEndpoint = webRtcEndpoint;

    }

 

    public MediaPipeline getMediaPipeline() {

        return mediaPipeline;

    }

 

    public void setMediaPipeline(MediaPipeline mediaPipeline) {

        this.mediaPipeline = mediaPipeline;

    }

 

    public void addCandidate(IceCandidate i) {

        webRtcEndpoint.addIceCandidate(i);

    }

 

    public void release() {

        this.mediaPipeline.release();

    }

}

为了释放向kurento Media Server请求的资源,我们需要把用户session(例如MediaPipelineWebRtcEndpoint)存储起来,以在用户调用stop时释放掉。

 

3. WebRTC SDP协商

WebRTC中,SDP用来在端间实现媒体数据交换的协商。它是基于SDP提交和回答的数据交换机制。

这个协商是在方法processRequest的第三部分完成的,它使用了浏览器客户端的SDP提交,然后返回由WebRtcEndpoint生成的SDP回答。

 

4. 收集ICE候选者

Version 6中,Kurento完全支持 Trickle ICE协议。

基于这个原因,WebRtcEndpoint可以接收异步的ICE候选者。
为了处理它,每个WebRtcEndpoint提供了一个监听器(addOnIceGatheringDoneListener)来监听当ICE收集处理已完成的事件。

 

阅读(2929) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~