這個示例頁面應用程序使用WebRTC技術實現了一對多的視頻呼叫。換句話說,它是一個基於頁面的視頻廣播應用。
html
運行這個DEMO以前,須要先安裝 Kurento Media Server. 另外,還須要先安裝JDK (at least version 7), Maven, Git, 及Bower。
Nodejs及bower的安裝指令以下:
# 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
示例代碼須要先從項目的GitHub上下載並編譯運行:
# git clone https://github.com/Kurento/kurento-tutorial-java.git
# cd kurento-tutorial-java/kurento-one2many-call
# mvn clean compile exec:java
此時,應用程序已在8080端口上啓動,在兼容WebRTC的瀏覽器 (Chrome, Firefox)上輸入網址:
http://localhost:8080/
java
在這個應用程序中,有兩種類型的用戶:
一我的負責發送媒體,稱做Master,
N我的從Master上接收媒體,稱做Viewer。
所以,媒體管道由1+N 個 WebRtcEndpoints互聯組成,下圖顯示了Master的頁面截圖:
Figure 8.1: One to many video call screenshot
爲了實現上述的動做,須要先建立一個由1+N WebRtcEndpoints 組成的媒體管道。
Master端發送它的流給其它的Viewers。Viewer配置成只接收模式。
媒體管道的示例圖示以下:
Figure 8.2: One to many video call Media Pipeline
這是一個頁面應用程序,所以它使用的是客戶-服務端架構。
在客戶端,它的邏輯是由JavaScript實現的。
在服務端,它使用Kurento Java Client以到達Kurento Media Server。
總而言之,這個DEMO的高層架構是一個三層結構,爲了實現這些實體間的通訊,須要使用兩個WebSocket:
首先,一個WebSocket創建在客戶端與服務端之間,以實現一個定製化的信令協議。
其次,另外一個WebSocket用來實現Kurento Java Client和 Kurento Media Server間的通訊,這個通訊是由Kurento Protocol實現的。
客戶端與應用服務端的通訊使用的是基於WebSocket,使用JSON消息實現的信令協議。
客戶端與服務端的工做邏輯以下:
1. Master進入系統,在任什麼時候候,有且僅有一個Master。
所以,若是Master已存在,在另外一個用戶嘗試成爲Master時會報出差信息。
2. N個Viewer鏈接到master,若是系統中沒有master, 那麼Viewer將會收到相應的出錯信息。
3. Viewer能夠在任什麼時候候離開此次通訊。
4. 當Master結束此次會話時,那麼每一個鏈接的Viewer都會收到一個StopCommunication消息並結束此次會話;
下面的時序圖顯示了客戶端與服務端消息傳遞的細節。
如圖所示,客戶端與服務端爲了在瀏覽器和Kurento之間創建WebRTC鏈接,須要使用SDP數據交換。
另外,SDP協商鏈接了瀏覽器上的 WebRtcPeer 與服務器上的WebRtcEndpoint。完整的源碼見GibHub;
Figure 8.3: One to many video call signaling protocol
5.2.3 應用程序服務端邏輯
這個DEMO的服務端使用Java的Spring Boot框架實現,這個技術能夠被嵌入到Tomcat頁面服務器中以簡化開發過程。
Note:
你可使用任何你喜歡的Java服務端技術來建立基於kurento的頁面應用。
例如,純粹的Java EE應用,SIP Servlets, Play, Vertex等。咱們一般選擇Spring Boot框架。
下面的源碼中能夠看到服務端代碼的類視圖:
DEMO中的主類命名爲 One2ManyCallApp,
KurentoClient在這個類中的實例是做爲一個Spring Bean, 這個Bean用來建立 Kurento 媒體管道,
它能夠用來給應用程序添加媒體能力。
在這個實例中,咱們能夠看到WebSocket被用來鏈接Kurento Media Server,
默認地,在本機上,它監聽8888端口。
源碼見:
src/main/java/org/kurento/tutorial/one2manycall/One2ManyCallApp.java
@Configuration
@EnableWebSocket
@EnableAutoConfiguration
public class One2ManyCallApp implements WebSocketConfigurer {
@Bean
public CallHandler callHandler() {
return new CallHandler();
}
@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(One2ManyCallApp.class).run(args);
}
}
Figure 8.4: Server-side class diagram of the MagicMirror app
這個頁面應用程序使用了單頁面應用程序架構(SPA:Single Page Application architecture ),
並使用了WebSocket來做爲客戶端與服務端通訊的請求與響應。
特別地,主app類實現了WebSocketConfigurer接口來註冊一個WebSocketHandler來處理WebSocket請求。
CallHandler類實現了TextWebSocketHandler,用來處理文本WebSocket的請求。
這個類的主要實現的方法就是handleTextMessage, 這個方法實現了對請求的動做:
經過WebSocket返回對請求的響應。換句話說,它實現前面的時序圖中的信令協議的服務端部分。
在設計的協議中,有三種類型的輸入消息:master, viewer和stop。
這些消息對應的處理都在switch中;
源碼見:
src/main/java/org/kurento/tutorial/one2manycall/CallHandler.java
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, UserSession> viewers =
new ConcurrentHashMap<String, UserSession>();
@Autowired
private KurentoClient kurento;
private MediaPipeline pipeline;
private UserSession masterUserSession;
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);
log.debug("Incoming message from session '{}': {}", session.getId(), jsonMessage);
switch (jsonMessage.get("id").getAsString()) {
case "master":
try {
master(session, jsonMessage);
} catch (Throwable t) {
stop(session);
log.error(t.getMessage(), t);
JsonObject response = new JsonObject();
response.addProperty("id", "masterResponse");
response.addProperty("response", "rejected");
response.addProperty("message", t.getMessage());
session.sendMessage(new TextMessage(response.toString()));
}
break;
case "viewer":
try {
viewer(session, jsonMessage);
} catch (Throwable t) {
stop(session);
log.error(t.getMessage(), t);
JsonObject response = new JsonObject();
response.addProperty("id", "viewerResponse");
response.addProperty("response", "rejected");
response.addProperty("message", t.getMessage());
session.sendMessage(new TextMessage(response.toString()));
}
break;
case "stop":
stop(session);
break;
default:
break;
}
}
private synchronized void master(WebSocketSession session,
JsonObject jsonMessage) throws IOException {
...
}
private synchronized void viewer(WebSocketSession session,
JsonObject jsonMessage) throws IOException {
...
}
private synchronized void stop(WebSocketSession session) throws IOException {
...
}
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus status) throws Exception {
stop(session);
}
}
下面的代碼片段中,能夠看到master方法,它爲master建立了一個Media管道和WebRtcEndpoint:
private synchronized void master(WebSocketSession session,
JsonObject jsonMessage) throws IOException {
if (masterUserSession == null) {
masterUserSession = new UserSession(session);
pipeline = kurento.createMediaPipeline();
masterUserSession.setWebRtcEndpoint(new WebRtcEndpoint.Builder(pipeline).build());
WebRtcEndpoint masterWebRtc = masterUserSession.getWebRtcEndpoint();
String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
String sdpAnswer = masterWebRtc.processOffer(sdpOffer);
JsonObject response = new JsonObject();
response.addProperty("id", "masterResponse");
response.addProperty("response", "accepted");
response.addProperty("sdpAnswer", sdpAnswer);
masterUserSession.sendMessage(response);
} else {
JsonObject response = new JsonObject();
response.addProperty("id", "masterResponse");
response.addProperty("response", "rejected");
response.addProperty("message",
"Another user is currently acting as sender. Try again later ...");
session.sendMessage(new TextMessage(response.toString()));
}
}
The viewer method is similar, but not he Master WebRtcEndpoint is
connected to each of the viewers WebRtcEndpoints,otherwise an error is sent back to the client.
viewer方法也是相似的,但
private synchronized void viewer(WebSocketSession session,
JsonObject jsonMessage) throws IOException {
if (masterUserSession == null || masterUserSession.getWebRtcEndpoint() == null) {
JsonObject response = new JsonObject();
response.addProperty("id", "viewerResponse");
response.addProperty("response", "rejected");
response.addProperty("message",
"No active sender now. Become sender or . Try again later ...");
session.sendMessage(new TextMessage(response.toString()));
} else {
if(viewers.containsKey(session.getId())){
JsonObject response = new JsonObject();
response.addProperty("id", "viewerResponse");
response.addProperty("response", "rejected");
response.addProperty("message",
"You are already viewing in this session. " +
"Use a different browser to add additional viewers.");
session.sendMessage(new TextMessage(response.toString()));
return;
}
UserSession viewer = new UserSession(session);
viewers.put(session.getId(), viewer);
String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
WebRtcEndpoint nextWebRtc = new WebRtcEndpoint.Builder(pipeline).build();
viewer.setWebRtcEndpoint(nextWebRtc);
masterUserSession.getWebRtcEndpoint().connect(nextWebRtc);
String sdpAnswer = nextWebRtc.processOffer(sdpOffer);
JsonObject response = new JsonObject();
response.addProperty("id", "viewerResponse");
response.addProperty("response", "accepted");
response.addProperty("sdpAnswer", sdpAnswer);
viewer.sendMessage(response);
}
}
最後,stop消息結束通訊。若是這個消息是由master發送的,則stopCommunication消息將發送到每一個鏈接的觀看端:
private synchronized void stop(WebSocketSession session) throws IOException {
String sessionId = session.getId();
if (masterUserSession != null
&& masterUserSession.getSession().getId().equals(sessionId)) {
for (UserSession viewer : viewers.values()) {
JsonObject response = new JsonObject();
response.addProperty("id", "stopCommunication");
viewer.sendMessage(response);
}
log.info("Releasing media pipeline");
if (pipeline != null) {
pipeline.release();
}
pipeline = null;
masterUserSession = null;
} else if (viewers.containsKey(sessionId)) {
if (viewers.get(sessionId).getWebRtcEndpoint() != null) {
viewers.get(sessionId).getWebRtcEndpoint().release();
}
viewers.remove(sessionId);
}
}
node
如今來看應用程序的客戶端,爲了呼叫前面在服務端建立的WebSocket服務,咱們使用了JavaScript類WebSocket。
咱們使用了一個特殊的Kurento JavaScripty庫,叫作 kurento-utils.js, 來簡化和服務端的WebRTC交互。
這個庫依賴於 adapter.js, 它是一個JavaScript WebRTC utility,由Google管理,它抽象了瀏覽器之間的差別。
最後,jquery.js在這個應用中也一樣須要;
這些庫都連接到了index.html頁面,並在index.js中被使用。
在下面的代碼片段中,咱們能夠看到在路徑 /call下建立了WebSocket(變量 ws)。
而後,WebSocket的監聽者onmessage用於在客戶端實現JSON信令協議。
這裏有四種輸入消息給客戶端:
masterResponse, viewerResponse, 和 stopCommunication。
這些動做都是用來實現通訊中的每一個步驟。
例如,在master函數中,Kurento-utils.js的函數WebRtcPeer.startSendRecv是用來啓動WebRTC通訊。
而後,WebRtcPeer.startRecvOnly在viewer函數中被使用。
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 'masterResponse':
masterResponse(parsedMessage);
break;
case 'viewerResponse':
viewerResponse(parsedMessage);
break;
case 'stopCommunication':
dispose();
break;
default:
console.error('Unrecognized message', parsedMessage);
}
}
function master() {
if (!webRtcPeer) {
showSpinner(videoInput, videoOutput);
webRtcPeer = kurentoUtils.WebRtcPeer.startSendRecv(videoInput, videoOutput,
function(offerSdp) {
var message = {
id : 'master',
sdpOffer : offerSdp
};
sendMessage(message);
});
}
}
function viewer() {
if (!webRtcPeer) {
document.getElementById('videoSmall').style.display = 'none';
showSpinner(videoOutput);
webRtcPeer = kurentoUtils.WebRtcPeer.startRecvOnly(videoOutput, function(offerSdp) {
var message = {
id : 'viewer',
sdpOffer : offerSdp
};
sendMessage(message);
});
}
}
jquery
這個Java Spring 應用使用Maven實現。在pom.xml中聲明瞭Kurento依賴庫。
以下面的代碼片段所示,咱們須要兩個依賴庫:
Kurento Client Java 依賴庫(kurento-client)和
用於客戶端的JavaScript Kurento utility庫(kurento-utils)
<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.
Kurento框架使用了語義化版本號發佈。
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>git