Kurento Tutorial 官方文檔學(xué)習(xí)記錄 Java - Hello world

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

理解例子的源碼

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

1.應(yīng)用的邏輯

很簡單,瀏覽器將攝像頭和麥克風(fēng)獲得的本地流傳輸給遠程的KMS(或本級部署的)然后再不經(jīng)調(diào)制的傳輸回本地瀏覽器進行顯示。
要完成這個動作,我們需要創(chuàng)造一個只包含一個Media ElementMedia Pipeline。本例中那個唯一的 Media Element 就是一個 WebRtcEndpoint,它具有全雙工交換WebRTC媒體流的能力。它自己與自己相連,以保證送出的流通過一圈再傳送回來,也就是我們要實現(xiàn)的鏡像功能(loopback)。
介紹如下


這塊其實很好理解,不用太過注意這個圖。

2.架構(gòu)

由于是瀏覽器應(yīng)用,所以也服從CS架構(gòu)。
在客戶端,使用 JavaScript 來實現(xiàn)客戶端邏輯。在服務(wù)器端,用基于 Spring-BootJava 實現(xiàn),調(diào)用 Kurento Java Client API 來控制 Kurento Media Server。
總之,這個應(yīng)用是一個三層結(jié)構(gòu)的應(yīng)用,一共有三層實體分別為

客戶端 —— 客戶端服務(wù)器&Kurento Java Client —— KMS

注意這里的客戶端服務(wù)器就是KJC,只是看的角度不一樣。它處理來自客戶端的請求,同時控制KMS,即,既是客戶端的服務(wù)器,又是KMS的客戶端。
由于是三層結(jié)構(gòu),就需要兩個WebSocket了,一個用于前兩個之間的連接,遵循Custom Signaling Protocol,另一個用于后兩者的連接,遵循Kurento Protocol。
想進一步了解可以看關(guān)于這塊的詳細信息

接下來用一個SD來體現(xiàn)這三者之間的關(guān)系。該圖中包含的詳細信息十分重要,每一個信息的交互都會在后邊的代碼中體現(xiàn)。


圖1

3.代碼分析

(1)服務(wù)器端

服務(wù)器端就是SD圖中間的綠色實體,也就是三層架構(gòu)中位于中間層的Application Server
再次標(biāo)注一下,服務(wù)器端用了基于Spring-BootJava來實現(xiàn)。

  • 首先來看一下服務(wù)器端代碼的整體架構(gòu)


    圖2

    圖中列出的幾個類就是我們的Java的代碼中的所有類了。一會再看代碼的時候我們會一步一步的發(fā)現(xiàn)這個圖的指向具體意思。這個圖也是一個對于理解代碼非常重要的圖。

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

我們可以看到,在HelloWorldApp類中,HelloWorldHandlerKurentoClient都被實例化為了 Bean. Bean是Spring-Boot框架中一個重要的結(jié)構(gòu)名稱,具體可以去了解Spring-Boot框架中的這部分內(nèi)容,這里你可以就把它當(dāng)成,是 HelloWorldApp 是一個大容器,里面裝了兩個小容器分別是 HelloWorldHandlerKurentoClient 。這就是為什么圖2HelloWorldApp 實體有兩條尖頭分別指向 HelloWorldHandlerKurentoClient 實體

這里的KurentoClient在創(chuàng)建時默認了KMS服務(wù)器在本機上。如果你的KMS服務(wù)器部署在遠程的話,就需要向如下代碼一樣創(chuàng)建KurentoClient

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

可以看到 HelloWorldApp 類在創(chuàng)建時 implements 了 WebSocketConfigurer

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

通過 Override WebSocketConfigurerregisterWebSocketHandlers(WebSocketHandlerRegistry registry)方法,將 HelloWorldHandler 類的實例handler作為WebSocketHandler來處理 WebSocket 請求,且處理路徑為“/helloworld”

  • WebSocket處理類 HelloWorldHandler
    可以看到 HelloWorldHandler 類在創(chuàng)建時 implements 了TextWebSocketHandler
    該類的核心部分是handleTextMessage方法,這個方法通過手動編寫邏輯實現(xiàn)了上文提到的兩個 WebSocket 中遵循 Custom Signaling Protocol 的那個 WebSocket 的處理程序,也就是作為Client Server的那部分功能,體現(xiàn)在圖1JavaScriptClientApplicationServer 之間的信息交互。而通過直接調(diào)用 Kurento Java Client API 來完成第二個 websocket 的通信,是自動的,不用手動碼邏輯。
    注意,這里的代碼每一處都可以在圖1中的信息交互中找到對應(yīng)的表示。
    根據(jù)JsonMessage的不同分了三個不同的case,其中最重要的是start,里面調(diào)用了start函數(shù),見下文。
    start方法主要完成了如下四個工作,注釋也都寫出來了。

配置媒體處理邏輯

MediaPipeline pipeline = kurento.createMediaPipeline();
WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
webRtcEndpoint.connect(webRtcEndpoint);
1.創(chuàng)建MediaPipeline
2.用pipeline創(chuàng)建WebRtcEndpoint
3.創(chuàng)建好的webRtcEndpoint自己和自己相連

  • 這幾步在前文都清楚得提到過

存儲用戶會話

UserSession user = new UserSession();
user.setMediaPipeline(pipeline);
user.setWebRtcEndpoint(webRtcEndpoint);
users.put(session.getId(), user);
由于在最后會調(diào)用stop來釋放,所以需要存儲用戶會話來保證這個功能,也就是要存儲MediaPipelineWebRtcEndpoint。

SDP通訊

String sdpOffer = >>jsonMessage.get("sdpOffer").getAsString();
jsonMessage中獲得sdpOffer部分并轉(zhuǎn)換成String。
String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);
sdpOffer作為參數(shù)傳給webRtcEndpoint形成sdpAnswer,調(diào)用了webRtcEndpointprocessOffer方法,這里對應(yīng)圖1中的左邊前兩條數(shù)據(jù)交換箭頭,其中processOffer方法完成了兩條線中間右邊Application ServerKMS之間的一系列數(shù)據(jù)交換。
JsonObject response = new JsonObject();
response.addProperty("id", "startResponse");
response.addProperty("sdpAnswer", sdpAnswer);
synchronized (session) { session.sendMessage(new TextMessage(response.toString())); }
這里是把sdpAnswer加入一個json里再加上一些必要信息然后再轉(zhuǎn)換成String并在會話里送出。

  • WebRtcpeer 之間交換媒體數(shù)據(jù)遵循SDP (Session Description protocol),也就是說用的是SDPOfferSDPAnswer機制。
  • 這一步完成了圖1中上半部份的SDP通信的功能,包括處理來自JavaScript的客戶端請求和控制遠端KMS兩部分動作。
  • 注意??,JavaScriptClientwebsocket請求用這里的HelloWorldHandler來處理是由于在Index.js里制定了websocket的處理路徑是/helloworld,并且在主程序HelloWorldApp里將handler注冊到了/helloworld路徑里。

收集ICE candidates

Kurento 6 以后全面支持Trickle ICE 協(xié)議,使得WebRtcEndpoint 可以異步得收集 ICE candidates。也正是因此,每一個WebRtcEndpoint 都需要一個監(jiān)聽器,可以在每次ICE 收集程序結(jié)束后接收到一個事件的觸發(fā)
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添加了一個監(jiān)聽器。
2.webRtcEndpoint.gatherCandidates(),開始收集ICE Candidates。

然后我們來分析第一個代碼。并將它與圖1的信息交互進行對照.
很簡單,在構(gòu)造監(jiān)聽器的時候重寫它的onEvent方法,接收參數(shù)為IceCandidateFoundEvent類型,這是 Kurento Java Client API 中的函數(shù)webRtcEndpoint.gatherCandidates();可以得到的結(jié)果類型,當(dāng)監(jiān)聽器發(fā)現(xiàn)這個類型的參數(shù)出現(xiàn),或者說一旦gatherCandidates()方法開始得到結(jié)果時,就會觸發(fā)監(jiān)聽器。監(jiān)聽器內(nèi)部所做的事情就是把candidate轉(zhuǎn)成Json然后加上附加信息再轉(zhuǎn)成String然后發(fā)出去。
現(xiàn)在可以對著代碼去圖1里找一下對應(yīng)的線了。同樣,左邊bar的信息交互是這里手動碼出來的動作,右邊bar的信息交互是由 Kurento Java Client API 自動實現(xiàn)的,不需要費心再碼。

(2)客戶端

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

首先看到,它用了 JavaScript 類來創(chuàng)建webSocket,并且把這個webSocket的處理路徑放在了/helloworld,這就是前文提到的,客戶端是怎樣和 server 端實現(xiàn) webSocket 的通信。
well,再總結(jié)一遍,其實就是在客戶端用 JavaScript 建一個webSocket,并把處理路徑設(shè)置為/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);
}

設(shè)置onload監(jiān)聽器,new 一個consloe,輸出“Page loaded”,然后獲取videoInputvideoOutput對象(這兩個的id是在html里面定義的),然后setState到可以啟動

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

設(shè)置onbeforeunload監(jiān)聽器,語意為關(guān)閉窗口前關(guān)掉 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);
    }
}

設(shè)置onmessage監(jiān)聽器,有一個參數(shù)message,進來以后把它Jason化,然后switch。一共會有三種類型的message ,分別是startResponse, error,和 iceCandidate。
然后就是每種message之后的操作。
(1)正常的操作是在I_CAN_START狀態(tài)中的start按鈕的onClick方法中做的,調(diào)用start()函數(shù)來執(zhí)行操作進行信息交互,而start里的操作就是圖1中JavaScriptClient端的各種信息交互操作了。
注意??,正常情況下這些函數(shù)基本都是在setState里面的設(shè)置按鈕的onClick方法里調(diào)用的,稍微有點隱蔽。而setState本身也是一個神奇的函數(shù),它是用來設(shè)置整個頁面的狀態(tài)的,狀態(tài)不同每個按鈕的狀態(tài)也就不同,點了以后的操作也不一樣。這是一個很神奇的操作。

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()方法做了什么,設(shè)置狀態(tài)輸出文字什么的就不說了,最主要是它用了kurento-utils.js里的WebRtcPeer.WebRtcPeerSendrecv()方法來創(chuàng)建一個 WebRtc 通信。
這里可能會有一些疑問,這里不是只是JavaScriptClientApplicationServer之間的通信嗎,換句話說這里不是應(yīng)該只是JavaScriptClient發(fā)出的webSocket請求嗎怎么又用了 Kurento 的一個 util.js 的庫。
其實是這樣的,這里就是JavaScriptClientApplicationServer之間的通信,雖然調(diào)用的是 kurento 的庫,但實現(xiàn)的是 WebRtc 的通信啊,WebRtc 本來就是基于瀏覽器的,如果你在JavaScriptClientApplicationServer之間不啟動 WebRtc ,ApplicationServerKMS之間的 Kurento 也就毫無意義了。所以這里雖然用了 Kurento 的一個庫但還牽扯不到 Kurento,還只是瀏覽器原生的 WebRtc 通信的建立。
(2)在點了start按鈕并且start()了以后,就進入了處理 WebRtc 通信的階段,其中startResponsecase中做的是開始JS客戶端的SDP信息交互操作,iceCandidate同樣,是調(diào)用那個kurento-utils.js里的 WebRtc 原生方法來實現(xiàn)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的一些設(shè)置,但我沒有看,應(yīng)該也比較好理解。所以,就到這里吧,還有什么之后看到了再隨時補充。

















最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容