整体

在浏览其中输入https://dst_host_domain:13004后, 请求了index.html,该文件在licode\extras\basic_example\public\index.html开始, 引入了erizo.js和script.js, testConnection()

//licode\extras\basic_example\public\index.html

<html>
<head>
<title>Licode Basic Example</title>
<script type="text/javascript" src="erizo.js"></script>
<script type="text/javascript" src="script.js"></script>//定义了windows.load
</head> <body>
<button id="startButton" onclick="startBasicExample()" disabled>Start</button>
<button id="testConnection" onclick="testConnection()">Test Connection</button>
<button id="recordButton" onclick="startRecording()" disabled>Toggle Recording</button>
<button id="slideShowMode" onclick="toggleSlideShowMode()" disabled>Toggle Slide Show Mode</button>
<h1 id="startWarning">Press the start buttong to start receiving streams</h1>
<div id="videoContainer"></div>
</body>
</html>

由于scripts中设置了window.onload()的回调,所以加载的时候就直接运行了如下回调,去调用例子

//licode\extras\basic_example\public\script.js

window.onload = () => {
const onlySubscribe = getParameterByName('onlySubscribe');
const bypassStartButton = getParameterByName('noStart');
if (!onlySubscribe || bypassStartButton) {
startBasicExample();//启动例子
} else {
document.getElementById('startButton').disabled = false;
}
};

startBasicExample()首先会读取配置,创建本地stream

//licode\extras\basic_example\public\script.js

  localStream = Erizo.Stream(config); //创建本地stream

Erizo是erizo.js定义导出的全局对象, Stream是其函数对象,函数定义Stream.js中, 其提供了一些操作流的接口,包括播放,控制等等,并且还创建了事件分发器,用来处理publisher或者room的事件

//licode\erizo_controller\erizoClient\src\Erizo.js

const Erizo = {
Room: Room.bind(null, undefined, undefined, undefined),
LicodeEvent,
RoomEvent,
StreamEvent,
//Stream 使用Stream.bind创建返回一个函数对象赋值
//创建的时候该对象函数中的this指针置NULL, 调用时第一个参数默认为undefined
Stream: Stream.bind(null, undefined),
Logger,
};
export default Erizo;

客户端完成本地媒体的初始化之后,将生成的Roomdata当作参数发送createtoken请求给服务端, 响应后调用callback进行回调

//licode\extras\basic_example\public\script.js

  const createToken = (roomData, callback) => {
const req = new XMLHttpRequest();
const url = `${serverUrl}createToken/`; req.onreadystatechange = () => { //设置响应回调
if (req.readyState === 4) {
callback(req.responseText);
}
}; req.open('POST', url, true);
req.setRequestHeader('Content-Type', 'application/json');
req.send(JSON.stringify(roomData));
}; createToken(roomData, (response) => {.....});

创建token请求会被发送到服务器,服务器的express http框架会进行处理,将请求通过匹配到对应函数,对请求进行处理,此处为创建完token并同时创建room,将tokenroomid返回发送回去

//licode\extras\basic_example\basicServer.js

basicServer.js

app.post('/createToken/', (req, res) => {
console.log('Creating token. Request body: ', req.body); const username = req.body.username;
const role = req.body.role; let room = defaultRoomName;
let type;
let roomId;
let mediaConfiguration; if (req.body.room) room = req.body.room;
if (req.body.type) type = req.body.type;
if (req.body.roomId) roomId = req.body.roomId;
if (req.body.mediaConfiguration) mediaConfiguration = req.body.mediaConfiguration; const createToken = (tokenRoomId) => {
N.API.createToken(tokenRoomId, username, role, (token) => {
console.log('Token created', token);
res.send(token);//将token发送回去
}, (error) => {
console.log('Error creating token', error);
res.status(401).send('No Erizo Controller found');
});
}; if (roomId) {
createToken(roomId);
} else {
getOrCreateRoom(room, type, mediaConfiguration, createToken);
}
});

发送了createroken请求,客户端收到token之后,根据返回的token(其中包含了服务端创建的room的一些信息)去初始化Room对象,并为一些事件绑定回调,比如房间连接成功了,流订阅等等, 然后调用localStream.init() 初始化本地媒体

//licode\extras\basic_example\public\script.js

  createToken(roomData, (response) => {
const token = response;
console.log(token);
//创建房间
room = Erizo.Room({ token }); //创建订阅流接口
const subscribeToStreams = (streams) => {...... }; //添加一些事件处理回调,
room.addEventListener('room-connected', (roomEvent) => {......}); room.addEventListener('stream-subscribed', (streamEvent) => {......}); room.addEventListener('stream-added', (streamEvent) => {......}); room.addEventListener('stream-removed', (streamEvent) => {......}); room.addEventListener('stream-failed', () => {......}); if (onlySubscribe) {
room.connect({ singlePC });
} else {
//默认执行的地方
const div = document.createElement('div');
div.setAttribute('style', 'width: 320px; height: 240px; float:left');
div.setAttribute('id', 'myVideo');
document.getElementById('videoContainer').appendChild(div);
//为'access-accepted'事件设置回调,该回调渲染视频画面
localStream.addEventListener('access-accepted', () => {
room.connect({ singlePC });
localStream.show('myVideo');
});
//初始化
localStream.init();
}
});

其中,在room.connect()时候,会对得到的token进行解析获得erizocontroller,也就是licode的媒体服务器入口的ip和port,建立ws连接,建立完成后,通过事件管理器(EventDispatcher)向上层抛出room-connected事件, room-connected事件的处理回调中,调用了room.publishroom.autoSubscribe进行推拉流

事件处理

无论是Erizo.Room还是Erizo.Stream,都可以分别在Room.js和Stream.js中找到其对应的对象生成方式,在生成对象的过程中都可以看到是先从生成一个EventDispatcher,然后在其上面作派生的

  const that = EventDispatcher(spec);

EventDispatcher是一个事件处理器,在Event.js中可以找到,其维护了一个对象数组eventListeners,将事件和回调做了key-value的绑定,当事件发生的时候,外部调用dispatchEvent 遍历搜索,执行其回调

//licode\erizo_controller\erizoClient\src\Events.js

const EventDispatcher = () => {
const that = {};
// Private vars
const dispatcher = {
eventListeners: {},
}; // Public functions //将事件和回调放到对象数组中去
that.addEventListener = (eventType, listener) => {
if (dispatcher.eventListeners[eventType] === undefined) {
dispatcher.eventListeners[eventType] = [];
}
dispatcher.eventListeners[eventType].push(listener);
}; // It removes an available event listener.
that.removeEventListener = (eventType, listener) => {
if (!dispatcher.eventListeners[eventType]) {
return;
}
const index = dispatcher.eventListeners[eventType].indexOf(listener);
if (index !== -1) {
dispatcher.eventListeners[eventType].splice(index, 1);
}
}; // It removes all listeners
that.removeAllListeners = () => {
dispatcher.eventListeners = {};
}; that.dispatchEvent = (event) => {
//遍历,找到该event的回调,并执行
let listeners = dispatcher.eventListeners[event.type] || [];
listeners = listeners.slice(0);
for (let i = 0; i < listeners.length; i += 1) {
listeners[i](event);
}
}; that.on = that.addEventListener;
that.off = that.removeEventListener;
that.emit = that.dispatchEvent; return that;
};

在使用Erizo.Room({ token });创建Room对象的过程中,可以看到其是先生成一个EventDispatcher对象然后在其上面进行扩展。

媒体

publish

publishroom-connected之后发生

//licode\extras\basic_example\public\script.js

      if (!onlySubscribe) {
room.publish(localStream, options);//将本地媒体publish
}

该函数实际如下,根据config对流进行一些设置之后开始推流

//licode\erizo_controller\erizoClient\src\Room.js

  that.publish = (streamInput, optionsInput = {}, callback = () => {}) => {
const stream = streamInput;
const options = optionsInput; //设置流的一些属性以及会调
省略......
if (stream && stream.local && !stream.failed && !localStreams.has(stream.getID())) {
if (stream.hasMedia()) {
if (stream.isExternal()) {
publishExternal(stream, options, callback);
} else if (that.p2p) {
publishP2P(stream, options, callback);
} else {
publishErizo(stream, options, callback);//推流
}
} else if (stream.hasData()) {
publishData(stream, options, callback);
}
} else {
Logger.error('Trying to publish invalid stream, stream:', stream);
callback(undefined, 'Invalid Stream');
}
};

publishErizo中发送了SDP,将流填充到本地数组进行管理, 创建流连接

//licode\erizo_controller\erizoClient\src\Room.js

  const publishErizo = (streamInput, options, callback = () => {}) => {
const stream = streamInput;
Logger.info('Publishing to Erizo Normally, is createOffer', options.createOffer);
const constraints = createSdpConstraints('erizo', stream, options);
constraints.minVideoBW = options.minVideoBW;
constraints.maxVideoBW = options.maxVideoBW;
constraints.scheme = options.scheme; //发送publish信令到媒体服务器和SDP
socket.sendSDP('publish', constraints, undefined, (id, erizoId, connectionId, error) => {
if (id === null) {
Logger.error('Error publishing stream', error);
callback(undefined, error);
return;
}
//填充流
populateStreamFunctions(id, stream, error, undefined);
//创建流连接
createLocalStreamErizoConnection(stream, connectionId, erizoId, options);
callback(id);
});
};

创建流连接中添加了icestatechanged的失败回调,以及调用了pc连接中的addstream接口

//licode\erizo_controller\erizoClient\src\Room.js

 const createLocalStreamErizoConnection = (streamInput, connectionId, erizoId, options) => {
const stream = streamInput;
const connectionOpts = getErizoConnectionOptions(stream, connectionId, erizoId, options);
stream.addPC(
that.erizoConnectionManager
.getOrBuildErizoConnection(connectionOpts, erizoId, spec.singlePC)); //绑定icestatechanged到failed的回调
stream.on('icestatechanged', (evt) => {
Logger.info(`${stream.getID()} - iceConnectionState: ${evt.msg.state}`);
if (evt.msg.state === 'failed') {
const message = 'ICE Connection Failed';
onStreamFailed(stream, message, 'ice-client');
if (spec.singlePC) {
connectionOpts.callback({ type: 'failed' });
}
}
});
//调用pcconnect连接中的添加流
stream.pc.addStream(stream);
};

其中pc连接是定义licode\erizo_controller\erizoClient\src\ErizoConnectionManager.js中的class ErizoConnection,其对浏览器原生的webrtc的js接口包了一层,并继承了事件发生器,将有关连接以及媒体的事件抛到上层的事件处理器中进行处理, 此处调用了原生的接口addStream之后,通过后续的发送offer协商完成之后就会自动开始推流。

licode(1) Basic Example 客户端解析的更多相关文章

  1. HTTP Basic 验证客户端 C#实现笔记

    HTTP Basic 验证客户端的原理:把HTTP头重的ContentType设置为:application/x-www-form-urlencoded如果HTTP头没有Authorization,那 ...

  2. LINUX DNS客户端 解析域名慢的问题。

    Linux系统下域名解析的配置文件是/etc/resolv.conf cat /etc/resolv.conf # Generated by NetworkManager options single ...

  3. Springboot 构建http服务,返回的http行是'HTTP/1.1 200' 无状态码描述 客户端解析错误

    ————————————————————————————————————————— *** 响应的数据格式  HTTP/1.1 200 OK  Server: Apache-Coyote/1.1  A ...

  4. OpenVPN客户端解析

    windows版本的VPN客户端,实际上就是一个外壳,创建了图形界面,托盘,和 右键菜单, 在connect的动作里,实际上是通过cmd调用 openvpn.exe openvpn --config ...

  5. Mysql异常问题排查与处理——mysql的DNS反向解析和客户端网卡重启

    中午刚想趴一会,不料锅从天降!!!Mysql连不上了....... 现象如下: 现象1:登录mysql所在服务器,连接MySQL 成功: 现象2:通过客户端远程连接MySQL,返回失败,如下: Ent ...

  6. Ace教你一步一步做Android新闻客户端(三) JSON数据解析

    对于服务器端来说,返回给客户端的数据格式一般分为html.xml和json这三种格式,现在给大家讲解一下json这个知识点, 1 如何通过json-lib和gson这两个json解析库来对解析我们的j ...

  7. Arcgis瓦片--js客户端加载

    接上篇博客,下载好arcgis格式的瓦片数据以后,需要用js客户端在前端加载出来.这里介绍两种方案: 1.使用超图iServer将瓦片发布成rest地图服务,或者arcgis地图服务,客户端直接加载 ...

  8. javascript客户端与服务器端通信

    高性能的网络通信包括以下方面:选择正确的数据格式和与之匹配的传输技术. 一.数据格式 用于传输的数据格式有: 1)html,仅适用于特定场合,传输数据量大,不过它可以节省客户端的CPU周期, 2)XM ...

  9. Delphi用QJSON解析JSON格式的数据

    本来用superobject来解析JSON已经够用了,可惜这个东东不能在移动端使用,于是找到QJSON来处理. 这是一个国内高手写开源免费的东西,赞一个. 假入数据如下: {"message ...

随机推荐

  1. 微信小程序-强制手机端更新

    小程序的更新机制 开发者在管理后台发布新版本的小程序之后,如果某个用户本地有小程序的历史版本,此时打开的可能还是旧版本.微信客户端会有若干个时机去检查本地缓存的小程序有没有更新版本,如果有则会静默更新 ...

  2. 建造者模式-C#改良实现

    区分网上已有的一般建造者模式实现,个人觉得实现太单一了,自己google查了一些好的实现,挑了其中比较适合的,做个笔记. # region 标准Builder模式实现 // 产品 class Tele ...

  3. 分享几个好看又实用的PPT网站~

    一,优品PPT[http://www.ypppt.com/] 一个有情怀的免费PPT模板下载网站!拥有非常多很精美的PPT模板,分类齐全,我们可以选择自己喜欢的PPT模板下载套用就可以了. 二,扑奔P ...

  4. 案例——UDP聊天

    UDP聊天案例   做一个网络编程相关的案例,想着用利用UDP的快速且不用连接的优点做一个聊天室,我们一个聊天程序需要可以接收消息,也要可以发送消息,所以我们的DatagramSocket对象不但需要 ...

  5. PHP 字符串大全

    PHP字符串函数是核心的一部分.无需安装即可使用这些函数 函数名称 描述 addcslashes 以 C 语言风格使用反斜线转义字符串中的字符 addslashes 使用反斜线引用字符串 bin2he ...

  6. FCC---CSS Flexbox: Apply the flex-direction Property to Create Rows in the Tweet Embed

    The header and footer in the tweet embed example have child items that could be arranged as rows usi ...

  7. iOS 中使用 webSocket

    iOS 中使用 webSocket 是服务器和app之间的一种通信方式 webSocket 实现了服务端推机制(主动向客户端发送消息).新的 web 浏览器全都支持 WebSocket,这使得它的使用 ...

  8. NFS挂载遇到的问题

    问题描述:生产环境中需要经常运用NFS挂载,就在测试环境中测试一下,将服务器中192.168.1.4 /u01/app/oracle/product/11.2.0/dbhome_1/dbs  挂载到1 ...

  9. CF#603 Div2

    差不多半年没打cf,还是一样的菜:不过也没什么,当时是激情,现在已是兴趣了,开心就好. A Sweet Problem 思维,公式推一下过了 B PIN Codes 队友字符串取余过了,结果今天早上一 ...

  10. curl 带 body

    curl -H "Content-Type:plain/text" -X POST -d '<?xml version="1.0" encoding=&q ...