相关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}], ${JSON.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, JSON.stringify(msg));
} /**
* 发送RTC相关消息给服务器
* @param {Object} msg 格式如{ type: 'xx', payload: {} }
*/
function sendRTCEvent(msg) {
socket.emit(CLIENT_RTC_EVENT, JSON.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 = JSON.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 = JSON.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

WebRTC:一个视频聊天的简单例子的更多相关文章

  1. ICE第二篇--一个"hello world"的简单例子

    1 本文介绍一个hello world输出的例子. ice应用的步骤如下: 1. 编写 Slice 定义并编译它. 2. 编写服务器并编译它. 3. 编写客户并编译它. 基本框架图示: 本文代码图示: ...

  2. 使用WebRTC搭建前端视频聊天室——入门篇

    http://segmentfault.com/a/1190000000436544 什么是WebRTC? 众所周知,浏览器本身不支持相互之间直接建立信道进行通信,都是通过服务器进行中转.比如现在有两 ...

  3. 在Ubuntu上部署一个基于webrtc的多人视频聊天服务

    最近研究webrtc视频直播技术,网上找了些教程最终都不太能顺利跑起来的,可能是文章写的比较老,使用的一些开源组件已经更新了,有些配置已经不太一样了,所以按照以前的步骤会有问题.折腾了一阵终于跑起来了 ...

  4. 使用WebRTC搭建前端视频聊天室——信令篇

    博客原文地址 建议看这篇之前先看一下使用WebRTC搭建前端视频聊天室——入门篇 如果需要搭建实例的话可以参照SkyRTC-demo:github地址 其中使用了两个库:SkyRTC(github地址 ...

  5. WebRTC实现网页版多人视频聊天室

    因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果. 话说 WebRTC Real Time Communication 简称 RTC,是谷歌若干年前收购的一项技术 ...

  6. WebRTC搭建前端视频聊天室——信令篇

    这篇文章讲述了WebRTC中所涉及的信令交换以及聊天室中的信令交换,主要内容来自WebRTC in the real world: STUN, TURN and signaling,我在这里提取出的一 ...

  7. 使用WebRTC搭建前端视频聊天室——点对点通信篇

    WebRTC给我们带来了浏览器中的视频.音频聊天体验.但个人认为,它最实用的特性莫过于DataChannel——在浏览器之间建立一个点对点的数据通道.在DataChannel之前,浏览器到浏览器的数据 ...

  8. 一个简单例子:贫血模型or领域模型

    转:一个简单例子:贫血模型or领域模型 贫血模型 我们首先用贫血模型来实现.所谓贫血模型就是模型对象之间存在完整的关联(可能存在多余的关联),但是对象除了get和set方外外几乎就没有其它的方法,整个 ...

  9. 5分钟快速打造WebRTC视频聊天

    百度一下WebRTC,我想也是一堆.本以为用这位朋友( 搭建WebRtc环境 )的SkyRTC-demo 就可以一马平川的实现聊天,结果折腾了半天,文本信息都发不出去,更别说视频了.于是自己动手. 想 ...

随机推荐

  1. 2018.8.19 2018暑假集训之maxnum

    昨天去做志愿服务去了没写成Q_Q 今天再来一道 先放题面描述 设有n个正整数(n<=20),将它们联成一排,组成一个最大的多位数 设有n个正整数(n<=20),将它们联成一排,组成一个最大 ...

  2. ORACLE常用函数的使用方法

    ORACLE常用函数的使用方法 1. 字符串函数 (1) length(); 获取字符长度SELECT LENGTH('中国') FROM PLATFORM_METAINFO_TABLES WHERE ...

  3. 对于Spring中AOP,DI,IoC概念的理解

    IOC IoC(inversion of Control),控制反转.就好像敏捷开发和SCRUM一样,不是什么技术,而是一种方法论,一种工程化的思想.使用IoC的思想意味着你将设计好的对象交给容器控制 ...

  4. 基于SpringBoot-Dubbo的微服务快速开发框架

    简介: 基于Dubbo的分布式/微服务基础框架,为前端提供脚手架开发服务,结合前一篇--Web AP快速开发基础框架,可快速上手基于Dubbo的分布式服务开发,项目代码: https://github ...

  5. Linux 文件系统的基本结构

    Linux文件系统为一个倒置的树状结构,所有文件或文件夹均包含在一个根目录/中. Linux系统严格区分大小写所以在Linux中:一个名为“A”的文件夹和一个名为“a”的文件夹是不同的两个文件夹,这点 ...

  6. python接口自动化(三十一)--html测试报告通过邮件发出去——下(详解)

    简介 本篇总结了 QQ (SSL)邮箱和 163(非SSL) 邮箱发送邮件,专治各种不行,总之看完这篇以后麻麻再也不用担心我的邮件收不到了.以下代码兼容 python2 和 python3,运行无异常 ...

  7. MyBatis 使用枚举或其他对象

    From<Mybatis从入门到精通> 1.笔记: <!-- 6.3 使用枚举或者其他对象 6.3.1 使用MyBatis提供的枚举处理器 不懂: 因为枚举除了本身的字面值外,还可以 ...

  8. 【HDU - 1043】Eight(反向bfs+康托展开)

    Eight Descriptions: 简单介绍一下八数码问题:在一个3×3的九宫格上,填有1~8八个数字,空余一个位置,例如下图: 1 2 3 4 5 6 7 8   在上图中,由于右下角位置是空的 ...

  9. UVA10375 选择与除法 Choose and divide 题解

    题目链接: https://www.luogu.org/problemnew/show/UVA10375 分析: 这道题可以用唯一分解定理来做. 什么是唯一分解定理?百度即可,这里也简介一下. 对于任 ...

  10. java中this 和 super关键字的作用

    emmmmmm也真的是好久没有写过java了,因为项目需要, 最近又必须重新拾起来了,虽然好多东西也都忘得差不多了.... 然后发现 竟然把super和this傻傻分不清.... 开个帖子记录一下: ...