这个示例页面应用程序使用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