licode(1) Basic Example 客户端解析

整体

在浏览其中输入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协商完成之后就会自动开始推流。

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