Kurento應用開發指南(以Kurento 5.0爲模板) 之四:示例教程 一對一視頻呼叫

5.3 教程四-一對一的視頻呼叫

這個頁面應用程序使用WebRTC技術實現了一個一對一的呼叫,換言話說,這個應用提供了一個簡單的視頻電話html

5.3.1 運行示例程序

運行這個DEMO以前,你須要先安裝Kurento Media Server.能夠看前面的介紹。
另外,你還須要先安裝好 JDK (at least version 7), Maven, Git, 和 Bower。
在Ubuntu上安裝這些的命令以下:
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
啓動應用程序以前,須要先下載源,並編譯運行,命令以下:
git clone https://github.com/Kurento/kurento-tutorial-java.git
cd kurento-tutorial-java/kurento-one2one-call
mvn clean compile exec:java
默認地,這個應用程序部署在8080端口上,可使用兼容WebRTC的瀏覽器打開URL http://localhost:8080

 java

5.3.2 Understanding this example

下面的圖片顯示了在瀏覽上運行這個DEMO時截圖。
這個應用程序(一個HTML頁面)的接口是由兩個HTML5視頻標籤組成的:
  一個用來顯示本地流;
  另外一個用來顯示遠端的流;
若是有兩用戶,A和B都使用這個應用程序,則媒體流的工做方式以下:
A的攝像頭的流發送到Kurento Media Server,Kurento Media Server會將這個流發送給B;
一樣地,B也會將流發送到Kurento Media Server,它再發給A。
這意味着,KMS提供了一個B2B (back-to-back) 的呼叫服務。
 
Figure 9.1: One to one video call screenshot


爲了實現上述的工做方式,須要建立一個由兩個WebRtc端點以B2B方式鏈接的媒體管道,媒體管道的示例圖以下:
 
Figure 9.2: One to one video call Media Pipeline


客戶端和服務端的通訊是經過基於WebSocket上的JSON消息的信令協議實現的,客戶端和服務端的工做時序以下:
1. 用戶A在服務器上註冊他的名字
2. 用戶B在服務器註冊他的名字
3. 用戶A呼叫用戶B
4. 用戶B接受呼叫
5. 通訊已創建,媒體在用戶A與用戶B之間流動
6. 其中一個用戶結束此次通訊
時序流程的細節以下圖所示:


 
Figure 9.3: One to many one call signaling protocol
如圖中所示,爲了在瀏覽器和Kurento之間創建WebRTC鏈接,須要在客戶端和服務端之間進行SDP交互。
特別是,SDP協商鏈接了瀏覽器的WebRtcPeer和服務端的WebRtcEndpoint。 
下面的章節描述了服務端和客戶端的細節,以及DEMO是如何運行的。源碼能夠從GitHub上下載;

 node

5.3.3 應用程序服務端邏輯

這個DEMO的服務端是使用Java的Spring Boot框架開發的。這個技術能夠嵌入到Tomcat頁面服務器中,從而簡化開發流程。
Note: You can use whatever Java server side technology you prefer to build 
web applications with Kurento. For example, a pure Java EE application, SIP Servlets, 
Play, Vertex, etc. We have choose Spring Boot for convenience.


下面的圖顯示了服務端的類圖。
這個DEMO的主類爲One2OneCallApp, 如代碼中所見,KurentoClient做爲Spring Bean在類中進行了實例化。






 


Figure 9.4: Server-side class diagram of the one to one video call app


@Configuration
@EnableWebSocket
@EnableAutoConfiguration
public class One2OneCallApp implements WebSocketConfigurer {
    @Bean
    public CallHandler callHandler() {
        return new CallHandler();
    }


    @Bean
    public UserRegistry registry() {
        return new UserRegistry();
    }


    @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(One2OneCallApp.class).run(args);
    }
}
這個頁面應用程序使用了單頁面應用程序架構(SPA:Single Page Application architecture ),
並使用了WebSocket來做爲客戶端與服務端通訊的請求與響應。
特別地,主app類實現了WebSocketConfigurer接口來註冊一個WebSocketHandler來處理WebSocket請求。


CallHandler類實現了TextWebSocketHandler,用來處理文本WebSocket的請求。
這個類的主要實現的方法就是handleTextMessage, 這個方法實現了對請求的動做: 
經過WebSocket返回對請求的響應。換句話說,它實現前面的時序圖中的信令協議的服務端部分。


在設計的協議中,有三種類型的輸入消息: 註冊,呼叫, incomingCallResponse和stop。
這些消息對應的處理都在switch中。
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, CallMediaPipeline> pipelines =
                new ConcurrentHashMap<String, CallMediaPipeline>();


    @Autowired
    private KurentoClient kurento;


    @Autowired
    private UserRegistry registry;


    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
    throws Exception {
        JsonObject jsonMessage = gson.fromJson(message.getPayload(),
        JsonObject.class);
        UserSession user = registry.getBySession(session);
        if (user != null) {
            log.debug("Incoming message from user '{}': {}", user.getName(),jsonMessage);
        } else {
            log.debug("Incoming message from new user: {}", jsonMessage);
    }
    switch (jsonMessage.get("id").getAsString()) {
    case "register":
        try {
            register(session, jsonMessage);
        } catch (Throwable t) {
            log.error(t.getMessage(), t);
            JsonObject response = new JsonObject();
            response.addProperty("id", "resgisterResponse");
            response.addProperty("response", "rejected");
            response.addProperty("message", t.getMessage());
            session.sendMessage(new TextMessage(response.toString()));
        }
    break;
    case "call":
        try {
            call(user, jsonMessage);
        } catch (Throwable t) {
            log.error(t.getMessage(), t);
            JsonObject response = new JsonObject();
            response.addProperty("id", "callResponse");
            response.addProperty("response", "rejected");
            response.addProperty("message", t.getMessage());
            session.sendMessage(new TextMessage(response.toString()));
        }
        break;
        case "incomingCallResponse":
            incomingCallResponse(user, jsonMessage);
        break;
        case "stop":
            stop(session);
        break;
        default:
        break;
    }
}
private void register(WebSocketSession session, JsonObject jsonMessage)
    throws IOException {
        ...
}
private void call(UserSession caller, JsonObject jsonMessage)
throws IOException {
    ...
}
private void incomingCallResponse(UserSession callee, JsonObject jsonMessage)
throws IOException {
    ...
}
public void stop(WebSocketSession session) throws IOException {
...
}
@Override
public void afterConnectionClosed(WebSocketSession session,
            CloseStatus status) throws Exception {
    registry.removeBySession(session);
    }
}
在下面的代碼片段中,咱們能夠看到註冊方法,基本上,它包含了從註冊信息中獲得的名字屬性,並檢測它是否被註冊過。
若是沒有,則新用戶被註冊且有一個接受的消息發送給它;


private void register(WebSocketSession session, JsonObject jsonMessage)
throws IOException {
    String name = jsonMessage.getAsJsonPrimitive("name").getAsString();


    UserSession caller = new UserSession(session, name);
    String responseMsg = "accepted";
    if (name.isEmpty()) {
        responseMsg = "rejected: empty user name";
    } else if (registry.exists(name)) {
        responseMsg = "rejected: user '" + name + "' already registered";
    } else {
        registry.register(caller);
    }
    JsonObject response = new JsonObject();
    response.addProperty("id", "resgisterResponse");
    response.addProperty("response", responseMsg);
    caller.sendMessage(response);
}


在call方法中,服務端會檢查在消息屬性欄中的名字是否已註冊,而後發送一個incomingCall消息給它。
或者,若是這個名字未註冊,則會有一個callResponse消息發送給呼叫者以拒絕此次呼叫。


private void call(UserSession caller, JsonObject jsonMessage)
throws IOException {
    String to = jsonMessage.get("to").getAsString();
    String from = jsonMessage.get("from").getAsString();
    JsonObject response = new JsonObject();
    if (registry.exists(to)) {
        UserSession callee = registry.getByName(to);
        caller.setSdpOffer(jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString());
        caller.setCallingTo(to);
        response.addProperty("id", "incomingCall");
        response.addProperty("from", from);
        callee.sendMessage(response);
        callee.setCallingFrom(from);
    } else {
        response.addProperty("id", "callResponse");
        response.addProperty("response", "rejected: user '" + to+ "' is not registered");
        caller.sendMessage(response);
    }
}


stop方法結束此次呼叫。這個過程會被呼叫者和被叫者在通訊中被調用。
結果是這兩端會釋放媒體管道並結束通訊:
public void stop(WebSocketSession session) throws IOException {
    String sessionId = session.getId();
    if (pipelines.containsKey(sessionId)) {
        pipelines.get(sessionId).release();
        CallMediaPipeline pipeline = pipelines.remove(sessionId);
        pipeline.release();
        // Both users can stop the communication. A 'stopCommunication'
        // message will be sent to the other peer.
        UserSession stopperUser = registry.getBySession(session);
        UserSession stoppedUser = (stopperUser.getCallingFrom() != null) ? registry
            .getByName(stopperUser.getCallingFrom()) : registry
            .getByName(stopperUser.getCallingTo());
        JsonObject message = new JsonObject();
        message.addProperty("id", "stopCommunication");
        stoppedUser.sendMessage(message);
    }
}


在 incomingCallResponse方法中,若是被叫用戶接受了這個呼叫,那麼就會以B2B方式建立媒體元素並鏈接呼叫者與被叫者。
一般,服務端會建立一個 CallMediaPipeline對象,用來封裝媒體管道的建立和管理。
而後,這個對象就用來在用戶瀏覽器間進行媒體交互協商。


瀏覽器上WebRTC端點與Kurento Media Server的WebRtcEndpoint間的協商
是經過客戶端生成的SDP(提交)與服務端生成的SDP(回答)實現的。
這個SDP的回答是由類CallMediaPipeline中Kurento Java Client生成的。
用於生成SDP的方法爲generateSdpAnswerForCallee(calleeSdpOffer) 和 generateSdpAnswerForCaller(callerSdpOffer):


private void incomingCallResponse(UserSession callee, JsonObject jsonMessage)
throws IOException {
    String callResponse = jsonMessage.get("callResponse").getAsString();
    String from = jsonMessage.get("from").getAsString();
    UserSession calleer = registry.getByName(from);
    String to = calleer.getCallingTo();


    if ("accept".equals(callResponse)) {
        log.debug("Accepted call from '{}' to '{}'", from, to);
        CallMediaPipeline pipeline = null;
        try {
            pipeline = new CallMediaPipeline(kurento);
            pipelines.put(calleer.getSessionId(), pipeline);
            pipelines.put(callee.getSessionId(), pipeline);
            String calleeSdpOffer = jsonMessage.get("sdpOffer").getAsString();
            String calleeSdpAnswer = pipeline.generateSdpAnswerForCallee(calleeSdpOffer);
            String callerSdpOffer = registry.getByName(from).getSdpOffer();
            String callerSdpAnswer = pipeline.generateSdpAnswerForCaller(callerSdpOffer);
            JsonObject startCommunication = new JsonObject();
            startCommunication.addProperty("id", "startCommunication");
            startCommunication.addProperty("sdpAnswer", calleeSdpAnswer);
            callee.sendMessage(startCommunication);
            JsonObject response = new JsonObject();
            response.addProperty("id", "callResponse");
            response.addProperty("response", "accepted");
            response.addProperty("sdpAnswer", callerSdpAnswer);
            calleer.sendMessage(response);
        } catch (Throwable t) {
            log.error(t.getMessage(), t);
            if (pipeline != null) {
                    pipeline.release();
            }
            pipelines.remove(calleer.getSessionId());
            pipelines.remove(callee.getSessionId());
            JsonObject response = new JsonObject();
            response.addProperty("id", "callResponse");
            response.addProperty("response", "rejected");
            calleer.sendMessage(response);
            response = new JsonObject();
            response.addProperty("id", "stopCommunication");
            callee.sendMessage(response);
        }
    } else {
        JsonObject response = new JsonObject();
        response.addProperty("id", "callResponse");
        response.addProperty("response", "rejected");
        calleer.sendMessage(response);
    }
}


這個DEMO的媒體邏輯是在類CallMediaPipeline中實現的,如上圖所見,媒體管道的組成很簡單:
由兩個WebRtcEndpoint直接相連組成。須要注意的WebRtcEndpoints須要作兩次鏈接,每次鏈接一個方向的。
public class CallMediaPipeline {
    private MediaPipeline pipeline;
    private WebRtcEndpoint callerWebRtcEP;
    private WebRtcEndpoint calleeWebRtcEP;
    public CallMediaPipeline(KurentoClient kurento) {
        try {
            this.pipeline = kurento.createMediaPipeline();
            this.callerWebRtcEP = new WebRtcEndpoint.Builder(pipeline).build();
            this.calleeWebRtcEP = new WebRtcEndpoint.Builder(pipeline).build();
            this.callerWebRtcEP.connect(this.calleeWebRtcEP);
            this.calleeWebRtcEP.connect(this.callerWebRtcEP);
        } catch (Throwable t) {
            if(this.pipeline != null){
                pipeline.release();
            }
        }
    }
    public String generateSdpAnswerForCaller(String sdpOffer) {
        return callerWebRtcEP.processOffer(sdpOffer);
    }


    public String generateSdpAnswerForCallee(String sdpOffer) {
        return calleeWebRtcEP.processOffer(sdpOffer);
    }
    public void release() {
        if (pipeline != null) {
            pipeline.release();
        }
    }
}


在這個類中,咱們能夠看到方法generateSdpAnswerForCaller 和 generateSdpAnswerForCallee的實現,
這些方法引導WebRtc端點建立合適的回答。

 jquery

5.3.4 客戶端

如今來看應用程序客戶端的代碼。爲了調用前面提到的服務端的WebSocket服務,咱們使用了JavaScript類WebSocket。
咱們使用了特殊的Kurento JavaScript庫,叫作kurento-utils.js來簡化WebRTC的交互,
這個庫依賴於adapter.js,它是一個JavaScript WebRTC設備,由Google維護,用來抽象瀏覽器之間的差別。
最後,這個應用程序還須要jquery.js.


這些庫都連接到了index.html頁面中,並都在index.js中被使用。
在下面的代碼片段中,咱們能夠看到在path /call下WebSocket(變量ws)的建立,
而後,WebSocket的監聽者onmessage被用來實如今客戶端的JSON信令協議。
.
注意,在客戶端有四個輸入信息:resgisterResponse, callResponse,incomingCall, 和startCommunication,
用來實現通訊中的各個步驟。
例如,在函數 call and incomingCall (for caller and callee respectively)中,
kurento-utils.js的函數WebRtcPeer.startSendRecv用來啓動WebRTC通訊。


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 'resgisterResponse':
        resgisterResponse(parsedMessage);
    break;
    case 'callResponse':
        callResponse(parsedMessage);
    break;
    case 'incomingCall':
        incomingCall(parsedMessage);
    break;
    case 'startCommunication':
        startCommunication(parsedMessage);
    break;
    case 'stopCommunication':
        console.info("Communication ended by remote peer");
        stop(true);
    break;
    default:
        console.error('Unrecognized message', parsedMessage);
    }
}
function incomingCall(message) {
    //If bussy just reject without disturbing user
    if(callState != NO_CALL){
        var response = {
            id : 'incomingCallResponse',
            from : message.from,
            callResponse : 'reject',
            message : 'bussy'
        };
        return sendMessage(response);
    }
    setCallState(PROCESSING_CALL);
    if (confirm('User ' + message.from + ' is calling you. Do you accept the call?')) {
        showSpinner(videoInput, videoOutput);
        webRtcPeer = kurentoUtils.WebRtcPeer.startSendRecv(videoInput, videoOutput,
        function(sdp, wp) {
        var response = {
            id : 'incomingCallResponse',
            from : message.from,
            callResponse : 'accept',
            sdpOffer : sdp
        };
        sendMessage(response);
    }, function(error){
        setCallState(NO_CALL);
    });
    } else {
        var response = {
            id : 'incomingCallResponse',
            from : message.from,
            callResponse : 'reject',
                message : 'user declined'
        };
        sendMessage(response);
        stop();
    }
}


function call() {
    if(document.getElementById('peer').value == ''){
        window.alert("You must specify the peer name");
        return;
}
setCallState(PROCESSING_CALL);
showSpinner(videoInput, videoOutput);
kurentoUtils.WebRtcPeer.startSendRecv(videoInput, videoOutput, function(offerSdp, wp) {
    webRtcPeer = wp;
    console.log('Invoking SDP offer callback function');
    var message = {
            id : 'call',
            from : document.getElementById('name').value,
            to : document.getElementById('peer').value,
            sdpOffer : offerSdp
    };
    sendMessage(message);
}, function(error){
    console.log(error);
    setCallState(NO_CALL);
});
}

 git

5.3.5 依賴庫

This Java Spring application is implementad using Maven. 
The relevant part of the pom.xml is where Kurento dependencies are declared. 
As the following snippet shows, we need two dependencies: the Kurento Client Java dependency
(kurento-client) and the JavaScript Kurento utility library (kurento-utils) for the client-side:
<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.
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>


 github

相關文章
相關標籤/搜索