与WebRTC实时通信

与WebRTC实时通信

一、介绍

WebRTC是一个开源项目,可以在Web和本机应用程序中实现音频,视频和数据的实时通信。WebRTC有几个JavaScript API:
getUserMedia():捕获音频和视频。
MediaRecorder:录制音频和视频。
RTCPeerConnection:在用户之间传输音频和视频。
RTCDataChannel:用户之间的流数据。
在Firefox,Opera和桌面和Android上的Chrome中可以使用WebRTC。WebRTC也可用于iOS和Android上的本机应用程序。

什么是signaling?
WebRTC使用RTCPeerConnection在浏览器之间传递流数据,但也需要一种协调通信和发送控制消息的机制,这一过程称为信令。WebRTC未指定信令方法和协议。本例将使用Socket.IO进行消息传递。

什么是STUN和TURN?
WebRTC旨在实现点对点工作,因此用户可以通过最直接的路由进行连接。但是,WebRTC的构建是为了应对真实的网络:客户端应用程序需要遍历NAT网关和防火墙,并且在直接连接失败的情况下,对等网络需要回退。作为此过程的一部分,WebRTC API使用STUN服务器获取计算机的IP地址,并使用TURN服务器作为中继服务器,以防对等通信失败。

二、概述

构建应用程序以获取视频并使用网络摄像头拍摄快照,并通过WebRTC进行点对点共享。在此过程中,将学习如何使用核心WebRTC API并使用Node.js设置消息传递服务器。

你将学到什么
从网络摄像头获取视频
使用RTCPeerConnection流式传输视频
使用RTCDataChannel流式传输数据
建立信令服务以交换消息
结合对等连接和信令
拍照并通过数据通道分享

你需要什么
Chrome 47或以上
适用于Chrome的Web Server,或使用您自己选择的Web服务器。
示例代码
文本编辑器
HTML,CSS和JavaScript的基础知识

三、获取示例代码

下载代码
如果您熟悉git,可以通过克隆它从GitHub下载此codelab的代码:

git clone https://github.com/googlecodelabs/webrtc-web

或者单击此处下载代码的.zip文件。

打开下载的zip文件。这个项目文件夹包含每个步骤以及需要的所有资源。您将在名为work的目录中完成所有编码工作。

安装并验证Web服务器
虽然您可以自由使用自己的Web服务器,但此codelab可以与Chrome Web服务器配合使用。如果您尚未安装该应用,则可以从Chrome网上应用店安装该应用。
与WebRTC实时通信_第1张图片

安装Web Server for Chrome应用程序后,单击书签栏,新标签页或应用启动器中的Chrome应用程序快捷方式:
在这里插入图片描述

单击Web Server图标:
在这里插入图片描述

接下来,您将看到此对话框,它允许您配置本地Web服务器:

与WebRTC实时通信_第2张图片

单击“ 选择文件夹”按钮,然后选择刚刚创建的工作文件夹。这样,您就可以通过“ Web服务器URL”部分的“Web服务器”对话框中突出显示的URL查看Chrome中正在进行的工作。

在选项下,选中自动显示index.html旁边的框,如下所示:

与WebRTC实时通信_第3张图片

然后通过滑动标记为Web Server的切换来停止并重新启动服务器:STARTED向左,然后向右移动。

在这里插入图片描述

现在,通过单击突出显示的Web服务器URL,在Web浏览器中访问您的工作站点。你应该看到一个看起来像这样的页面,它对应于work / index.html:
与WebRTC实时通信_第4张图片

从现在开始,应使用此Web服务器设置执行所有测试和验证,只需刷新测试浏览器选项卡。

四、通过网络摄像头流式传输视频

在这一步中,您将了解如何:
从您的网络摄像头获取视频流。
操纵流播放。
使用CSS和SVG来操纵视频。

此步骤的完整版本位于step-01文件夹中。

在工作目录中为index.html添加video元素和script元素:






  Realtime communication with WebRTC

  





  

Realtime communication with WebRTC

以下内容添加到main.js在您的JS文件夹

'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
};

// Video element where stream will be placed.
const localVideo = document.querySelector('video');

// Local stream that will be reproduced on the video.
let localStream;

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

这里的所有JavaScript示例都用于’use strict’;避免常见的编码问题。

在浏览器中打开index.html,可以看到网络摄像头的视图。

在getUserMedia()响应之后,浏览器请求用户访问其摄像机的许可(如果这是第一次请求当前的摄像机访问)。如果成功,则返回MediaStream,媒体元素可以通过该srcObject属性使用它:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

该constraints参数允许您指定要获取的媒体。在此示例中,仅限视频,因为默认情况下禁用音频:

const mediaStreamConstraints = {
  video: true,
};

您可以使用约束来满足其他要求,例如视频分辨率:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

如果getUserMedia()成功,则将来自网络摄像头的视频流设置为视频元素的来源:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

不要忘记元素的autoplay属性video。没有它,你只会看到一个帧!

五、使用RTCPeerConnection流式传输视频

在这一步中,您将了解:

使用RTCPeerConnection API流式传输视频。
控制媒体捕获和流媒体。

此步骤的完整版本位于step-2文件夹中。

什么是RTCPeerConnection?
RTCPeerConnection是用于进行WebRTC调用以流式传输视频和音频以及交换数据的API。

此示例在同一页面上的两个RTCPeerConnection对象(称为对等方)之间建立连接。实用性不大,但有助于理解RTCPeerConnection的工作原理。

添加视频元素和控制按钮
在index.html中,用两个视频元素和三个按钮替换单个视频元素:





一个视频元素将显示getUserMedia()流,另一个将显示通过RTCPeerconnection流传输的相同视频。(在实际应用程序中,一个视频元素将显示本地流,另一个视频元素将显示远程流。)

添加adapter.js填充程序
添加当前版本adapter.js 链接上面main.js:


adapter.js是一个填充程序,可以将应用程序与规范更改和前缀差异隔离开来。
Index.html现在应该如下所示:





  Realtime communication with WebRTC
  



  

Realtime communication with WebRTC

安装RTCPeerConnection代码
将main.js替换为step-02文件夹中的版本。
在codelab中使用大块代码进行剪切和粘贴并不理想,但为了使RTCPeerConnection启动并运行,除了整个替换之外别无选择。

打开index.html,单击“ Start”按钮以从网络摄像头获取视频,然后单击“ Call”以建立对等连接。您应该在两个视频元素中看到相同的视频(来自您的网络摄像头)。查看浏览器控制台以查看WebRTC日志记录。

在WebRTC对等体之间建立呼叫涉及三个任务:
为呼叫的每一端创建一个RTCPeerConnection,并在每一端添加本地流getUserMedia()。
获取和共享网络信息:潜在的连接端点称为ICE候选者。
获取并共享本地和远程描述:SDP格式的本地媒体元数据。

想象一下,Alice和Bob想要使用RTCPeerConnection来设置视频聊天。
首先,Alice和Bob交换网络信息。“查找候选者”一词是指使用ICE框架查找网络接口和端口的过程。
Alice使用onicecandidate (addEventListener(‘icecandidate’))处理程序创建RTCPeerConnection对象。这对应于main.js中的以下代码:

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);

Alice调用getUserMedia()并添加传递给它的流:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');

当网络候选者可用时,步骤1中的处理程序onicecandidate被调用。
Alice将序列化的候选数据发送给Bob。在实际应用程序中,此过程(信令)通过消息传递服务进行。在此步骤中,两个RTCPeerConnection对象位于同一页面上,可以直接通信,无需外部消息传递。
当Bob从Alice获取候选消息时,他调用addIceCandidate(),将候选者添加到远程对等描述中:

function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

WebRTC对等体还需要找出并交换本地和远程音频和视频媒体信息,例如分辨率和编解码器功能。通过使用称为SDP的会话描述协议格式交换元数据块(称为offer 和answer)来进行交换媒体配置信息的信令:

1 Alice运行RTCPeerConnection createOffer()方法。返回的promise提供了一个RTCSessionDescription:Alice的本地会话描述:

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);

2 如果成功,Alice使用本地描述设置setLocalDescription(),然后通过其信令通道将此会话描述发送给Bob。
3 Bob将Alice发送给他的描述设置为使用的远程描述setRemoteDescription()。
4 Bob运行RTCPeerConnection createAnswer()方法,向其传递从Alice获得的远程描述,因此可以生成与她兼容的本地会话。createAnswer() promise 传递一个RTCSessionDescription:Bob设置为本地描述,并将其发送给Alice。
5 当Alice获得Bob的会话描述时,她将其设置为远程描述setRemoteDescription()。

// Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer connection session descriptions.
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

六、使用RTCDataChannel交换数据

你将学到什么
如何在WebRTC端点(对等方)之间交换数据。

此步骤的完整版本位于步骤03文件夹中。

更新您的HTML
对于此步骤,您将使用WebRTC数据通道textarea在同一页面上的两个元素之间发送文本。这不是很有用,但确实演示了WebRTC如何用于共享数据以及流式视频。

从index.html中删除视频和按钮元素,并使用以下HTML替换它们:




更新您的JavaScript

将main.js替换为step-03 / js / main.js的内容。

在对等体之间尝试流数据:打开index.html,按 Start 设置对等连接,在左侧textarea输入一些文本,然后单击Send以使用WebRTC数据通道传输文本。

此代码使用RTCPeerConnection和RTCDataChannel来启用文本消息的交换。此步骤中的大部分代码与RTCPeerConnection示例相同。

sendData()和createConnection()功能有大部分新代码:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

注意使用dataConstraint。可以配置数据通道以实现不同类型的数据共享 - 例如,优先考虑可靠的交付而不是性能。

七、建立信令服务以交换消息

你将学到什么

使用npm安装项目依赖中规定的package.json
运行Node.js服务器并使用node-static来提供静态文件。
使用Socket.IO在Node.js上设置消息传递服务。
用它来创建“房间”并交换消息。

此步骤的完整版本位于step-04文件夹中。

为了设置和维护WebRTC调用,WebRTC客户端(对等方)需要交换元数据:候选人(网络)信息。提供和回答提供有关媒体信息的消息,例如分辨率和编解码器。换句话说,在可以发生音频,视频或数据的对等流传输之前,需要交换元数据。此过程称为信令。

在前面的步骤中,发送方和接收方RTCPeerConnection对象位于同一页面上,因此“信令”只是在对象之间传递元数据的问题。

在现实世界的应用程序中,发送方和接收方RTCPeerConnections在不同设备上的网页中运行,您需要一种方式让它们进行元数据通信。

为此,您使用信令服务器:可以在WebRTC客户端(对等方)之间传递消息的服务器。实际的消息是纯文本:字符串化的JavaScript对象。

先决条件:安装Node.js.
为了运行此codelab的下一步(文件夹步骤04到步骤06),您需要使用Node.js在localhost上运行服务器。

您可以下载并安装由Node.js的这个链接。

安装后,您将能够导入后续步骤(运行npm install)所需的依赖项,以及运行一个小型localhost服务器来执行codelab(运行node index.js)。这些命令将在以后需要时显示。

关于该应用程序
WebRTC使用客户端JavaScript API,但对于实际使用,还需要信令(消息)服务器,以及STUN和TURN服务器。
在这一步中,您将构建一个简单的Node.js信令服务器,使用Socket.IO Node.js模块和JavaScript库进行消息传递。使用Node.js和Socket.IO会很有用,但并不重要; 消息传递组件非常简单。

Socket.IO的设计使得构建交换消息的服务变得简单,Socket.IO因其内置的“房间”概念而适合学习WebRTC信令。
在此示例中,服务器(Node.js应用程序)在index.js中实现,在其上运行的客户端(Web应用程序)在index.html中实现。

此步骤中的Node.js应用程序有两个任务。

首先,它充当消息中继:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

其次,它管理WebRTC视频聊天’房间’:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

我们简单的WebRTC应用程序将允许最多两个对等方共享一个房间。
HTML和JavaScript
更新index.html所以它看起来像这样:


    




  Realtime communication with WebRTC

  





  

Realtime communication with WebRTC

在此步骤中,您不会在页面上看到任何内容:所有日志记录都在浏览器控制台上完成。(要在Chrome中查看控制台,请按Ctrl-Shift-J。如果您使用的是Mac,Command-Option-J。)
用以下内容替换js / main.js:

'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

设置Socket.IO以在Node.js上运行
在HTML文件中,您可能已经看到您正在使用Socket.IO文件:


  
  
  



将js / main.js替换为步骤05 / js / main.js的内容。

运行Node.js服务器

node index.js

在浏览器中,打开localhost:8080。

在新选项卡或窗口中再次打开localhost:8080。一个视频元素将显示本地流getUserMedia(),另一个将显示通过RTCPeerconnection流式传输的“远程”视频。
注意:每次关闭客户端选项卡或窗口时,都需要重新启动Node.js服务器。
可在浏览器控制台中查看日志记录

如果您遇到奇怪的缓存问题,请尝试以下方法:
按住ctrl并单击“ 重新加载”按钮进行硬刷新
重启浏览器
运行npm cache clean命令行。

九、拍照并通过数据通道分享

你将学到什么

拍摄照片并使用canvas元素从中获取数据。
与远程用户交换图像数据。

此步骤的完整版本位于步骤06文件夹中。

以前,您学习了如何使用RTCDataChannel交换文本消息。此步骤可以共享整个文件:在此示例中,通过getUserMedia()拍摄照片。

该步骤的核心部分如下:

建立数据通道。请注意,在此步骤中不要向对等连接添加任何媒体流。
使用以下方法捕获用户的网络摄像头视频流getUserMedia():

var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}

当用户单击“ Snap”按钮时,从视频流中获取快照(视频帧)并将其显示在canvas元素中:

var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}

当用户单击“ Send”按钮时,将图像转换为字节并通过数据通道发送:
function sendPhoto() {
// Split data channel message in chunks of this byte length.
var CHUNK_LEN = 64000;
var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
len = img.data.byteLength,
n = len / CHUNK_LEN | 0;

console.log(‘Sending a total of ’ + len + ’ byte(s)’);
dataChannel.send(len);

// split the photo and send in chunks of about 64KB
for (var i = 0; i < n; i++) {
var start = i * CHUNK_LEN,
end = (i + 1) * CHUNK_LEN;
console.log(start + ’ - ’ + (end - 1));
dataChannel.send(img.data.subarray(start, end));
}

// send the reminder, if any
if (len % CHUNK_LEN) {
console.log(‘last ’ + len % CHUNK_LEN + ’ byte(s)’);
dataChannel.send(img.data.subarray(n * CHUNK_LEN));
}
}
接收方将数据通道消息字节转换回图像并将图像显示给用户:

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // trail is the element holding the incoming images
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

获取代码

用步骤06的内容替换work文件夹的内容。你的index.html文件的工作,现在应该是这样的:






  Realtime communication with WebRTC

  





  

Realtime communication with WebRTC

Room URL: ...

then or

Incoming photos

该应用程序将创建一个随机的房间ID,并将该ID添加到URL。在新的浏览器选项卡或窗口中打开地址栏中的URL。

单击“ Snap & Send”按钮,然后在页面底部的另一个选项卡中查看传入区域。该应用在标签之间传输照片。

你可能感兴趣的:(与WebRTC实时通信)