必读:WebRTC API - Web API接口 | MDN
必读:WebRTC的前世今生
Web端用户视频推流使用webRTC方案,使用已有JS SDK(和这个兼容)。
WebRTC由Google主推,全称Web Browser Real Time Communication。目标是希望在Web端进行点对点音视频通信。
整个P2P过程很复杂,但是对于浏览器而言,WebRTC实际上只是提供了三个API:
- MediaStream, 即getUserMedia(
navigator.getUserMedia
),获取媒体数据,如来自用户摄像头和麦克风的音视频流
- RTCPeerConnection,用于peer跟peer之间呼叫和建立连接以便传输音视频数据流;这个不同浏览器的实现不同,官网推荐使用
adapter.js
进行适配
- RTCDataChannel,用于peer跟peer之间传输音视频之外的一般数据。
参考MDN。来自navigator.getUserMedia()
,这个方法接收三个参数:
- 一个约束对象,如
{ audio: false, video: true }
,除了这两种,其他外设也可以作为输入
- 一个成功回调
- 一个失败回调
返回的MediaStream对象有addTrack
, getAudioTracks
, getVideoTracks
等方法。通过这些方法取出的MediaStreamTrack数组代表对应类型的流,可以把取出的这些流导入到<video>
等标签输出。在Chrome或Opera中,URL.createObjectURL()
方法可以转换一个MediaStream到一个Blob URL,可以被设置作为视频的源。除了这种方法,还可以使用AudioContext
API,对音频做处理后输出。
1 2 3 4 5 6 7 8 9 10 11 12 13
| function gotStream(stream) { window.AudioContext = window.AudioContext || window.webkitAudioContext; var audioContext = new AudioContext();
var mediaStreamSource = audioContext.createMediaStreamSource(stream);
mediaStreamSource.connect(audioContext.destination); }
navigator.getUserMedia({audio:true}, gotStream);
|
使用HTTPS请求getUserMedia
会向用户给出一次提示。不建议在HTTP环境下使用。
这里是一个demo,打开console,查看全局变量stream就明白MediaStream结构了。
约束对象中可以商议视频分辨率等信息。它会影响获取到的视频流。
RTCPeerConnection
WebRTC使用RTCPeerConnection在浏览器间传递数据流,但在之前需要有一个交换信令的过程。这个过程不在WebRTC中定义,开发者可以使用任何想用的消息协议,比如WebSocket或XHR轮询什么的。信令过程需要传递三种信息:
- 连接控制信息:初始化或者关闭连接报告错误。
- 网络配置:对于外网,我们电脑的 IP 地址和端口?
- 多媒体数据:使用什么编码解码器,浏览器可以处理什么信息
点对点的连接需要ICE(Interactive Connectivity Establishment)的帮助,ICE靠STUN和TURN服务器处理NAT穿透等复杂问题。起初连接建立在UDP之上,STUN服务器让位于NAT中的client获知自己的公网IP和端口。如果UDP建立失败,考虑TCP连接,再考虑HTTP和HTTPS连接。否则使用TURN服务器做中转工作。
W3C给了RTCPeerConnection的样例,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| var signalingChannel = new SignalingChannel(); var configuration = { "iceServers": [{ "url": "stun:stun.example.org" }] }; var pc;
function start() { pc = new RTCPeerConnection(configuration);
pc.onicecandidate = function (evt) { if (evt.candidate) signalingChannel.send(JSON.stringify({ "candidate": evt.candidate })); };
pc.onnegotiationneeded = function () { pc.createOffer(localDescCreated, logError); }
pc.onaddstream = function (evt) { remoteView.src = URL.createObjectURL(evt.stream); };
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) { selfView.src = URL.createObjectURL(stream); pc.addStream(stream); }, logError); }
function localDescCreated(desc) { pc.setLocalDescription(desc, function () { signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription })); }, logError); }
signalingChannel.onmessage = function (evt) { if (!pc) start();
var message = JSON.parse(evt.data); if (message.sdp) { pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () { if (pc.remoteDescription.type == "offer") pc.createAnswer(localDescCreated, logError); }, logError); } else { pc.addIceCandidate(new RTCIceCandidate(message.candidate)); } };
function logError(error) { log(error.name + ": " + error.message); }
|
在开始建立连接时,调用start()
,创建RTCPeerConnection
对象,接着完成下面步骤:
- 交换网络信息,
onicecandidate
回调在有任何candidate出现时,将通过SignalChannel(使用额外方法创建,如WebSocket)传递给对方。同样地,在通过SignalChannel接收到对方发来的该信息时,加入这个candidate到RTCPeerConnection中。
- 交换多媒体信息,使用SDP(Session Description Protocol)与对端交换多媒体资讯,在
onnegotiationneeded
中,调用createOffer
通过setLocalDescription
创建RTCSessionDecription
对象进行本地存储,并传给对方。接收方通过setRemoteDescription
方法设定remote description。
上述过程称为JavaScript Session Establishment Protocol(JSEP)。一旦这个signaling完成了,数据可以直接的在端到端之间进行数据传输。如果失败了,通过中介服务器relay服务进行转发。
通常RTCPeerConnection的API太复杂,所以有很多在此基础上的库封装出了更加友善的API。
JS-SDK接入流程
参考:Agora:视频通话API
- 准备工作,包括界面绘制等
- 向远端注册当前用户,获取token,为后面做准备
- 使用
createClient()
方法创建客户端
- 指定回调,包括已订阅流、已添加流、移除、失败等生命周期事件的回调
- 初始化客户端,传入appId和成功回调
- 初始化成功后,调用join方法根据获取到的token加入指定房间(原理是WebRTC的stream有id)
- 指定配置创建本地流(getUserMedia),发布本地流,播放本地流
RTMP相关
必读:RTMP H5 直播流技术解析
一次RTMP握手的模拟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| class C { constructor(time, random) { this.time = time || 0; this.random = random || Buffer.alloc(0); }
get C0() { const buf = Buffer.alloc(1, 3); return buf; }
get C1() { const buf = Buffer.alloc(1536); return buf; }
get C2() { let buf = Buffer.alloc(1536); buf.writeUInt32BE(this.time, 4); this.random.copy(buf, 8, 0, 1528); } }
const client = new net.socket(); const RTMP_C = new C();
client.connect({ port: 1935, host: 'example.live.com', () => { console.log('connected'); } });
client.on('data', (data) => { if (!data) { console.warn('Empty Buffer.'); } if (!RTMP_C.S0 && data.length > 0) { RTMP_C.S0 = data.readUInt8(0); data = data.slice(1); }
if (!RTMP_C.S1 && data.length >= 1536) { RTMP_C.time = data.readUInt32BE(0); RTMP_C.random = data.slice(8, 1536); RTMP_C.S1 = true; data = data.slice(1536) console.log('send C2'); client.write(RTMP_C.C2); }
if (!RTMP_C.S2 && data.length >= 1536) { RTMP_C.S2 = true; data = data.slice(1536); } });
|