美文网首页Java服务器端编程kurento开发者流媒体服务器
Kurento Tutorial 官方文档学习记录 Java -

Kurento Tutorial 官方文档学习记录 Java -

作者: 你就不能换个昵称 | 来源:发表于2017-11-02 19:44 被阅读103次

    想看英文可以移步官方文档

    理解例子的源码

    Kurento 提供了 Kurento Java Client 来控制Kurento Media
    Server(KMS)
    ,这个例子用了Kurento Java Client(KJC)来控制KMS。并且在Kurento Java Client上用了Java 的Spring-Boot框架。

    1.应用的逻辑

    很简单,浏览器将摄像头和麦克风获得的本地流传输给远程的KMS(或本级部署的)然后再不经调制的传输回本地浏览器进行显示。
    要完成这个动作,我们需要创造一个只包含一个Media ElementMedia Pipeline。本例中那个唯一的 Media Element 就是一个 WebRtcEndpoint,它具有全双工交换WebRTC媒体流的能力。它自己与自己相连,以保证送出的流通过一圈再传送回来,也就是我们要实现的镜像功能(loopback)。
    介绍如下


    这块其实很好理解,不用太过注意这个图。

    2.架构

    由于是浏览器应用,所以也服从CS架构。
    在客户端,使用 JavaScript 来实现客户端逻辑。在服务器端,用基于 Spring-BootJava 实现,调用 Kurento Java Client API 来控制 Kurento Media Server
    总之,这个应用是一个三层结构的应用,一共有三层实体分别为

    客户端 —— 客户端服务器&Kurento Java Client —— KMS

    注意这里的客户端服务器就是KJC,只是看的角度不一样。它处理来自客户端的请求,同时控制KMS,即,既是客户端的服务器,又是KMS的客户端。
    由于是三层结构,就需要两个WebSocket了,一个用于前两个之间的连接,遵循Custom Signaling Protocol,另一个用于后两者的连接,遵循Kurento Protocol
    想进一步了解可以看关于这块的详细信息

    接下来用一个SD来体现这三者之间的关系。该图中包含的详细信息十分重要,每一个信息的交互都会在后边的代码中体现。


    图1

    3.代码分析

    (1)服务器端

    服务器端就是SD图中间的绿色实体,也就是三层架构中位于中间层的Application Server
    再次标注一下,服务器端用了基于Spring-BootJava来实现。

    • 首先来看一下服务器端代码的整体架构


      图2

      图中列出的几个类就是我们的Java的代码中的所有类了。一会再看代码的时候我们会一步一步的发现这个图的指向具体意思。这个图也是一个对于理解代码非常重要的图。

    • 主类 HelloWorldApp
    @Bean
       public HelloWorldHandler handler() {
          return new HelloWorldHandler();
       }
    @Bean
       public KurentoClient kurentoClient() {
          return KurentoClient.create();
       }
    

    我们可以看到,在HelloWorldApp类中,HelloWorldHandlerKurentoClient都被实例化为了 Bean. Bean是Spring-Boot框架中一个重要的结构名称,具体可以去了解Spring-Boot框架中的这部分内容,这里你可以就把它当成,是 HelloWorldApp 是一个大容器,里面装了两个小容器分别是 HelloWorldHandlerKurentoClient 。这就是为什么图2HelloWorldApp 实体有两条尖头分别指向 HelloWorldHandlerKurentoClient 实体

    这里的KurentoClient在创建时默认了KMS服务器在本机上。如果你的KMS服务器部署在远程的话,就需要向如下代码一样创建KurentoClient

    @Bean
      public KurentoClient kurentoClient() {
        return KurentoClient.create("ws://197.162.38.40:8888/kurento");
      }
    
    public class HelloWorldApp implements WebSocketConfigurer {
    

    可以看到 HelloWorldApp 类在创建时 implements 了 WebSocketConfigurer

    @Override
       public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
          registry.addHandler(handler(), "/helloworld");
       }
    

    通过 Override WebSocketConfigurerregisterWebSocketHandlers(WebSocketHandlerRegistry registry)方法,将 HelloWorldHandler 类的实例handler作为WebSocketHandler来处理 WebSocket 请求,且处理路径为“/helloworld”

    • WebSocket处理类 HelloWorldHandler
      可以看到 HelloWorldHandler 类在创建时 implements 了TextWebSocketHandler
      该类的核心部分是handleTextMessage方法,这个方法通过手动编写逻辑实现了上文提到的两个 WebSocket 中遵循 Custom Signaling Protocol 的那个 WebSocket 的处理程序,也就是作为Client Server的那部分功能,体现在图1JavaScriptClientApplicationServer 之间的信息交互。而通过直接调用 Kurento Java Client API 来完成第二个 websocket 的通信,是自动的,不用手动码逻辑。
      注意,这里的代码每一处都可以在图1中的信息交互中找到对应的表示。
      根据JsonMessage的不同分了三个不同的case,其中最重要的是start,里面调用了start函数,见下文。
      start方法主要完成了如下四个工作,注释也都写出来了。

    配置媒体处理逻辑

    MediaPipeline pipeline = kurento.createMediaPipeline();
    WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
    webRtcEndpoint.connect(webRtcEndpoint);
    1.创建MediaPipeline
    2.用pipeline创建WebRtcEndpoint
    3.创建好的webRtcEndpoint自己和自己相连

    • 这几步在前文都清楚得提到过

    存储用户会话

    UserSession user = new UserSession();
    user.setMediaPipeline(pipeline);
    user.setWebRtcEndpoint(webRtcEndpoint);
    users.put(session.getId(), user);
    由于在最后会调用stop来释放,所以需要存储用户会话来保证这个功能,也就是要存储MediaPipelineWebRtcEndpoint

    SDP通讯

    String sdpOffer = >>jsonMessage.get("sdpOffer").getAsString();
    jsonMessage中获得sdpOffer部分并转换成String
    String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);
    sdpOffer作为参数传给webRtcEndpoint形成sdpAnswer,调用了webRtcEndpointprocessOffer方法,这里对应图1中的左边前两条数据交换箭头,其中processOffer方法完成了两条线中间右边Application ServerKMS之间的一系列数据交换。
    JsonObject response = new JsonObject();
    response.addProperty("id", "startResponse");
    response.addProperty("sdpAnswer", sdpAnswer);
    synchronized (session) { session.sendMessage(new TextMessage(response.toString())); }
    这里是把sdpAnswer加入一个json里再加上一些必要信息然后再转换成String并在会话里送出。

    • WebRtcpeer 之间交换媒体数据遵循SDP (Session Description protocol),也就是说用的是SDPOfferSDPAnswer机制。
    • 这一步完成了图1中上半部份的SDP通信的功能,包括处理来自JavaScript的客户端请求和控制远端KMS两部分动作。
    • 注意⚠️,JavaScriptClientwebsocket请求用这里的HelloWorldHandler来处理是由于在Index.js里制定了websocket的处理路径是/helloworld,并且在主程序HelloWorldApp里将handler注册到了/helloworld路径里。

    收集ICE candidates

    Kurento 6 以后全面支持Trickle ICE 协议,使得WebRtcEndpoint 可以异步得收集 ICE candidates。也正是因此,每一个WebRtcEndpoint 都需要一个监听器,可以在每次ICE 收集程序结束后接收到一个事件的触发
    webRtcEndpoint.addIceCandidateFoundListener(new EventListener<IceCandidateFoundEvent>() {
    @Override
    public void onEvent(IceCandidateFoundEvent 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();
    第二件事
    总体上来看这段代码一共做了两件事。
    1.webRtcEndpoint.addIceCandidateFoundListener(),给webRtcEndpoint添加了一个监听器。
    2.webRtcEndpoint.gatherCandidates(),开始收集ICE Candidates

    然后我们来分析第一个代码。并将它与图1的信息交互进行对照.
    很简单,在构造监听器的时候重写它的onEvent方法,接收参数为IceCandidateFoundEvent类型,这是 Kurento Java Client API 中的函数webRtcEndpoint.gatherCandidates();可以得到的结果类型,当监听器发现这个类型的参数出现,或者说一旦gatherCandidates()方法开始得到结果时,就会触发监听器。监听器内部所做的事情就是把candidate转成Json然后加上附加信息再转成String然后发出去。
    现在可以对着代码去图1里找一下对应的线了。同样,左边bar的信息交互是这里手动码出来的动作,右边bar的信息交互是由 Kurento Java Client API 自动实现的,不需要费心再码。

    (2)客户端

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

    首先看到,它用了 JavaScript 类来创建webSocket,并且把这个webSocket的处理路径放在了/helloworld,这就是前文提到的,客户端是怎样和 server 端实现 webSocket 的通信。
    well,再总结一遍,其实就是在客户端用 JavaScript 建一个webSocket,并把处理路径设置为/helloworld;然后再在主 java 程序中进行注册,把处理程序注册到处理路径也就是/helloworld上就OK了。

    var videoInput;
    var videoOutput;
    var webRtcPeer;
    var state = null;
    
    const I_CAN_START = 0;
    const I_CAN_STOP = 1;
    const I_AM_STARTING = 2;
    
    window.onload = function() {
        console = new Console();
        console.log('Page loaded ...');
        videoInput = document.getElementById('videoInput');
        videoOutput = document.getElementById('videoOutput');
        setState(I_CAN_START);
    }
    

    设置onload监听器,new 一个consloe,输出“Page loaded”,然后获取videoInputvideoOutput对象(这两个的id是在html里面定义的),然后setState到可以启动

    window.onbeforeunload = function() {
        ws.close();
    }
    

    设置onbeforeunload监听器,语意为关闭窗口前关掉 websocket

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

    设置onmessage监听器,有一个参数message,进来以后把它Jason化,然后switch。一共会有三种类型的message ,分别是startResponse, error,和 iceCandidate
    然后就是每种message之后的操作。
    (1)正常的操作是在I_CAN_START状态中的start按钮的onClick方法中做的,调用start()函数来执行操作进行信息交互,而start里的操作就是图1中JavaScriptClient端的各种信息交互操作了。
    注意⚠️,正常情况下这些函数基本都是在setState里面的设置按钮的onClick方法里调用的,稍微有点隐蔽。而setState本身也是一个神奇的函数,它是用来设置整个页面的状态的,状态不同每个按钮的状态也就不同,点了以后的操作也不一样。这是一个很神奇的操作。

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

    这里看start()方法做了什么,设置状态输出文字什么的就不说了,最主要是它用了kurento-utils.js里的WebRtcPeer.WebRtcPeerSendrecv()方法来创建一个 WebRtc 通信。
    这里可能会有一些疑问,这里不是只是JavaScriptClientApplicationServer之间的通信吗,换句话说这里不是应该只是JavaScriptClient发出的webSocket请求吗怎么又用了 Kurento 的一个 util.js 的库。
    其实是这样的,这里就是JavaScriptClientApplicationServer之间的通信,虽然调用的是 kurento 的库,但实现的是 WebRtc 的通信啊,WebRtc 本来就是基于浏览器的,如果你在JavaScriptClientApplicationServer之间不启动 WebRtcApplicationServerKMS之间的 Kurento 也就毫无意义了。所以这里虽然用了 Kurento 的一个库但还牵扯不到 Kurento,还只是浏览器原生的 WebRtc 通信的建立。
    (2)在点了start按钮并且start()了以后,就进入了处理 WebRtc 通信的阶段,其中startResponsecase中做的是开始JS客户端的SDP信息交互操作,iceCandidate同样,是调用那个kurento-utils.js里的 WebRtc 原生方法来实现ICE的信息交互操作。反正最重要的东西是图1,它确切反映了所有类的动作还有类与类之间的信息交互。

    
    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 onError(error) {
        console.error(error);
    }
    
    function onIceCandidate(candidate) {
        console.log('Local candidate' + JSON.stringify(candidate));
    
        var message = {
            id : 'onIceCandidate',
            candidate : candidate
        };
        sendMessage(message);
    }
    
    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 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);
    }
    
    function setState(nextState) {
        switch (nextState) {
        case I_CAN_START:
            $('#start').attr('disabled', false);
            $('#start').attr('onclick', 'start()');
            $('#stop').attr('disabled', true);
            $('#stop').removeAttr('onclick');
            break;
    
        case I_CAN_STOP:
            $('#start').attr('disabled', true);
            $('#stop').attr('disabled', false);
            $('#stop').attr('onclick', 'stop()');
            break;
    
        case I_AM_STARTING:
            $('#start').attr('disabled', true);
            $('#start').removeAttr('onclick');
            $('#stop').attr('disabled', true);
            $('#stop').removeAttr('onclick');
            break;
    
        default:
            onError('Unknown state ' + nextState);
            return;
        }
        state = nextState;
    }
    
    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";
        }
    }
    
    function hideSpinner() {
        for (var i = 0; i < arguments.length; i++) {
            arguments[i].src = '';
            arguments[i].poster = './img/webrtc.png';
            arguments[i].style.background = '';
        }
    }
    /**
     * Lightbox utility (to display media pipeline image in a modal dialog)
     */
    $(document).delegate('*[data-toggle="lightbox"]', 'click', function(event) {
        event.preventDefault();
        $(this).ekkoLightbox();
    });
    



    之后的官方文档还讲解了dependency的一些设置,但我没有看,应该也比较好理解。所以,就到这里吧,还有什么之后看到了再随时补充。















    相关文章

      网友评论

        本文标题:Kurento Tutorial 官方文档学习记录 Java -

        本文链接:https://www.haomeiwen.com/subject/eclipxtx.html