关于这个应用的具体教程详见“一个简单的视频聊天客户端”章节。
1.MediaStream
(别名getUserMedia)
2.RTCPeerConnection
3.RTCDataChannel
MediaStream:
从设备获取数据流,比如说摄像头和麦克风。RTCDataChannel
:p2p通信。function gotStream(stream) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
// Create an AudioNode from the stream
var mediaStreamSource = audioContext.createMediaStreamSource(stream);
// Connect it to destination to hear yourself
// or any other node for processing!
mediaStreamSource.connect(audioContext.destination);
}
navigator.getUserMedia({audio:true}, gotStream);
在manifest中添加audioCapture和videoCapture权限可以在加载的时候得到(仅一次)授权,毕竟加载之后用户不会再有对摄像头或麦克风的访问请求。ASCII Camera使用Canvas API来生成ASCII码的图片。
navigator.getUserMedia error:
NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}
var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;
// run start(true) to initiate a call
function start(isCaller) {
pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};
// get the local stream, show it in the local video element and send it
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer(gotDescription);
else
pc.createAnswer(pc.remoteDescription, gotDescription);
function gotDescription(desc) {
pc.setLocalDescription(desc);
signalingChannel.send(JSON.stringify({ "sdp": desc }));
}
});
}
signalingChannel.onmessage = function (evt) {
if (!pc)
start(false);
var signal = JSON.parse(evt.data);
if (signal.sdp)
pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else
pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};
首先,Alice和Bob交换网络信息,‘finding candidates’表示通过
ICE framework
查找网络接口和端口。
v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126
// ...
a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810
交换网络和媒体信息可以同时进行,但这两个过程必须在音视频流开始传输之前完成。
上述的offer/answer架构被称为JSEP(JavaScript Session Establishment Protocol),JSEP架构如下所示:
一旦信令过程成功,就可以直接进行Caller和callee之间p2p的数据流传输了。下图是WebRTC的架构图,标明了RTCPeerConnection扮演的角色。你可能注意到了,绿色部分是相当复杂的。
从JavaScript的角度来看,理解这个图最重要的是理解RTCpeerConnection这一部分。WebRTC对编解码器和协议做了大量的工作,使实时通信成为可能,甚至在一些不可靠的网络中:// servers is an optional config file (see TURN and STUN discussion below)
pc1 = new webkitRTCPeerConnection(servers);
// ...
pc1.addStream(localStream);
2.创建一个offer,并将它设置为pc1的本地会话描述,设置为pc2的远程会话描述。可以直接在代码中设置,不需要使用信令,因为caller和callee在同一个网页中。
pc1.createOffer(gotDescription1);
//...
function gotDescription1(desc){
pc1.setLocalDescription(desc);
trace("Offer from pc1 \n" + desc.sdp);
pc2.setRemoteDescription(desc);
pc2.createAnswer(gotDescription2);
}
callee
pc2 = new webkitRTCPeerConnection(servers);
pc2.onaddstream = gotRemoteStream;
//...
function gotRemoteStream(e){
vid2.src = URL.createObjectURL(e.stream);
}
如果UDP传输失败,ICE会尝试TCP:首先是HTTP,然后才会选择 HTTPS。如果直接连接失败,通常因为企业的NAT穿透和防火墙,此时ICE使用中继(Relay)服务器。换句话说,ICE首先使用STUN和UDP直接连接两端,失败之后返回中继服务器。‘finding cadidates’就是寻找网络接口和端口的过程。
WebRTC工程师Justin Uberti在幻灯片2013 Google I/O WebRTC presentation中提供了许多关于ICE、STUN和TURN的信息。下面我们详细的过一遍代码。
如何开始
function initialize() {
console.log("Initializing; room=99688636.");
card = document.getElementById("card");
localVideo = document.getElementById("localVideo");
miniVideo = document.getElementById("miniVideo");
remoteVideo = document.getElementById("remoteVideo");
resetStatus();
openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');
doGetUserMedia();
}
需要注意的是,变量room和openChannel()参数的值都是由Google App Engine应用自身提供的。
查看一下index.html template 就知道该赋什么值了。
function openChannel(channelToken) {
console.log("Opening channel.");
var channel = new goog.appengine.Channel(channelToken);
var handler = {
'onopen': onChannelOpened,
'onmessage': onChannelMessage,
'onerror': onChannelError,
'onclose': onChannelClosed
};
socket = channel.open(handler);
}
关于信令,本例使用的是Google App Engine Channel API,这使得JavaScritp客户端无需轮询就能实现消息传输。
使用Channel API建立通道的流程大致如下:5.客户端A打开socket并监听服务器上建立的通道。
发送消息的流程大致如下:4.客户端A的onmessage回调函数被调用。
重申一次,信令传输机制是由开发者选择的。WebRTC并没有指定信令机制。本例的Channel API能被其他的方式取代,比如WebSocket。function onUserMediaSuccess(stream) {
console.log("User has granted access to local media.");
// Call the polyfill wrapper to attach the media stream to this element.
attachMediaStream(localVideo, stream);
localVideo.style.opacity = 1;
localStream = stream;
// Caller creates PeerConnection.
if (initiator) maybeStart();
}
这样一来,本地摄像头就能显示在localVideo元素中了。
function maybeStart() {
if (!started && localStream && channelReady) {
// ...
createPeerConnection();
// ...
pc.addStream(localStream);
started = true;
// Caller initiates offer to peer.
if (initiator)
doCall();
}
}
该函数使用了一种巧妙的结构,可以工作于多个异步回调:maybeStart()可能被任何函数调用,但是只有当localStream被定义、channelReady为true且通信还未开始的情况下,maybeStart()才会运行。
因此,当连接还未建立,本地流已经可用,且信令通道已经准备好时,连接才会创建并加载本地视频流。接着started被设置为true。所以连接不会被创建多次 。
function createPeerConnection() {
var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
try {
// Create an RTCPeerConnection via the polyfill (adapter.js).
pc = new RTCPeerConnection(pc_config);
pc.onicecandidate = onIceCandidate;
console.log("Created RTCPeerConnnection with config:\n" + " \"" +
JSON.stringify(pc_config) + "\".");
} catch (e) {
console.log("Failed to create PeerConnection, exception: " + e.message);
alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
return;
}
pc.onconnecting = onSessionConnecting;
pc.onopen = onSessionOpened;
pc.onaddstream = onRemoteStreamAdded;
pc.onremovestream = onRemoteStreamRemoved;
}
这段代码的目的是使用STUN服务器建立一个连接,并将onIceCandidate()作为回调函数。
然后给RTCPeerConnection每个事件指定处理器(函数):当会话连接或打开,当远程流被加载或移除。
在本例中,这些处理器只是记录了状态消息——除了onRemoteStreamAdded(),它给remoteVideo元素设置了数据源。
function onRemoteStreamAdded(event) {
// ...
miniVideo.src = localVideo.src;
attachMediaStream(remoteVideo, event.stream);
remoteStream = event.stream;
waitForRemoteVideo();
}
一旦createPeerConnection()在maybeStart()中被调用,就会发起通话,创建Offer并发送消息给对端。
function doCall() {
console.log("Sending offer to peer.");
pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}
这里的offer创建过程类似于上面无信令的例子。但是,除此之外,一条消息被发送到了对端,详见setLocalAndSendMessage():
function setLocalAndSendMessage(sessionDescription) {
// Set Opus as the preferred codec in SDP if Opus is present.
sessionDescription.sdp = preferOpus(sessionDescription.sdp);
pc.setLocalDescription(sessionDescription);
sendMessage(sessionDescription);
}
用Channel API传输信令
function onIceCandidate(event) {
if (event.candidate) {
sendMessage({type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate});
} else {
console.log("End of candidates.");
}
}
从客户端到服务器的消息外传,是通过sendMessage()方法内的XHR请求实现的。
function sendMessage(message) {
var msgString = JSON.stringify(message);
console.log('C->S: ' + msgString);
path = '/message?r=99688636' + '&u=92246248';
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send(msgString);
}
XHR多用于从客户端发送信令消息到服务端,但是某些机制需要用来实现服务端到客户端的消息传输:本例用的是Google App Engine Channel API。
来自此API的消息会传递到processSignalingMessage():
function processSignalingMessage(message) {
var msg = JSON.parse(message);
if (msg.type === 'offer') {
// Callee creates PeerConnection
if (!initiator && !started)
maybeStart();
pc.setRemoteDescription(new RTCSessionDescription(msg));
doAnswer();
} else if (msg.type === 'answer' && started) {
pc.setRemoteDescription(new RTCSessionDescription(msg));
} else if (msg.type === 'candidate' && started) {
var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,
candidate:msg.candidate});
pc.addIceCandidate(candidate);
} else if (msg.type === 'bye' && started) {
onRemoteHangup();
}
}
如果消息是来自对端的answer(offer的回应),RTCPeerConnection设置远程会话描述,通信开始。
如果消息是offer(来自callee),RTCPeerConnection设置远程会话描述,发送answer给callee,然后调用RTCPeerConnection的startIce()方法发起连接。
function doAnswer() {
console.log("Sending answer to peer.");
pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}
于是乎,caller和callee都发现了对方并交换相关信息,会话被初始化,实时数据通信可以开始了。
WebRTC目前只实现了一对一的通信,但是可用于更复杂的网络环境:比如,多个peer各自直接通信,即p2p;或者通过MCU(Multipoint Control Unit)服务器来实现流的转发、合成或音视频的录制。
许多WebRTC应用只演示了浏览器间的通信,但是通过网关服务器可以实现WebRTC与telephones(别名PSTN)和VOIP系统直接的通信。 2012年5月,Doubango Telecom开源了sipml5 SIP client,该客户端基于WebRTC和WebSocket,能实现浏览器和IOS或Android应用之间的视频通话。var pc = new webkitRTCPeerConnection(servers,
{optional: [{RtpDataChannels: true}]});
pc.ondatachannel = function(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
document.querySelector("div#receive").innerHTML = event.data;
};
};
sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});
document.querySelector("button#send").onclick = function (){
var data = document.querySelector("textarea#send").value;
sendChannel.send(data);
};
因为是浏览器间的直接通信,所以RTCDataChannel要比WebSocket快得多,即使通信用到了中继服务器。
更多关于RTCDataChannel的信息,可以参考IETF的draft protocol spec。
实时通信应用或插件会在许多方面忽视了安全性:
1.浏览器之间、浏览器与服务器之间的音视频或其他数据没有加密。 关于流媒体安全的讨论超出了本文的范畴。更多信息可参考IETF的WebRTC Security Architecture。
原文链接:https://www.html5rocks.com/en/tutorials/webrtc/basics/