整體
在瀏覽其中輸入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()首先會讀取配置始赎,創(chuàng)建本地stream
//licode\extras\basic_example\public\script.js
localStream = Erizo.Stream(config); //創(chuàng)建本地stream
Erizo是erizo.js定義導出的全局對象, Stream是其函數對象鄙早,函數定義Stream.js中, 其提供了一些操作流的接口撬腾,包括播放,控制等等装黑,并且還創(chuàng)建了事件分發(fā)器副瀑,用來處理publisher或者room的事件
//licode\erizo_controller\erizoClient\src\Erizo.js
const Erizo = {
Room: Room.bind(null, undefined, undefined, undefined),
LicodeEvent,
RoomEvent,
StreamEvent,
//Stream 使用Stream.bind創(chuàng)建返回一個函數對象賦值
//創(chuàng)建的時候該對象函數中的this指針置NULL, 調用時第一個參數默認為undefined
Stream: Stream.bind(null, undefined),
Logger,
};
export default Erizo;
客戶端完成本地媒體的初始化之后恋谭,將生成的Roomdata當作參數發(fā)送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) => {.....});
創(chuàng)建token請求會被發(fā)送到服務器糠睡,服務器的express http框架會進行處理,將請求通過匹配到對應函數疚颊,對請求進行處理狈孔,此處為創(chuàng)建完token并同時創(chuàng)建room,將token和roomid返回發(fā)送回去
//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發(fā)送回去
}, (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);
}
});
發(fā)送了createroken請求材义,客戶端收到token之后均抽,根據返回的token(其中包含了服務端創(chuàng)建的room的一些信息)去初始化Room對象,并為一些事件綁定回調其掂,比如房間連接成功了油挥,流訂閱等等, 然后調用localStream.init() 初始化本地媒體
//licode\extras\basic_example\public\script.js
createToken(roomData, (response) => {
const token = response;
console.log(token);
//創(chuàng)建房間
room = Erizo.Room({ token });
//創(chuàng)建訂閱流接口
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 {
//默認執(zhí)行的地方
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.publish和room.autoSubscribe進行推拉流
事件處理
無論是Erizo.Room還是Erizo.Stream负饲,都可以分別在Room.js和Stream.js中找到其對應的對象生成方式,在生成對象的過程中都可以看到是先從生成一個EventDispatcher喂链,然后在其上面作派生的
const that = EventDispatcher(spec);
EventDispatcher是一個事件處理器返十,在Event.js中可以找到,其維護了一個對象數組eventListeners椭微,將事件和回調做了key-value的綁定洞坑,當事件發(fā)生的時候,外部調用dispatchEvent 遍歷搜索蝇率,執(zhí)行其回調
//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的回調,并執(zhí)行
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 });創(chuàng)建Room對象的過程中本慕,可以看到其是先生成一個EventDispatcher對象然后在其上面進行擴展排拷。
媒體
| publish
publish在room-connected之后發(fā)生
//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中發(fā)送了SDP锅尘,將流填充到本地數組進行管理监氢, 創(chuàng)建流連接
//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;
//發(fā)送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);
//創(chuàng)建流連接
createLocalStreamErizoConnection(stream, connectionId, erizoId, options);
callback(id);
});
};
創(chuàng)建流連接中添加了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接口包了一層浪腐,并繼承了事件發(fā)生器,將有關連接以及媒體的事件拋到上層的事件處理器中進行處理, 此處調用了原生的接口addStream之后顿乒,通過后續(xù)的發(fā)送offer協商完成之后就會自動開始推流议街。