多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] 参考:[https://www.jianshu.com/p/17bdf7af4f89](https://www.jianshu.com/p/17bdf7af4f89) ## basic example的client 访问https://ip:3004请求的文件是`extras/basic_example/public/index.html` 引入了两个js脚本 ``` <script type="text/javascript" src="erizo.js"></script> <script type="text/javascript" src="script.js"></script> ``` ## 初始化流-读取配置文件 scripts中设置了`window.onload()`的回调,所以加载的时候就直接运行了如下回调 ``` window.onload = () => { fillInConfigFlagsFromParameters(configFlags); window.configFlags = configFlags; const shouldSkipButton = !configFlags.forceStart && (!configFlags.onlySubscribe || configFlags.noStart); if (shouldSkipButton) { startBasicExample(); // 启动 } else { document.getElementById('startButton').disabled = false; } }; ``` startBasicExample();的作用是获取本地流和读取配置文件 ``` localStream = Erizo.Stream(config); //创建本地流 window.localStream = localStream; // 把本地流赋值到window.localStream供全局使用 ``` ## Erizo对象 **Erizo**是erizo.js定义导出的全局对象, Stream是其函数对象,函数定义Stream.js中, 其提供了一些操作流的接口,包括播放,控制等等,并且还创建了事件分发器,用来处理publisher或者room的事件 ``` require('expose-loader?adapter!../lib/adapter.js'); const Erizo = { Room: Room.bind(null, undefined, undefined, undefined), LicodeEvent, RoomEvent, StreamEvent, ConnectionEvent, //Stream 使用Stream.bind创建返回一个函数对象赋值 //创建的时候该对象函数中的this指针置NULL, 调用时第一个参数默认为undefined Stream: Stream.bind(null, undefined), Logger, }; export default Erizo; ``` ## 创建token流程 ### 客户端发起请求 客户端完成本地媒体的初始化之后,将生成的**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和roomid数据 创建token请求会被发送到服务器,服务器的**express http框架**会进行处理,将请求通过匹配到对应函数,对请求进行处理,此处为创建完**token**并同时创建**room**,将**token**和**roomid**返回发送回去 ``` //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); } }); ``` ## 初始化本地媒体 ### 收到token初始化Room对象,绑定事件回调 发送了**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 (configFlags.onlySubscribe) { room.connect({ singlePC: configFlags.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); localStream.addEventListener('access-accepted', () => { room.connect({ singlePC: configFlags.singlePC }); localStream.show('myVideo'); }); localStream.init(); } }); ``` 在**room.connect()**时候,会对得到的**token**进行解析获得**erizocontroller**,也就是licode的媒体服务器入口的ip和port,建立ws连接,建立完成后,通过事件管理器(**EventDispatcher**)向上层抛出**room-connected**事件, **room-connected**事件的处理回调中,调用了**room.publish**和**room.autoSubscribe**进行推拉流 ## 事件处理 无论是Erizo.Room还是Erizo.Stream,都可以分别在Room.js和Stream.js中找到其对应的对象生成方式,在生成对象的过程中都可以看到是先从生成一个**EventDispatcher**,然后在其上面作派生的 ``` //licode/erizo_controller/erizoClient/src/Erizo.js //licode/erizo_controller/erizoClient/src/Room.js const that = EventDispatcher(spec); ``` **EventDispatcher**是一个事件处理器,在**Event.js**中可以找到,其维护了一个对象数组**eventListeners**,将事件和回调做了key-value的绑定,当事件发生的时候,外部调用dispatchEvent 遍历搜索,执行其回调. ``` const EventDispatcher = () => { const that = {}; // Private vars const dispatcher = { eventListeners: {}, }; // Public functions // It adds an event listener attached to an event type. 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 = {}; }; // It dispatch a new event to the event listeners, based on the type // of event. All events are intended to be LicodeEvents. that.dispatchEvent = (event) => { if (!event || !event.type) { throw new Error('Undefined event'); } let listeners = dispatcher.eventListeners[event.type] || []; listeners = listeners.slice(0); for (let i = 0; i < listeners.length; i += 1) { try { listeners[i](event); } catch (e) { log.info(`Error triggering event: ${event.type}, error: ${e}`); } } }; that.on = that.addEventListener; that.off = that.removeEventListener; that.emit = that.dispatchEvent; return that; }; ``` 在使用**Erizo.Room({ token });**创建**Room**对象的过程中,可以看到其是先生成一个**EventDispatcher**对象然后在其上面进行扩展。 ``` room = Erizo.Room({ token }); window.room = room; ``` ## 推拉流 **publish**在**room-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协商完成之后就会自动开始推流。