WebRTC实现浏览器上的音视频通信

概述

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。

webRTC协议介绍

ice

交互式连接机构 (ICE)是一个框架,允许您的网络浏览器与同行连接。有许多原因,为什么从对等A到同行B的直升连接将不起作用。它需要绕过防火墙,防止打开连接,给您一个独特的地址,如果像大多数情况下,您的设备没有公共 IP 地址,并通过服务器中继数据,如果你的路由器不允许您直接与对等连接。ICE 使用 STUN 和/或 TURN 服务器来实现此目的,如下所述。

STUN

NAT 的会话横向公用设施 (STUN)是一个协议,以发现您的公共地址,并确定任何限制,在你的路由器,将阻止与同行的直接连接。

客户端将向 Internet 上的 STUN 服务器发送请求,该服务器将回复客户的公共地址,以及路由器 NAT 后面是否访问客户端。
 

NAT

网络地址翻译 (NAT)用于为您的设备提供公共 IP 地址。路由器将具有公共 IP 地址,连接到路由器的每个设备都将有一个私有 IP 地址。请求将从设备的私有 IP 转换为路由器的公共 IP,并具有独特的端口。这样,您不需要每个设备都有独特的公共 IP,但仍可以在互联网上发现。

某些路由器将限制谁可以连接到网络上的设备。这可能意味着,即使我们有 STUN 服务器找到的公共 IP 地址,也没有人能够创建连接。在这种情况下,我们需要转向转向。

TURN

一些使用 NAT 的路由器采用一种称为"对称 NAT"的限制。这意味着路由器将只接受您以前连接过的对等的连接。

使用 NAT 周围的中继进行横向(转)旨在通过打开与 TURN 服务器的连接并通过该服务器中继所有信息来绕过对称 NAT 限制。您将创建与 TURN 服务器的连接,并告诉所有对应方将数据包发送到服务器,然后转发给服务器。这显然伴随着一些开销,所以它只使用,如果没有其他选择。
 

SDP

会话描述协议 (SDP)是描述连接的多媒体内容的标准,如分辨率、格式、编解码器、加密等,以便在数据传输时两个对等可以相互理解。这在本质上是描述内容而不是媒体内容本身的元数据。

因此,从技术上讲,SDP 并不是真正的协议,而是用于描述设备之间共享介质连接的数据格式。

记录 SDP 远远超出了此文档的范围;然而,这里有一些值得注意的事情。

webRTC API

WebRTC主要让浏览器具备三个作用。

  • 获取音频和视频
  • 进行音频和视频通信
  • 进行任意数据的通信

WebRTC共分成三个API,分别对应上面三个作用。

  • MediaStream (又称getUserMedia)
  • RTCPeerConnection
  • RTCDataChannel

getUserMedia

概述

navigator.getUserMedia方法目前主要用于,在浏览器中获取音频(通过麦克风)和视频(通过摄像头),将来可以用于获取任意数据流,比如光盘和传感器。

下面的代码用于检查浏览器是否支持getUserMedia方法。

navigator.getUserMedia  = navigator.getUserMedia ||
                          navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia;

if (navigator.getUserMedia) {
    // 支持
} else {
    // 不支持
}

Chrome 21, Opera 18和Firefox 17,支持该方法。目前,IE还不支持,上面代码中的msGetUserMedia,只是为了确保将来的兼容。

getUserMedia方法接受三个参数。

navigator.getUserMedia({
    video: true, 
    audio: true
}, onSuccess, onError);

getUserMedia的第一个参数是一个对象,表示要获取哪些多媒体设备,上面的代码表示获取摄像头和麦克风;onSuccess是一个回调函数,在获取多媒体设备成功时调用;onError也是一个回调函数,在取多媒体设备失败时调用。

下面是一个例子。

var constraints = {video: true};

function onSuccess(stream) {
  var video = document.querySelector("video");
  video.src = window.URL.createObjectURL(stream);
}

function onError(error) {
  console.log("navigator.getUserMedia error: ", error);
}

navigator.getUserMedia(constraints, onSuccess, onError);

如果网页使用了getUserMedia方法,浏览器就会询问用户,是否同意浏览器调用麦克风或摄像头。如果用户同意,就调用回调函数onSuccess;如果用户拒绝,就调用回调函数onError。

onSuccess回调函数的参数是一个数据流对象stream。stream.getAudioTracks方法和stream.getVideoTracks方法,分别返回一个数组,其成员是数据流包含的音轨和视轨(track)。使用的声音源和摄影头的数量,决定音轨和视轨的数量。比如,如果只使用一个摄像头获取视频,且不获取音频,那么视轨的数量为1,音轨的数量为0。每个音轨和视轨,有一个kind属性,表示种类(video或者audio),和一个label属性(比如FaceTime HD Camera (Built-in))。

onError回调函数接受一个Error对象作为参数。Error对象的code属性有如下取值,说明错误的类型。

  • PERMISSION_DENIED:用户拒绝提供信息。
  • NOT_SUPPORTED_ERROR:浏览器不支持硬件设备。
  • MANDATORY_UNSATISFIED_ERROR:无法发现指定的硬件设备。

范例:获取摄像头

下面通过getUserMedia方法,将摄像头拍摄的图像展示在网页上。

首先,需要先在网页上放置一个video元素。图像就展示在这个元素中。

<video id="webcam"></video>

然后,用代码获取这个元素。

function onSuccess(stream) {
    var video = document.getElementById('webcam');
}

接着,将这个元素的src属性绑定数据流,摄影头拍摄的图像就可以显示了。

function onSuccess(stream) {
    var video = document.getElementById('webcam');
    if (window.URL) {
	    video.src = window.URL.createObjectURL(stream);
	} else {
		video.src = stream;
	}

	video.autoplay = true; 
	// 或者 video.play();
}

if (navigator.getUserMedia) {
	navigator.getUserMedia({video:true}, onSuccess);
} else {
	document.getElementById('webcam').src = 'somevideo.mp4';
}

在Chrome和Opera中,URL.createObjectURL方法将媒体数据流(MediaStream)转为一个二进制对象的URL(Blob URL),该URL可以作为video元素的src属性的值。 在Firefox中,媒体数据流可以直接作为src属性的值。Chrome和Opera还允许getUserMedia获取的音频数据,直接作为audio或者video元素的值,也就是说如果还获取了音频,上面代码播放出来的视频是有声音的。

获取摄像头的主要用途之一,是让用户使用摄影头为自己拍照。Canvas API有一个ctx.drawImage(video, 0, 0)方法,可以将视频的一个帧转为canvas元素。这使得截屏变得非常容易。

<video autoplay></video>
<img src="">
<canvas style="display:none;"></canvas>

<script>
  var video = document.querySelector('video');
  var canvas = document.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  var localMediaStream = null;

  function snapshot() {
    if (localMediaStream) {
      ctx.drawImage(video, 0, 0);
      // “image/webp”对Chrome有效,
      // 其他浏览器自动降为image/png
      document.querySelector('img').src = canvas.toDataURL('image/webp');
    }
  }

  video.addEventListener('click', snapshot, false);

  navigator.getUserMedia({video: true}, function(stream) {
    video.src = window.URL.createObjectURL(stream);
    localMediaStream = stream;
  }, errorCallback);
</script>

范例:捕获麦克风声音

通过浏览器捕获声音,需要借助Web Audio API。

window.AudioContext = window.AudioContext ||
                      window.webkitAudioContext;

var context = new AudioContext();

function onSuccess(stream) {
	var audioInput = context.createMediaStreamSource(stream);
	audioInput.connect(context.destination);
}

navigator.getUserMedia({audio:true}, onSuccess);

捕获的限定条件

getUserMedia方法的第一个参数,除了指定捕获对象之外,还可以指定一些限制条件,比如限定只能录制高清(或者VGA标准)的视频。

var hdConstraints = {
  video: {
    mandatory: {
      minWidth: 1280,
      minHeight: 720
    }
  }
};

navigator.getUserMedia(hdConstraints, onSuccess, onError);

var vgaConstraints = {
  video: {
    mandatory: {
      maxWidth: 640,
      maxHeight: 360
    }
  }
};

navigator.getUserMedia(vgaConstraints, onSuccess, onError);

MediaStreamTrack.getSources()

如果本机有多个摄像头/麦克风,这时就需要使用MediaStreamTrack.getSources方法指定,到底使用哪一个摄像头/麦克风。

MediaStreamTrack.getSources(function(sourceInfos) {
  var audioSource = null;
  var videoSource = null;

  for (var i = 0; i != sourceInfos.length; ++i) {
    var sourceInfo = sourceInfos[i];
    if (sourceInfo.kind === 'audio') {
      console.log(sourceInfo.id, sourceInfo.label || 'microphone');

      audioSource = sourceInfo.id;
    } else if (sourceInfo.kind === 'video') {
      console.log(sourceInfo.id, sourceInfo.label || 'camera');

      videoSource = sourceInfo.id;
    } else {
      console.log('Some other kind of source: ', sourceInfo);
    }
  }

  sourceSelected(audioSource, videoSource);
});

function sourceSelected(audioSource, videoSource) {
  var constraints = {
    audio: {
      optional: [{sourceId: audioSource}]
    },
    video: {
      optional: [{sourceId: videoSource}]
    }
  };

  navigator.getUserMedia(constraints, onSuccess, onError);
}

上面代码表示,MediaStreamTrack.getSources方法的回调函数,可以得到一个本机的摄像头和麦克风的列表,然后指定使用最后一个摄像头和麦克风。

RTCPeerConnectionl,RTCDataChannel

RTCPeerConnectionl

RTCPeerConnection的作用是在浏览器之间建立数据的“点对点”(peer to peer)通信,也就是将浏览器获取的麦克风或摄像头数据,传播给另一个浏览器。这里面包含了很多复杂的工作,比如信号处理、多媒体编码/解码、点对点通信、数据安全、带宽管理等等。

不同客户端之间的音频/视频传递,是不用通过服务器的。但是,两个客户端之间建立联系,需要通过服务器。服务器主要转递两种数据。

  • 通信内容的元数据:打开/关闭对话(session)的命令、媒体文件的元数据(编码格式、媒体类型和带宽)等。
  • 网络通信的元数据:IP地址、NAT网络地址翻译和防火墙等。

WebRTC协议没有规定与服务器的通信方式,因此可以采用各种方式,比如WebSocket。通过服务器,两个客户端按照Session Description Protocol(SDP协议)交换双方的元数据。

下面是一个示例。

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));
};

RTCPeerConnection带有浏览器前缀,Chrome浏览器中为webkitRTCPeerConnection,Firefox浏览器中为

mozRTCPeerConnection。Google维护一个函数库adapter.js,用来抽象掉浏览器之间的差异。

RTCDataChannel

RTCDataChannel的作用是在点对点之间,传播任意数据。它的API与WebSockets的API相同。

下面是一个示例。

 

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);
};

Chrome 25、Opera 18和Firefox 22支持RTCDataChannel。

外部函数库

由于这两个API比较复杂,一般采用外部函数库进行操作。目前,视频聊天的函数库有SimpleWebRTCeasyRTCwebRTC.io,点对点通信的函数库有PeerJSSharefest

下面是SimpleWebRTC的示例。
 

var webrtc = new WebRTC({
  localVideoEl: 'localVideo',
  remoteVideosEl: 'remoteVideos',
  autoRequestMedia: true
});

webrtc.on('readyToCall', function () {
    webrtc.joinRoom('My room name');
});

下面是PeerJS的示例。

var peer = new Peer('someid', {key: 'apikey'});
peer.on('connection', function(conn) {
  conn.on('data', function(data){
    // Will print 'hi!'
    console.log(data);
  });
});

// Connecting peer
var peer = new Peer('anotherid', {key: 'apikey'});
var conn = peer.connect('someid');
conn.on('open', function(){
  conn.send('hi!');
});

原文 WebRTC实现浏览器上的音视频通信 - 掘金

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/132212115