WebRTC:一个视频谈天的简朴例子

访客4年前黑客资讯1081

相关API简介

在前面的章节中,已经对WebRTC相关的主要知识点举行了先容,包罗涉及的 *** 协议、会话形貌协议、若何举行 *** 穿透等,剩下的就是WebRTC的API了。

WebRTC通讯相关的API异常多,主要完成了如下功效:

  1. 信令交流
  2. 通讯候选地址交流
  3. 音视频采集
  4. 音视频发送、吸收

相关API太多,为制止篇幅过长,文中部门接纳了伪代码举行解说。详细代码参考文章末尾,也可以在笔者的Github上找到,有问题迎接留言交流。


信令交流

信令交流是WebRTC通讯中的关键环节,交流的信息包罗编解码器、 *** 协议、候选地址等。对于若何举行信令交流,WebRTC并没有明确说明,而是交给应用自己来决议,好比可以接纳WebSocket。

发送方伪代码如下:

const pc = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeerViaSignalingServer(SIGNALING_OFFER, offer); // 发送方发送信令新闻

吸收方伪代码如下:

const pc = new RTCPeerConnection(iceConfig);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeerViaSignalingServer(SIGNALING_ANSWER, answer); // 吸收方发送信令新闻


候选地址交流服务

当内陆设置了会话形貌信息,并添加了媒体流的情况下,ICE框架就会更先 *** 候选地址。双方 *** 到候选地址后,需要交流候选地址,并从中知道合适的候选地址对。

候选地址的交流,同样接纳前面提到的信令服务,伪代码如下:

// 设置内陆会话形貌信息
const localPeer = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await localPeer.setLocalDescription(offer);

// 内陆采集音视频
const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({ 
    video: true, 
    audio: true
});
localVideo.srcObject = mediaStream;

// 添加音视频流
mediaStream.getTracks().forEach(track => {
    localPeer.addTrack(track, mediaStream);
});

// 交流候选地址
localPeer.onicecandidate = function(evt) {
    if (evt.candidate) {
        sendToPeerViaSignalingServer(SIGNALING_CANDIDATE, evt.candidate);
    }
}


音视频采集

可以使用浏览器提供的getUserMedia接口,采集内陆的音视频。

const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({ 
    video: true, 
    audio: true
});
localVideo.srcObject = mediaStream;


音视频发送、吸收

将采集到的音视频轨道,通过addTrack举行添加,发送给远端。

mediaStream.getTracks().forEach(track => {
    localPeer.addTrack(track, mediaStream);
});

远端可以通过监听ontrack来监听音视频的到达,并举行播放。

remotePeer.ontrack = function(evt) {
    const remoteVideo = document.getElementById('remote-video');
    remoteVideo.srcObject = evt.streams[0];
}


完整代码

包罗两部门:客户端代码、服务端代码。

1、客户端代码

const socket = io.connect('http://localhost:3000');

const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';

const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
const SERVER_USER_EVENT = 'SERVER_USER_EVENT';

const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录

const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';

const SIGNALING_OFFER = 'SIGNALING_OFFER';
const SIGNALING_ANSWER = 'SIGNALING_ANSWER';
const SIGNALING_CANDIDATE = 'SIGNALING_CANDIDATE';

let remoteUser = ''; // 远端用户
let localUser = ''; // 内陆登录用户

function log(msg) {
    console.log(`[client] ${msg}`);
}

socket.on('connect', function() {
    log('ws connect.');
});

socket.on('connect_error', function() {
    log('ws connect_error.');
});

socket.on('error', function(errorMessage) {
    log('ws error, ' + errorMessage);
});

socket.on(SERVER_USER_EVENT, function(msg) {
    const type = msg.type;
    const payload = msg.payload;

    switch(type) {
        case SERVER_USER_EVENT_UPDATE_USERS:
            updateUserList(payload);
            break;
    }
    log(`[${SERVER_USER_EVENT}] [${type}], ${ *** ON.stringify(msg)}`);
});

socket.on(SERVER_RTC_EVENT, function(msg) {
    const {type} = msg;

    switch(type) {
        case SIGNALING_OFFER:
            handleReceiveOffer(msg);
            break;
        case SIGNALING_ANSWER:
            handleReceiveAnswer(msg);
            break;
        case SIGNALING_CANDIDATE:
            handleReceiveCandidate(msg);
            break;
    }
});

async function handleReceiveOffer(msg) {
    log(`receive remote description from ${msg.payload.from}`);
    
    // 设置远端形貌
    const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
    remoteUser = msg.payload.from;
    createPeerConnection();
    await pc.setRemoteDescription(remoteDescription); // TODO 错误处置

    // 内陆音视频采集
    const localVideo = document.getElementById('local-video');
    const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    localVideo.srcObject = mediaStream;
    mediaStream.getTracks().forEach(track => {
        pc.addTrack(track, mediaStream);
        // pc.addTransceiver(track, {streams: [mediaStream]}); // 这个也可以
    });
    // pc.addStream(mediaStream); // 现在这个也可以,不外接口后续会废弃

    const answer = await pc.createAnswer(); // TODO 错误处置
    await pc.setLocalDescription(answer);
    sendRTCEvent({
        type: SIGNALING_ANSWER,
        payload: {
            sdp: answer,
            from: localUser,
            target: remoteUser
        }
    });
}

async function handleReceiveAnswer(msg) {
    log(`receive remote answer from ${msg.payload.from}`);
    
    const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
    remoteUser = msg.payload.from;

    await pc.setRemoteDescription(remoteDescription); // TODO 错误处置
}

async function handleReceiveCandidate(msg){
    log(`receive candidate from ${msg.payload.from}`);
    await pc.addIceCandidate(msg.payload.candidate); // TODO 错误处置
}

/**
 * 发送用户相关新闻给服务器
 * @param {Object} msg 花样如 { type: 'xx', payload: {} }
 */
function sendUserEvent(msg) {
    socket.emit(CLIENT_USER_EVENT,  *** ON.stringify(msg));
}

/**
 * 发送RTC相关新闻给服务器
 * @param {Object} msg 花样如{ type: 'xx', payload: {} }
 */
function sendRTCEvent(msg) {
    socket.emit(CLIENT_RTC_EVENT,  *** ON.stringify(msg));
}

let pc = null;

/**
 * 约请用户加入视频谈天
 *  1、内陆启动视频采集
 *  2、交流信令
 */
async function startVideoTalk() {
    // 开启内陆视频
    const localVideo = document.getElementById('local-video');
    const mediaStream = await navigator.mediaDevices.getUserMedia({
        video: true, 
        audio: true
    });
    localVideo.srcObject = mediaStream;

    // 建立 peerConnection
    createPeerConnection();

    // 将媒体流添加到webrtc的音视频收发器
    mediaStream.getTracks().forEach(track => {
        pc.addTrack(track, mediaStream);
        // pc.addTransceiver(track, {streams: [mediaStream]});
    });
    // pc.addStream(mediaStream); // 现在这个也可以,不外接口后续会废弃
}

function createPeerConnection() {
    const iceConfig = {"iceServers": [
        {url: 'stun:stun.ekiga.net'},
        {url: 'turn:turnserver.com', username: 'user', credential: 'pass'}
    ]};
    
    pc = new RTCPeerConnection(iceConfig);

    pc.onnegotiationneeded = onnegotiationneeded;
    pc.onicecandidate = onicecandidate;
    pc.onicegatheringstatechange = onicegatheringstatechange;
    pc.oniceconnectionstatechange = oniceconnectionstatechange;
    pc.onsignalingstatechange = onsignalingstatechange;
    pc.ontrack = ontrack;
    
    return pc;
}

async function onnegotiationneeded() {
    log(`onnegotiationneeded.`);

    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer); // TODO 错误处置

    sendRTCEvent({
        type: SIGNALING_OFFER,
        payload: {
            from: localUser,
            target: remoteUser,
            sdp: pc.localDescription // TODO 直接用offer?
        }
    });
}

function onicecandidate(evt) {
    if (evt.candidate) {
        log(`onicecandidate.`);

        sendRTCEvent({
            type: SIGNALING_CANDIDATE,            
            payload: {
                from: localUser,
                target: remoteUser,
                candidate: evt.candidate
            }
        });
    }
}

function onicegatheringstatechange(evt) {
    log(`onicegatheringstatechange, pc.iceGatheringState is ${pc.iceGatheringState}.`);
}

function oniceconnectionstatechange(evt) {
    log(`oniceconnectionstatechange, pc.iceConnectionState is ${pc.iceConnectionState}.`);
}

function onsignalingstatechange(evt) {
    log(`onsignalingstatechange, pc.signalingstate is ${pc.signalingstate}.`);
}

// 挪用 pc.addTrack(track, mediaStream),remote peer的 onTrack 会触发两次
// 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用
// 这个行为有点新鲜,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313
let stream;
function ontrack(evt) {
    // if (!stream) {
    //     stream = evt.streams[0];
    // } else {
    //     console.log(`${stream === evt.streams[0]}`); // 这里为true
    // }
    log(`ontrack.`);
    const remoteVideo = document.getElementById('remote-video');
    remoteVideo.srcObject = evt.streams[0];
}

// 点击用户列表
async function handleUserClick(evt) {
    const target = evt.target;
    const userName = target.getAttribute('data-name').trim();

    if (userName === localUser) {
        alert('不能跟自己举行视频会话');
        return;
    }

    log(`online user selected: ${userName}`);

    remoteUser = userName;
    await startVideoTalk(remoteUser);
}

/**
 * 更新用户列表
 * @param {Array} users 用户列表,好比 [{name: '小明', name: '小强'}]
 */
function updateUserList(users) {
    const fragment = document.createDocumentFragment();
    const userList = document.getElementById('login-users');
    userList.innerHTML = '';

    users.forEach(user => {
        const li = document.createElement('li');
        li.innerHTML = user.userName;
        li.setAttribute('data-name', user.userName);
        li.addEventListener('click', handleUserClick);
        fragment.appendChild(li);
    });    
    
    userList.appendChild(fragment);
}

/**
 * 用户登录
 * @param {String} loginName 用户名
 */
function login(loginName) {
    localUser = loginName;
    sendUserEvent({
        type: CLIENT_USER_EVENT_LOGIN,
        payload: {
            loginName: loginName
        }
    });
}

// 处置登录
function handleLogin(evt) {
    let loginName = document.getElementById('login-name').value.trim();
    if (loginName === '') {
        alert('用户名为空!');
        return;
    }
    login(loginName);
}

function init() {
    document.getElementById('login-btn').addEventListener('click', handleLogin);
}

init();

2、服务端代码

// 添加ws服务
const io = require('socket.io')(server);
let connectionList = [];

const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';

const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
const SERVER_USER_EVENT = 'SERVER_USER_EVENT';

const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN';
const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';

function getOnlineUser() {
  return connectionList
  .filter(item => {
    return item.userName !== '';
  })
  .map(item => {
    return {
      userName: item.userName
    };
  });
}

function setUserName(connection, userName) {
  connectionList.forEach(item => {
    if (item.connection.id === connection.id) {
      item.userName = userName;
    }
  });
}

function updateUsers(connection) {
  connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});  
}

io.on('connection', function (connection) {

  connectionList.push({
    connection: connection,
    userName: ''
  });
  
  // 连接上的用户,推送在线用户列表
  // connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
  updateUsers(connection);

  connection.on(CLIENT_USER_EVENT, function(jsonString) {
    const msg =  *** ON.parse(jsonString);
    const {type, payload} = msg;

    if (type === CLIENT_USER_EVENT_LOGIN) {
      setUserName(connection, payload.loginName);
      connectionList.forEach(item => {
        // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
        updateUsers(item.connection);
      });
    }
  });

  connection.on(CLIENT_RTC_EVENT, function(jsonString) {
    const msg =  *** ON.parse(jsonString);
    const {payload} = msg;
    const target = payload.target;

    const targetConn = connectionList.find(item => {
      return item.userName === target;
    });
    if (targetConn) {
      targetConn.connection.emit(SERVER_RTC_EVENT, msg);
    }
  });

  connection.on('disconnect', function () {
    connectionList = connectionList.filter(item => {
      return item.connection.id !== connection.id;
    });
    connectionList.forEach(item => {
      // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
      updateUsers(item.connection);
    });    
  });
});


写在后面

WebRTC的API异常多,由于WebRTC自己就比较复杂,随着时间的推移,WebRTC的某些API(包罗某些协议细节)也在改动或被废弃,这其中也有向后兼容带来的复杂性,好比内陆视频采集后加入传输流,可以接纳 addStream 或 addTrack 或 addTransceiver,再好比会话形貌版本从plan-b迁移到unified-plan。

建议亲自动手撸一遍代码,加深领会。


相关链接

2019.08.02-video-talk-using-webrtc

https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection

onremotestream called twice for each remote stream

原文:https://segmentfault.com/a/1190000019970102


思源资源网:分类流动

1.阿里云: 本站现在使用的是阿里云主机,平安/可靠/稳固。点击领取2000米代金券、领会最新阿里云产物的种种优惠流动点击进入

返回列表

上一篇:notice.js

下一篇:CoffeeScript

相关文章

黑客接单支持当面交易_网站入侵

这种传达方法的呈现,导致2018年11月GandCrab勒索病毒忽然成规划的迸发,令许多用户遭受进犯。 在315晚会上,关于智能机器人打电话这部分大部分都是那些厂家在宣扬“运用客户广泛”“打电话多”等...

产品分析 | Bilibili,干杯!! (゜-゜)つロ

产品分析 | Bilibili,干杯!! (゜-゜)つロ

编辑导读:作为一个依靠二次元起家的内容社区,B站近几年正在不绝“开疆拓土”,《后浪》、《入海》更是引起了现象级的接头。可是跟着而来的,是掌声与骂声,尚有其他平台对up主的虎视眈眈,B站正站在转型的十字...

唐三代而亡(福安唐三代)

唐太宗时代,社会上出现了一本《秘记》,里面说:“唐代在三世之后,将有女主武王取代其天下。”李世民继位之后,左武卫将军、武连县公、武定人李君羡, 这是谣言。如果非要给出一个理由的话,那就是武则天太优秀了...

手抄报模版(手抄报模板可爱)

手抄报模版(手抄报模板可爱)

本文导读目录: 1、关于防电信诈骗的手抄报的内容 2、我的精彩假期手抄报(图) 3、手抄报要怎么做? 4、手抄报多图不多字 少一点字 5、怎样做手抄报?还有怎样画手抄报? 6、扫黄...

查询我老婆全部的微信聊天记录

孩子牙齿的问题如果特别严重的话,还是建议拍牙片的,不过有的家长比较担心会不会有辐射呢,孩子拍牙片会有拿出拍片要求呢,友谊长存小编就来说说吧。 自然界中辐射无处不在,泥土、空气、岩石、宇宙都存在小剂量...

找黑客盗一个普通QQ要多少钱(黑客技术入门盗号)

每日好文 有钱去收购它,破腾讯也该换换了. 如果这个号码是你的你想要回的话打电话去腾讯公司吧提供一定的证明就可以恢复密码 你可以试试申诉…………那点分请不动黑客的 盗指定QQ黑客根本不干你觉得有这么...