程序
nodejs,jquery
定义
WebRTC(Web Real-Time Communication) 网页即时通信 ,是一个支持网页浏览器进行实时语音、视频对话的API。
于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准
核心API
- getUserMedia:可以获取本地的媒体流,一个流包含几个轨道,比如视频和音频轨道。
- RTCPeerConnection:用于建立 P2P 连接以及传输多媒体数据。
- RTCDataChannel:建立一个双向通信的数据通道,可以传递多种数据类型。
获取本地媒体流
通过 getUserMedia 函数,我们可以发起获取本地媒体流的请求:
navigator.getUserMedia(constraints, successCallback, errorCallback);
函数有三个参数,分别是约束条件,成功的回调和失败的回调。成功获取媒体流后,媒体流可以提供给本地的音频或视频元素进行播放、后期处理的 JavaScript 代理,或者发送给另一端。
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="page-container">
<div class="message-box">
<div class="message-list"></div>
<div class="send-box">
<textarea class="send-content"></textarea>
<button class="sendbtn">发送</button>
</div>
</div>
<div class="user-box">
<video id="local-video" autoplay class="local-video"></video>
<video id="remote-video" autoplay class="remote-video"></video>
<p class="title">在线用户</p>
<ul class="user-list"></ul>
</div>
<div class="mask">
<div class="mask-content">
<input class="myname" type="text" placeholder="输入用户名加入房间">
<button class="add-room">加入</button>
</div>
</div>
<div class="video-box">
</div>
</div>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/socket.io/2.1.1/socket.io.js"></script>
<script>
class Chat{
constructor({ calledHandle, host, socketPath, getCallReject } = {}) {
this.host = host;
this.socketPath = socketPath;
this.socket = null;
this.calledHandle =calledHandle;
this.getCallReject = getCallReject;
this.peer = null
this.localMedia = null
}
async init(){
this.socket = await this.connentSocket();
return this;
}
async connentSocket(){
if(this.socket){
return this.socket;
}
return new Promise((resolve, reject) => {
let socket = io(this.host,{
path: this.socketPath
})
socket.on('connect', () => {
console.log('连接成功');
resolve(socket);
})
socket.on('connect_error', e => {
console.log('连接失败');
throw e;
reject()
})
/*
* @desc 呼叫被接受
* @example
* setRemoteDescription方法,改变与连接相关的远端描述。这个描述定义了连接的属性,例如:连接的编码方式。
* 连接会受到它的改变的影响,而且连接必须能同时支持新的以及旧的描述。
* 这个方法可以接收三个参数,一个RTCSessionDescription 对象包含设置信息,
* 还有两个回调函数,它们分别是方法调用成功以及失败的回调函数。
* */
socket.on('answer', ({answer}) => {
this.peer && this.peer.setRemoteDescription(answer);
})
/*
* @desc 被呼叫
* */
socket.on('called', (callingInfo) => {
this.called && this.called(callingInfo);
})
/*
* @desc 呼叫被拒绝
* */
socket.on('callRejected', () => {
this.getCallReject && this.getCallReject()
})
socket.on('iceCandidate', ({ iceCandidate }) => {
this.peer && this.peer.addIceCandidate(new RTCIceCandidate(iceCandidate))
})
})
}
addEvent(name, cb) {
if (!this.socket) return
this.socket.on(name, data => {
cb.call(this, data)
})
}
sendMessage(name, data) {
if (!this.socket) return
this.socket.emit(name, data)
}
/*
* @desc 获取本地媒体流
* */
async getLocalMedia(){
let localMedia = await navigator.mediaDevices
.getUserMedia({ video: { facingMode: "user" }, audio: true })
.catch(e => {
console.log(e)
})
this.localMedia = localMedia;
return this;
}
/*
* @desc 设置媒体流到video
* */
setMediaTo(eleId, media) {
document.getElementById(eleId).srcObject = media;
}
/*
* @dsec 被叫响应
* */
called(callingInfo){
this.calledHandle && this.calledHandle(callingInfo);
}
/*
* @desc 创建RTC
* */
createLoacalPeer(){
var PeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.RTCPeerConnection;
this.peer = new PeerConnection()
return this;
}
/*
* @desc 将媒体流加入通信
* */
addTrack(){
if (!this.peer || !this.localMedia) return
this.peer.addStream(this.localMedia);
return this;
}
/*
* @desc 创建 SDP offer
* */
async createOffer(cb) {
if (!this.peer) return
let offer = await this.peer.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
this.peer.setLocalDescription(offer)
cb && cb(offer)
return this
}
async createAnswer(offer, cb) {
if (!this.peer) return
this.peer.setRemoteDescription(offer)
let answer = await this.peer.createAnswer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
this.peer.setLocalDescription(answer)
cb && cb(answer)
return this
}
listenerAddStream(cb){
this.peer.addEventListener('addstream', event => {
console.log('addstream事件触发', event.stream);
cb && cb(event.stream);
})
return this;
}
/*
* @desc 监听候选加入
* */
listenerCandidateAdd(cb) {
this.peer.addEventListener('icecandidate', event => {
let iceCandidate = event.candidate;
if (iceCandidate) {
console.log('发送candidate给远端');
cb && cb(iceCandidate);
}
})
return this
}
// 检测ice协商过程
listenerGatheringstatechange () {
return this;
}
// 关闭RTC
closeRTC() {
// ....
}
}
</script>
<script>
$(function () {
let chat = new Chat({
host: 'http://127.0.0.1:3003',
socketPath:'/websocket',
calledHandle: calledHandle,
getCallReject: getCallReject
})
// 更新视图
function updateUserList(list) {
$(".user-list").html(list.reduce((temp, li) => {
temp += `<li class="user-li">${li.name}<button data-calling=${li.calling} data-id=${li.id} class=${li.id === this.socket.id || li.calling ? 'cannot-call' : 'can-call'}>通话</button></li>`
return temp
}, ''))
}
// 更新消息li表视图
function updateMessageList(msg) {
$('.message-list').append(`<li class=${msg.userId === this.socket.id ? 'left' : 'right'}>${msg.user}: ${msg.content}</li>`)
}
// 加入房间
$('.add-room').on('click', async () => {
let name = $('.myname').val();
if(!name){
return;
}
$('.mask').fadeOut();
await chat.init();
chat.addEvent('updateUserList', updateUserList);
chat.addEvent('updateMessageList', updateMessageList);
chat.sendMessage('addUser', {name})
})
// 发送消息
$('.sendbtn').on('click', () => {
let sendContent = $('.send-content').val()
if (!sendContent) return
$('.send-content').val('')
chat.sendMessage('sendMessage', { content: sendContent })
})
// 视频
$('.user-list').on('click', '.can-call', async function () {
// 被叫方信息
let calledParty = $(this).data();
if(calledParty.calling){
alert('对方正在通话中');
return;
}
// 初始本地视频
$('.local-video').fadeIn();
await chat.getLocalMedia();
chat.setMediaTo('local-video', chat.localMedia);
chat.createLoacalPeer().listenerGatheringstatechange().addTrack().listenerAddStream(stream => {
$('.remote-video').fadeIn();
chat.setMediaTo('remote-video', stream);
}).listenerCandidateAdd(iceCandidate => {
chat.sendMessage('iceCandidate', { iceCandidate, id: calledParty.id })
}).createOffer(offer => {
chat.sendMessage('offer', { offer, ...calledParty })
})
})
//呼叫被拒绝
function getCallReject() {
chat.closeRTC()
$('.local-video').fadeIn()
console.log('呼叫被拒');
}
// 被叫
async function calledHandle(callingInfo) {
if (!confirm(`是否接受${callingInfo.name}的视频通话`)) {
chat.sendMessage('rejectCall', callingInfo.id)
return
}
$('.local-video').fadeIn()
await chat.getLocalMedia()
chat.setMediaTo('local-video', chat.localMedia)
chat.createLoacalPeer()
.listenerGatheringstatechange()
.addTrack()
.listenerCandidateAdd(function (iceCandidate) {
chat.sendMessage('iceCandidate', { iceCandidate, id: callingInfo.id })
})
.listenerAddStream(function (stream) {
$('.remote-video').fadeIn()
chat.setMediaTo('remote-video', stream)
})
.createAnswer(callingInfo.offer, function (answer) {
chat.sendMessage('answer', { answer, id: callingInfo.id })
})
}
})
</script>
</body>
</html>
node端
- server.js
const SocketIO = require('socket.io')
const socketIO = new SocketIO({
path: '/websocket'
})
let userRoom = {
list: [],
add(user) {
this.list.push(user)
return this
},
del(id) {
this.list = this.list.filter(u => u.id !== id)
return this
},
sendAllUser(name, data) {
this.list.forEach(({ id }) => {
console.log('>>>>>', id)
socketIO.to(id).emit(name, data)
})
return this
},
sendTo(id) {
return (eventName, data) => {
socketIO.to(id).emit(eventName, data)
}
},
findName(id) {
return this.list.find(u => u.id === id).name
}
}
socketIO.on('connection', function(socket) {
console.log('连接加入.', socket.id)
socket.on('addUser', function(data) {
console.log(data.name, '加入房间')
let user = {
id: socket.id,
name: data.name,
calling: false
}
userRoom.add(user).sendAllUser('updateUserList', userRoom.list)
})
socket.on('sendMessage', ({ content }) => {
console.log('转发消息:', content)
userRoom.sendAllUser('updateMessageList', { userId: socket.id, content, user: userRoom.findName(socket.id) })
})
socket.on('iceCandidate', ({ id, iceCandidate }) => {
console.log('转发信道')
userRoom.sendTo(id)('iceCandidate', { iceCandidate, id: socket.id })
})
socket.on('offer', ({id, offer}) => {
console.log('转发offer')
userRoom.sendTo(id)('called', { offer, id: socket.id, name: userRoom.findName(socket.id)})
})
socket.on('answer', ({id, answer}) => {
console.log('接受视频');
userRoom.sendTo(id)('answer', {answer})
})
socket.on('rejectCall', id => {
console.log('转发拒接视频')
userRoom.sendTo(id)('callRejected')
})
socket.on('disconnect', () => {
// 断开删除
console.log('连接断开', socket.id)
userRoom.del(socket.id).sendAllUser('updateUserList', userRoom.list)
})
})
module.exports = socketIO
- www.js
// www.js 这就不关键了
const http = require('http')
const socketIO = require('./server.js')
const server = http.createServer()
socketIO.attach(server)
server.listen(3003, () => {
console.log('server start on 127.0.0.1:3003')
})
启动
node www.js
火狐浏览器打开html