Kurento Tutorial 官方文档学习记录 Java - Hello world

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

理解例子的源码

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

1.应用的逻辑

很简单,浏览器将摄像头和麦克风获得的本地流传输给远程的KMS(或本级部署的)然后再不经调制的传输回本地浏览器进行显示。
要完成这个动作,我们需要创造一个只包含一个Media ElementMedia Pipeline。本例中那个唯一的 Media Element 就是一个 WebRtcEndpoint,它具有全双工交换WebRTC媒体流的能力。它自己与自己相连,以保证送出的流通过一圈再传送回来,也就是我们要实现的镜像功能(loopback)。
介绍如下


这块其实很好理解,不用太过注意这个图。

2.架构

由于是浏览器应用,所以也服从CS架构。
在客户端,使用 JavaScript 来实现客户端逻辑。在服务器端,用基于 Spring-BootJava 实现,调用 Kurento Java Client API 来控制 Kurento Media Server
总之,这个应用是一个三层结构的应用,一共有三层实体分别为

客户端 —— 客户端服务器&Kurento Java Client —— KMS

注意这里的客户端服务器就是KJC,只是看的角度不一样。它处理来自客户端的请求,同时控制KMS,即,既是客户端的服务器,又是KMS的客户端。
由于是三层结构,就需要两个WebSocket了,一个用于前两个之间的连接,遵循Custom Signaling Protocol,另一个用于后两者的连接,遵循Kurento Protocol
想进一步了解可以看关于这块的详细信息

接下来用一个SD来体现这三者之间的关系。该图中包含的详细信息十分重要,每一个信息的交互都会在后边的代码中体现。


图1

3.代码分析

(1)服务器端

服务器端就是SD图中间的绿色实体,也就是三层架构中位于中间层的Application Server
再次标注一下,服务器端用了基于Spring-BootJava来实现。

  • 首先来看一下服务器端代码的整体架构


    图2

    图中列出的几个类就是我们的Java的代码中的所有类了。一会再看代码的时候我们会一步一步的发现这个图的指向具体意思。这个图也是一个对于理解代码非常重要的图。

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

我们可以看到,在HelloWorldApp类中,HelloWorldHandlerKurentoClient都被实例化为了 Bean. Bean是Spring-Boot框架中一个重要的结构名称,具体可以去了解Spring-Boot框架中的这部分内容,这里你可以就把它当成,是 HelloWorldApp 是一个大容器,里面装了两个小容器分别是 HelloWorldHandlerKurentoClient 。这就是为什么图2HelloWorldApp 实体有两条尖头分别指向 HelloWorldHandlerKurentoClient 实体

这里的KurentoClient在创建时默认了KMS服务器在本机上。如果你的KMS服务器部署在远程的话,就需要向如下代码一样创建KurentoClient

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

可以看到 HelloWorldApp 类在创建时 implements 了 WebSocketConfigurer

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

通过 Override WebSocketConfigurerregisterWebSocketHandlers(WebSocketHandlerRegistry registry)方法,将 HelloWorldHandler 类的实例handler作为WebSocketHandler来处理 WebSocket 请求,且处理路径为“/helloworld”

  • WebSocket处理类 HelloWorldHandler
    可以看到 HelloWorldHandler 类在创建时 implements 了TextWebSocketHandler
    该类的核心部分是handleTextMessage方法,这个方法通过手动编写逻辑实现了上文提到的两个 WebSocket 中遵循 Custom Signaling Protocol 的那个 WebSocket 的处理程序,也就是作为Client Server的那部分功能,体现在图1JavaScriptClientApplicationServer 之间的信息交互。而通过直接调用 Kurento Java Client API 来完成第二个 websocket 的通信,是自动的,不用手动码逻辑。
    注意,这里的代码每一处都可以在图1中的信息交互中找到对应的表示。
    根据JsonMessage的不同分了三个不同的case,其中最重要的是start,里面调用了start函数,见下文。
    start方法主要完成了如下四个工作,注释也都写出来了。

配置媒体处理逻辑

MediaPipeline pipeline = kurento.createMediaPipeline();
WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
webRtcEndpoint.connect(webRtcEndpoint);
1.创建MediaPipeline
2.用pipeline创建WebRtcEndpoint
3.创建好的webRtcEndpoint自己和自己相连

  • 这几步在前文都清楚得提到过

存储用户会话

UserSession user = new UserSession();
user.setMediaPipeline(pipeline);
user.setWebRtcEndpoint(webRtcEndpoint);
users.put(session.getId(), user);
由于在最后会调用stop来释放,所以需要存储用户会话来保证这个功能,也就是要存储MediaPipelineWebRtcEndpoint

SDP通讯

String sdpOffer = >>jsonMessage.get("sdpOffer").getAsString();
jsonMessage中获得sdpOffer部分并转换成String
String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);
sdpOffer作为参数传给webRtcEndpoint形成sdpAnswer,调用了webRtcEndpointprocessOffer方法,这里对应图1中的左边前两条数据交换箭头,其中processOffer方法完成了两条线中间右边Application ServerKMS之间的一系列数据交换。
JsonObject response = new JsonObject();
response.addProperty("id", "startResponse");
response.addProperty("sdpAnswer", sdpAnswer);
synchronized (session) { session.sendMessage(new TextMessage(response.toString())); }
这里是把sdpAnswer加入一个json里再加上一些必要信息然后再转换成String并在会话里送出。

  • WebRtcpeer 之间交换媒体数据遵循SDP (Session Description protocol),也就是说用的是SDPOfferSDPAnswer机制。
  • 这一步完成了图1中上半部份的SDP通信的功能,包括处理来自JavaScript的客户端请求和控制远端KMS两部分动作。
  • 注意⚠️,JavaScriptClientwebsocket请求用这里的HelloWorldHandler来处理是由于在Index.js里制定了websocket的处理路径是/helloworld,并且在主程序HelloWorldApp里将handler注册到了/helloworld路径里。

收集ICE candidates

Kurento 6 以后全面支持Trickle ICE 协议,使得WebRtcEndpoint 可以异步得收集 ICE candidates。也正是因此,每一个WebRtcEndpoint 都需要一个监听器,可以在每次ICE 收集程序结束后接收到一个事件的触发
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添加了一个监听器。
2.webRtcEndpoint.gatherCandidates(),开始收集ICE Candidates

然后我们来分析第一个代码。并将它与图1的信息交互进行对照.
很简单,在构造监听器的时候重写它的onEvent方法,接收参数为IceCandidateFoundEvent类型,这是 Kurento Java Client API 中的函数webRtcEndpoint.gatherCandidates();可以得到的结果类型,当监听器发现这个类型的参数出现,或者说一旦gatherCandidates()方法开始得到结果时,就会触发监听器。监听器内部所做的事情就是把candidate转成Json然后加上附加信息再转成String然后发出去。
现在可以对着代码去图1里找一下对应的线了。同样,左边bar的信息交互是这里手动码出来的动作,右边bar的信息交互是由 Kurento Java Client API 自动实现的,不需要费心再码。

(2)客户端

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

首先看到,它用了 JavaScript 类来创建webSocket,并且把这个webSocket的处理路径放在了/helloworld,这就是前文提到的,客户端是怎样和 server 端实现 webSocket 的通信。
well,再总结一遍,其实就是在客户端用 JavaScript 建一个webSocket,并把处理路径设置为/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);
}

设置onload监听器,new 一个consloe,输出“Page loaded”,然后获取videoInputvideoOutput对象(这两个的id是在html里面定义的),然后setState到可以启动

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

设置onbeforeunload监听器,语意为关闭窗口前关掉 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);
    }
}

设置onmessage监听器,有一个参数message,进来以后把它Jason化,然后switch。一共会有三种类型的message ,分别是startResponse, error,和 iceCandidate
然后就是每种message之后的操作。
(1)正常的操作是在I_CAN_START状态中的start按钮的onClick方法中做的,调用start()函数来执行操作进行信息交互,而start里的操作就是图1中JavaScriptClient端的各种信息交互操作了。
注意⚠️,正常情况下这些函数基本都是在setState里面的设置按钮的onClick方法里调用的,稍微有点隐蔽。而setState本身也是一个神奇的函数,它是用来设置整个页面的状态的,状态不同每个按钮的状态也就不同,点了以后的操作也不一样。这是一个很神奇的操作。

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()方法做了什么,设置状态输出文字什么的就不说了,最主要是它用了kurento-utils.js里的WebRtcPeer.WebRtcPeerSendrecv()方法来创建一个 WebRtc 通信。
这里可能会有一些疑问,这里不是只是JavaScriptClientApplicationServer之间的通信吗,换句话说这里不是应该只是JavaScriptClient发出的webSocket请求吗怎么又用了 Kurento 的一个 util.js 的库。
其实是这样的,这里就是JavaScriptClientApplicationServer之间的通信,虽然调用的是 kurento 的库,但实现的是 WebRtc 的通信啊,WebRtc 本来就是基于浏览器的,如果你在JavaScriptClientApplicationServer之间不启动 WebRtcApplicationServerKMS之间的 Kurento 也就毫无意义了。所以这里虽然用了 Kurento 的一个库但还牵扯不到 Kurento,还只是浏览器原生的 WebRtc 通信的建立。
(2)在点了start按钮并且start()了以后,就进入了处理 WebRtc 通信的阶段,其中startResponsecase中做的是开始JS客户端的SDP信息交互操作,iceCandidate同样,是调用那个kurento-utils.js里的 WebRtc 原生方法来实现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的一些设置,但我没有看,应该也比较好理解。所以,就到这里吧,还有什么之后看到了再随时补充。

















最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容