Kurento Java Tutorial - Hello World
這個web應用程序是爲Java開發人員介紹用Kurento編程的原則而設計的。它包含一個WebRTC鏡像視頻通信(環回)。本教程假設您具備Java、JavaScript、HTML和WebRTC的基本知識。在開始本教程之前,我們還建議閱讀關於Kurento和WebRTC的部分。
註釋
此應用程序使用HTTPS。如果您在本地主機中運行它並在瀏覽器中接受安全異常,它將正常工作,但是如果遠程運行,您應該保護您的應用程序。有關更多信息,請選中“配置Java服務器以使用HTTPS”。
快速開始
跟着下面的步驟運行演示程序:
1、安裝Kurento Media Server: 安裝嚮導鏈接
2、運行這些命令:
git clone https://github.com/Kurento/kurento-tutorial-java.git
cd kurento-tutorial-java/kurento-hello-world
git checkout 6.13.0
mvn -U clean spring-boot:run -Dkms.url=ws://localhost:8888/kurento
3、使用WebRTC兼容瀏覽器(Chrome、Firefox)打開演示頁:https://localhost:8443/
4、單擊“開始”開始演示。
5、允許訪問您的網絡攝像頭。
6、一旦協商並建立環回連接,您就應該在本地和遠程佔位符中看到您的網絡攝像機視頻。
7、單擊“停止”完成演示。
理解這個例子
Kurento爲開發人員提供了一個Kurento Java客戶端來控制Kurento媒體服務器。這個客戶端庫可以用於任何類型的Java應用程序:服務器端Web、桌面、Android等。它與任何框架都兼容,如JavaEE、Spring、Play、Vert.x、Swing和JavaFX。
這個Hello World演示是使用Kurento可以創建的最簡單的web應用程序之一。下圖顯示了此演示運行的屏幕截圖:
Kurento Hello World Screenshot: WebRTC in loopback
應用程序的界面(一個HTML網頁)由兩個HTML5標記組成:一個顯示本地流(由設備網絡攝像頭捕獲),另一個顯示媒體服務器發送回客戶端的遠程流。
應用程序的邏輯非常簡單:本地流被髮送到Kurento媒體服務器,該服務器將其發送回客戶端,而無需修改。要實現此行爲,我們需要創建一個由單個媒體元素(即WebRtcEndpoint)組成的媒體管道,該管道具有交換全雙工(雙向)WebRTC媒體流的能力。此媒體元素連接到自身,以便將它從瀏覽器接收到的媒體發送回瀏覽器。此媒體管道如下圖所示:
這是一個web應用程序,因此它遵循客戶機-服務器體系結構。在客戶端,邏輯是用JavaScript實現的。在服務器端,我們使用基於Spring引導的應用服務器,使用Kurento Java客戶機API來控制Kurento媒體服務器的功能。總之,這個演示的頂層架構是三層的。爲了與這些實體通信,使用了兩個websocket:
1.在客戶端和應用服務器之間創建一個WebSocket來實現一個自定義的信令協議。
2、另一個WebSocket用於執行Kurento Java客戶端和Kurento媒體服務器之間的通信。
此通信使用Kurento協議進行。有關詳細說明,請閱讀本節:Kurento協議。
下圖顯示了與應用程序接口交互的完整序列圖:
i)JavaScript邏輯;
ii)應用服務器邏輯(使用Kurento Java客戶端);
iii)Kurento媒體服務器。
Kurento Hello World(loopbak中的WebRTC)演示的完整序列圖
應用服務邏輯
這個演示是在服務器端使用Java開發的,基於Spring Boot框架,它在生成的maven工件中嵌入了Tomcat web服務器,從而簡化了開發和部署過程。
註釋
您可以使用您喜歡的任何Java服務器端技術來使用Kurento構建web應用程序。例如,一個純Java EE應用程序、SIP Servlets、Play、Vert.x等等。爲了方便起見,我們選擇了Spring Boot。
在下圖中,您可以看到服務器端代碼的類圖:
HelloWorld應用程序的服務器端類圖
這個演示的main class是HelloWorldApp。
如您所見,KurentoClient在這個類中被實例化爲一個Spring Bean。此bean用於創建Kurento媒體管道,用於嚮應用程序添加媒體功能。在這個實例中,我們看到需要向客戶機庫指定Kurento媒體服務器的位置。在這個例子中,我們假設它位於localhost,監聽端口8888。如果複製此示例,則需要在其中插入Kurento媒體服務器實例的特定位置。
一旦Kurento客戶端被實例化,就可以與Kurento媒體服務器通信並控制其多媒體功能。
@SpringBootApplication
@EnableWebSocket
public class HelloWorldApp implements WebSocketConfigurer {
@Bean
public HelloWorldHandler handler() {
return new HelloWorldHandler();
}
@Bean
public KurentoClient kurentoClient() {
return KurentoClient.create();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler(), "/helloworld");
}
public static void main(String[] args) throws Exception {
SpringApplication.run(HelloWorldApp.class, args);
}
}
此web應用程序遵循單頁應用程序體系結構(SPA),並使用WebSocket通過請求和響應與應用服務器通信。具體來說,main app類實現接口WebSocketConfigurer以註冊一個WebSocketHandler,該WebSocketHandler處理 /helloworld 的WebSocket請求。
類HelloWorldHandler實現TextWebSocketHandler來處理文本WebSocket請求。這個類的中心部分是handleTextMessage方法。此方法實現請求的操作,通過WebSocket返回響應。換句話說,它實現了前面序列圖中描述的信令協議的服務器部分。
public void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
[...]
switch (messageId) {
case "start":
start(session, jsonMessage);
break;
case "stop": {
stop(session);
break;
}
case "onIceCandidate":
onRemoteIceCandidate(session, jsonMessage);
break;
default:
sendError(session, "Invalid message, ID: " + messageId);
break;
}
[...]
}
The start() method performs the following actions:
- 配置媒體處理邏輯。這是應用程序配置Kurento如何處理媒體的部分。換句話說,媒體管道就是在這裏創建的。爲此,對象KurentoClient用於創建mediapipline對象。使用它,我們需要的媒體元素被創建和連接。在本例中,我們只實例化一個WebRtcEndpoint,用於接收WebRTC流並將其發送回客戶端。
final MediaPipeline pipeline = kurento.createMediaPipeline();
final WebRtcEndpoint webRtcEp =
new WebRtcEndpoint.Builder(pipeline).build();
webRtcEp.connect(webRtcEp);
- 創建事件偵聽器。由Kurento管理的所有對象都能夠發出幾種類型的事件,如端點事件中所述。應用服務器可以監聽它們,以便對媒體服務器的處理邏輯內部的情況有更多的瞭解。監聽所有可能的事件是一個很好的實踐,因此客戶機應用程序擁有儘可能多的信息。
// Common events for all objects that inherit from BaseRtpEndpoint
addErrorListener(
new EventListener<ErrorEvent>() { ... });
addMediaFlowInStateChangeListener(
new EventListener<MediaFlowInStateChangeEvent>() { ... });
addMediaFlowOutStateChangeListener(
new EventListener<MediaFlowOutStateChangeEvent>() { ... });
addConnectionStateChangedListener(
new EventListener<ConnectionStateChangedEvent>() { ... });
addMediaStateChangedListener(
new EventListener<MediaStateChangedEvent>() { ... });
addMediaTranscodingStateChangeListener(
new EventListener<MediaTranscodingStateChangeEvent>() { ... });
// Events specific to objects of class WebRtcEndpoint
addIceCandidateFoundListener(
new EventListener<IceCandidateFoundEvent>() { ... });
addIceComponentStateChangeListener(
new EventListener<IceComponentStateChangeEvent>() { ... });
addIceGatheringDoneListener(
new EventListener<IceGatheringDoneEvent>() { ... });
addNewCandidatePairSelectedListener(
new EventListener<NewCandidatePairSelectedEvent>() { ... });
- WebRTC SDP協商。在WebRTC中,SDP提供/應答模型用於協商將在對等方之間交換的音頻或視頻軌跡,以及它們支持的公共特性的子集。此協商通過在一個對等方中生成SDP提議,將其發送給另一個對等方,並返回將生成的SDP響應來完成。
在這種特殊情況下,SDP提議由瀏覽器生成併發送給Kurento,Kurento隨後生成SDP應答,該應答必須作爲響應發送回瀏覽器。
// 'webrtcSdpOffer' is the SDP Offer generated by the browser;
// send the SDP Offer to KMS, and get back its SDP Answer
String webrtcSdpAnswer = webRtcEp.processOffer(webrtcSdpOffer);
sendMessage(session, webrtcSdpAnswer);
- Gather ICE candidates。當SDP提供/應答協商正在進行時,每個對等方都可以開始收集將用於ICE協議的連接候選。此過程的工作方式與瀏覽器通過發出事件RTCPeerConnection.onicecandidate通知其客戶機代碼的方式非常相似;同樣,Kurento的WebRtcEndpoint將通過事件IceCandidateFound通知其客戶機應用程序。
webRtcEp.gatherCandidates();
Client-Side Logic
現在讓我們轉到應用程序的客戶端。要在服務器端調用先前創建的WebSocket服務,我們使用JavaScript類WebSocket。我們使用一個名爲Kurento-utils.js的特定Kurento JavaScript庫來簡化WebRTC與服務器的交互。這個庫依賴於adapter.js,這是一個由Google維護的JavaScript WebRTC實用程序,它可以消除瀏覽器之間的差異。
這些庫作爲Maven依賴項引入到項目中,Maven依賴項從WebJars.org下載所有必需的文件;它們加載在index.html頁面中,並在index.js文件中使用。
在下面的代碼片段中,我們可以看到在path/helloworld中創建的WebSocket。然後,使用WebSocket的onmessage監聽器在客戶端實現JSON信令協議。請注意,有三條傳入客戶端的消息:startResponse、error和iceCandidate。爲實現通信中的每一步都採取了方便的措施。例如,在函數start中,kurento-utils.js的函數webrtcpeeer.WebRtcPeerSendrecv用於啓動WebRTC通信。
var ws = new WebSocket('ws://' + location.host + '/helloworld');
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);
}
}
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 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 sendMessage(message) {
var jsonMessage = JSON.stringify(message);
console.log('Sending message: ' + jsonMessage);
ws.send(jsonMessage);
}
依賴項
這個Java Spring應用程序是使用Maven實現的。pom.xml的相關部分是聲明Kurento依賴項的地方。如下面的代碼片段所示,我們需要兩個依賴項:客戶端的Kurento客戶機Java依賴項(Kurento客戶機)和JavaScript Kurento實用程序庫(Kurento實用程序庫)。其他客戶端庫由WebJars管理。