实时对讲 webrtc_WebRTC,实时Web API

本文介绍了WebRTC技术,用于实现实时通信,包括访问摄像头和麦克风的MediaStream API,处理音频和视频流的RTCPeerConnection,以及信令过程。通过WebRTC,可以创建视频聊天、文件分享和多人游戏等应用。示例展示了如何使用WebRTC连接两个远程摄像头,通过WebSocket进行信令通信。此外,解释了RTCPeerConnection的配置,如ICE和STUN服务器的角色,以及连接的建立和关闭过程。
摘要由CSDN通过智能技术生成

实时对讲 webrtc

WebRTC stands for Web Real Time Communication.

WebRTC代表Web实时通信

It allows to create a direct data communication between browsers.

它允许在浏览器之间创建直接的数据通信。

You can use it to

你可以用它来

  • stream audio

    流音频
  • stream video

    流视频
  • share files

    分享文件
  • video chat

    视讯通讯
  • create a peer-to-peer data sharing service

    创建对等数据共享服务
  • create multiplayer games

    创建多人游戏

and more.

和更多。

It’s an effort to make real-time communication applications easy to create, leveraging Web technologies, so that no 3rd party plugin or external technology is needed beside your Web browser.

为了使利用Web技术的实时通信应用程序易于创建是一项努力,因此您的Web浏览器旁边不需要第三方插件或外部技术。

There should be no need for a plugin to perform RTC in the future, but all should instead rely on a standard technology - WebRTC.

将来无需再有插件来执行RTC,但所有人都应该依赖于标准技术-WebRTC。

It is supported by all the modern browsers (with partial support from Edge which does not support RTCDataChannel - see later):

所有现代浏览器都支持它(Edge的部分支持不支持RTCDataChannel稍后请参见):

WebRTC implements the following APIs:

WebRTC实现以下API:

  • MediaStream gets access to data streams from the user’s end, like the camera and the microphone

    MediaStream可以从用户端访问摄像头和麦克风等数据流

  • RTCPeerConnection handles communication of audio and video streaming between peers

    RTCPeerConnection处理对等点之间的音频和视频流通信

  • RTCDataChannel: handles communication of other kinds of data (arbitrary data)

    RTCDataChannel :处理其他类型的数据(任意数据)的通信

With video and audio communication you’ll use MediaStream and RTCPeerConnection.

通过视频和音频通信,您将使用MediaStreamRTCPeerConnection

Other kind of application, like gaming, file sharing and others rely on RTCDataChannel.

其他类型的应用程序,例如游戏,文件共享等,都依赖RTCDataChannel

In this article I’ll create an example using WebRTC to connect two remote webcams, using a Websockets server using Node.js.

在本文中,我将创建一个示例,该示例使用WebRTC通过使用Node.jsWebsockets服务器连接两个远程摄像头。

Tip: in your projects you’ll likely use a library that abstracts away many of those details. This tutorial aims to explain the WebRTC technology, so you know what is going on under the hood.

提示:在您的项目中,您可能会使用一个抽象许多细节的库。 本教程旨在说明WebRTC技术,因此您可以了解幕后情况。

媒体流 (MediaStream)

This API lets you access the camera and microphone stream using JavaScript.

通过此API,您可以使用JavaScript访问摄像头和麦克风流。

Here is a simple example that asks you to access the video camera and plays the video in the page:

这是一个简单的示例,要求您访问摄像机并在页面中播放视频:

See the Pen WebRTC MediaStream simple example by Flavio Copes (@flaviocopes) on CodePen.

见笔的WebRTC MediaStream简单的例子,由弗拉维奥·科佩斯( @flaviocopes上) CodePen

We add a button to get access to the camera, then we add a video element, with the autoplay attribute.

我们添加一个按钮以访问摄像机,然后添加一个具有autoplay属性的video元素。

We also add the WebRTC Adapter which helps for cross-browser compatibility:

我们还添加了WebRTC适配器 ,该适配器有助于跨浏览器兼容性:

<button id="get-access">Get access to camera</button>
<video autoplay></video>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

The JS listens for a click on the button, then calls navigator.mediaDevices.getUserMedia() asking for the video.

JS监听按钮的单击,然后调用navigator.mediaDevices.getUserMedia()询问视频。

See the getUserMedia() tutorial

请参阅getUserMedia()教程

Then we access the name of the camera used by calling stream.getVideoTracks() on the result of the call to getUserMedia().

然后,我们访问摄像机的调用使用的名称stream.getVideoTracks()的调用的结果getUserMedia()

The stream is set to be the source object for the video tag, so that playback can happen:

流被设置为video标签的源对象,因此可以进行回放:

document.querySelector('#get-access').addEventListener('click', async function init(e) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true
    })
    document.querySelector('video').srcObject = stream
    document.querySelector('#get-access').setAttribute('hidden', true)
    setTimeout(() => { track.stop() }, 3 * 1000)
  } catch (error) {
    alert(`${error.name}`)
    console.error(error)
  }
})

The arguments of getUserMedia() can specify additional requirements for the video stream:

getUserMedia()的参数可以指定视频流的其他要求:

navigator.mediaDevices.getUserMedia({
  video: {
    mandatory: { minAspectRatio: 1.333, maxAspectRatio: 1.334 },
    optional: [
      { minFrameRate: 60 },
      { maxWidth: 640 },
      { maxHeigth: 480 }
    ]
  }
}, successCallback, errorCallback);

To get an audio stream you would ask for the audio media object too, and call stream.getAudioTracks() instead of stream.getVideoTracks().

要获取音频流,您还需要音频媒体对象,然后调用stream.getAudioTracks()而不是stream.getVideoTracks()

After 3 seconds of playback we stop the video streaming by calling track.stop().

播放3秒后,我们通过调用track.stop()停止视频流。

发信号 (Signaling)

Signaling is not part of the WebRTC protocol but it’s an essential part for real time communication.

信令不是WebRTC协议的一部分,而是实时通信的重要组成部分。

Via signaling, devices communicate between each other and agree on the communication initialization, sharing information such as IP addresses and ports, resolutions and more.

通过信令,设备之间可以相互通信并就通信初始化达成协议,共享IP地址和端口,分辨率等信息。

You are free to choose any kind of communication mechanism, including:

您可以自由选择任何一种通信机制,包括:

  • Websockets

    网络套接字
  • Channel Messaging API

    频道讯息API
  • XHR

    XHR

We implement it using Websockets.

我们使用Websockets实现它。

Install ws using npm:

使用npm安装ws

npm init
npm install ws

We start with a simple Websockets server skeleton:

我们从一个简单的Websockets服务器框架开始:

const WebSocket = require('ws')

const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', ws => {
  console.log('User connected')

  ws.on('message', message => {
    console.log(`Received message => ${message}`)
  })

  ws.on('close', () => {
    //handle closing
  })
})

We first add a ‘username’ box to our frontend, so the user can pick a username before connecting to the server.

我们首先在前端添加一个“用户名”框,以便用户在连接服务器之前可以选择一个用户名。

<div id="login">
  <label for="username">Login</label>
  <input id="username" placeholder="Login" required="" autofocus="">
  <button id="login">Login</button>
</div>

In the client JavaScript we initialize the Websocket to the server:

在客户端JavaScript中,我们将Websocket初始化为服务器:

const ws = new WebSocket('ws://localhost:8080')

ws.onopen = () => {
  console.log('Connected to the signaling server')
}

ws.onerror = err => {
  console.error(err)
}

When the user enters the username and clicks the login button we get the username value and we check it, then we send this information to the server:

当用户输入用户名并单击登录按钮时,我们将获得用户名值并进行检查,然后将此信息发送到服务器:

document.querySelector('button#login').addEventListener('click', event => {
  username = document.querySelector('input#username').value

  if (username.length < 0) {
    alert('Please enter a username 🙂')
    return
  }

  sendMessage({
    type: 'login',
    username: username
  })
})

sendMessage is a wrapper function for sending a JSON-encoded message to the Websocket server. We use a type parameter to separate different kind of messages we’ll send:

sendMessage是用于将JSON编码的消息发送到Websocket服务器的包装函数。 我们使用type参数来分隔将发送的不同类型的消息:

const sendMessage = message => {
  ws.send(JSON.stringify(message))
}

Server side, we decode the JSON message and we detect the message type

服务器端,我们解码JSON消息并检测消息类型

ws.on('message', message => {
  let data = null

  try {
    data = JSON.parse(message)
  } catch (error) {
    console.error('Invalid JSON', error)
    data = {}
  }

  switch (data.type) {
    case 'login':
      console.log('User logged', data.username)
      break
  }
})

We must add the user to a list of connected users, stored in an associative array users:

我们必须将用户添加到已连接用户的列表中,并存储在关联数组users

const users = {}

If there is another user already with this same username, we send an error to the client, otherwise we add the user to the array, storing the Websocket connection:

如果已经有另一个具有相同用户名的用户,我们将错误发送给客户端,否则我们将该用户添加到数组中,并存储Websocket连接:

//...
case 'login':
  console.log('User logged', data.username)
  if (users[data.username]) {
    sendTo(ws, { type: 'login', success: false })
  } else {
    users[data.username] = ws
    ws.username = data.username
    sendTo(ws, { type: 'login', success: true })
  }
  break

Client-side, when this happens we handle the message and we call the getUserMedia() function:

在客户端,发生这种情况时,我们将处理消息,并调用getUserMedia()函数:

ws.onmessage = msg => {
  console.log('Got message', msg.data)

  const data = JSON.parse(msg.data)

  switch (data.type) {
    case 'login':
      handleLogin(data.success)
      break
  }
}
//handleLogin...
navigator.mediaDevices.getUserMedia(
  { video: true, audio: true },
  localStream => {
    //...
  },
  error => {
    console.error(error)
  }
)

Inside the success callback, which gets the local stream object, we first hide the #login div and we can show a new div that hosts the video elements:

在获取本地流对象的成功回调内部,我们首先隐藏#login div,然后可以显示一个托管video元素的新div:

<div id="call">
  <video id="local" autoplay></video>
  <video id="remote" autoplay></video>
</div>
document.querySelector('div#login').style.display = 'none'
document.querySelector('div#call').style.display = 'block'

so that we can start streaming it on the video#local element in the page:

这样我们就可以在页面的video#local元素上开始流式传输:

document.querySelector('video#local').src = window.URL.createObjectURL(localStream)

RTCPeerConnection (RTCPeerConnection)

Now we must configure an RTCPeerConnection.

现在,我们必须配置一个RTCPeerConnection。

There are a few alien terms you’ll find now. ICE stands for Internet Connectivity Establishment, and STUN stands for Session Traversal of User Datagram Protocol [UDP] Through Network Address Translators [NATs])

您现在会发现一些外来术语。 ICE代表Internet连接建立STUN代表通过网络地址转换器[NAT]的用户数据报协议[UDP]的会话遍历)

In practice, we must have a way to get 2 computers located in local networks (like your home) to talk to each other. Since most users are behind a NAT router, computers cannot accept incoming connections out of the box.

实际上,我们必须有一种方法可以让位于本地网络(例如您的家庭)中的两台计算机相互通信。 由于大多数用户都在NAT路由器后面,因此计算机无法立即接受传入的连接。

There is a lot of code that’s just needed so we can have 2 endpoints to connect to each other, before the connection takes place.

只是需要很多代码,因此在建立连接之前,我们可以有2个端点相互连接。

The peer connection must be initiated using a STUN server and that server will send back our ICE candidate to communicate with another peer.

对等连接必须使用STUN服务器启动,并且该服务器将发回我们的ICE候选者与另一个对等进行通信。

This is basically what the code below does:

这基本上是以下代码的作用:

//using Google public stun server
const configuration = {
  iceServers: [{ url: 'stun:stun2.1.google.com:19302' }]
}

connection = new RTCPeerConnection(configuration)

connection.addStream(localStream)

connection.onaddstream = event => {
  document.querySelector('video#remote').srcObject = event.stream
}

connection.onicecandidate = event => {
  if (event.candidate) {
    sendMessage({
      type: 'candidate',
      candidate: event.candidate
    })
  }
}

We configure an ICE server using the Google public STUN server (which works fine for testing purposes, but you’ll most likely need to configure your own for production use).

我们使用Google公共STUN服务器(可以很好地用于测试目的配置ICE服务器),但是您很可能需要配置自己的ICE服务器以用于生产用途。

Then we add the local stream to that connection using addStream(), and we pass 2 callback handlers for the RTCPeerConnection.onaddstream and RTCPeerConnection.onicecandidate events.

然后,使用addStream()将本地流添加到该连接,并为RTCPeerConnection.onaddstreamRTCPeerConnection.onicecandidate事件传递2个回调处理程序。

RTCPeerConnection.onaddstream is called when we have a remote audio/video stream coming in, and we assign it to the remote video element to stream.

当我们有一个远程音频/视频流进入时,将调用RTCPeerConnection.onaddstream ,并将其分配给远程video元素以进行流传输。

For data, the event would be called RTCPeerConnection.ondatachannel and instead of using the addStream() method you would have used createDataChannel().

对于数据,该事件将称为RTCPeerConnection.ondatachannel ,而不是使用addStream()方法,而是使用createDataChannel()

RTCPeerConnection.onicecandidate is called when we receive an ICE candidate, and we send it to our server.

当我们收到ICE候选者时,将调用RTCPeerConnection.onicecandidate ,并将其发送到我们的服务器。

Before this happens we must attempt to connect to a peer.

在这种情况发生之前,我们必须尝试连接到同级。

In this simple example we must know the username of the other person we want to connect to, and they must already be “logged in”.

在这个简单的示例中,我们必须知道我们要连接的其他人的用户名,并且他们必须已经“登录”。

One of the 2 users must enter the username in the box and click the “Call” button.

2个用户之一必须在框中输入用户名,然后单击“呼叫”按钮。

<div>
  <input id="username-to-call" placeholder="Username to call" />
  <button id="call">Call</button>
  <button id="close-call">Close call</button>
</div>

In the client JavaScript we listen for the click event on this button and we get the username value.

在客户端JavaScript中,我们监听该按钮上的click事件,并获得用户名值。

If the username is valid we store it in the otherUsername variable we’ll use later, and we create an offer.

如果用户名有效,我们将其存储在otherUsername变量中,我们将在以后使用,并创建一个offer

let otherUsername

document.querySelector('button#call').addEventListener('click', () => {
  const callToUsername = document.querySelector('input#username-to-call').value

  if (callToUsername.length === 0) {
    alert('Enter a username 😉')
    return
  }

  otherUsername = callToUsername

  // create an offer
  connection.createOffer(
    offer => {
      sendMessage({
        type: 'offer',
        offer: offer
      })

      connection.setLocalDescription(offer)
    },
    error => {
      alert('Error when creating an offer')
      console.error(error)
    }
  )
})

Once we get the offer by calling RTCPeerConnection.createOffer(), we pass it to our server and we call RTCPeerConnection.setLocalDescription() to configure the connection.

通过调用RTCPeerConnection.createOffer()获得报价后,将其传递到服务器,然后调用RTCPeerConnection.setLocalDescription()来配置连接。

On the server side, we process the offer and we send it to the user that we want to connect to, passed as data.otherUsername:

在服务器端,我们处理商品并将其发送给我们要连接的用户,并以data.otherUsername传递:

case 'offer':
  console.log('Sending offer to: ', data.otherUsername)
  if (users[data.otherUsername] != null) {
    ws.otherUsername = data.otherUsername
    sendTo(users[data.otherUsername], {
      type: 'offer',
      offer: data.offer,
      username: ws.username
    })
  }
  break

The client receives this offer as a Websocket message, and we call the handleOffer method:

客户收到此报价作为Websocket消息,我们调用handleOffer方法:

ws.onmessage = msg => {
  //...
  switch (data.type) {
    //...
    case 'offer':
      handleOffer(data.offer, data.username)
      break
  }
}

This method accepts the offer and the username, and we first call RTCPeerConnection.setRemoteDescription() to specify the properties of the remote end of the connection, then RTCPeerConnection.createAnswer() to create the answer to the offer.

此方法接受要约和用户名,我们首先调用RTCPeerConnection.setRemoteDescription()来指定连接远程端的属性,然后RTCPeerConnection.createAnswer()来创建要约的答案。

Once the answer is created, we use it to set the properties of the local end of the connection and we post it to our server, using the sendMessage function.

创建答案后,我们将使用它来设置连接本地端的属性,然后使用sendMessage函数将其发布到服务器。

The RTCSessionDescription object describes the connection capabilities and must be initialized before the RTC can happen. We must set both the description of the local end of the connection (setLocalDescription), and the description of the other end of the connection (setRemoteDescription).

RTCSessionDescription对象描述了连接功能,必须在RTC发生之前进行初始化。 我们必须同时设置连接本地端的描述( setLocalDescription )和连接另一端的描述( setRemoteDescription )。

const handleOffer = (offer, username) => {
  otherUsername = username
  connection.setRemoteDescription(new RTCSessionDescription(offer))
  connection.createAnswer(
    answer => {
      connection.setLocalDescription(answer)
      sendMessage({
        type: 'answer',
        answer: answer
      })
    },
    error => {
      alert('Error when creating an answer')
      console.error(error)
    }
  )
}

On the server side we handle the answer event:

在服务器端,我们处理answer事件:

case 'answer':
  console.log('Sending answer to: ', data.otherUsername)
  if (users[data.otherUsername] != null) {
    ws.otherUsername = data.otherUsername
    sendTo(users[data.otherUsername], {
      type: 'answer',
      answer: data.answer
    })
  }
  break

We check if the username we want to talk with exists, then we set it as the otherUsername of the Websocket connection. We send the answer back to that user.

我们检查要与之交谈的用户名是否存在,然后将其设置为Websocket连接的otherUsername 。 我们将答案发送回该用户。

On the client side that user will get the answer message that triggers the handleAnswer() method, which calls RTCPeerConnection.setRemoteDescription() to synchronize the properties of the remote end of the connection:

在客户端,用户将收到触发handleAnswer()方法的answer消息,该方法调用RTCPeerConnection.setRemoteDescription()来同步连接远程端的属性:

ws.onmessage = msg => {
  //...
  switch (data.type) {
    //...
    case 'answer':
      handleAnswer(data.answer)
      break
  }
}

const handleAnswer = answer => {
  connection.setRemoteDescription(new RTCSessionDescription(answer))
}

Now that the session descriptions have been synchronized, the two peers start to determine how to establish the connection between them, using the ICE protocol. This is the key part that works around the NAT routers limitations.

现在,会话描述已经同步,两个对等方开始确定如何使用ICE协议在它们之间建立连接。 这是解决NAT路由器限制的关键部分。

RTCPeerConnection produces an ICE candidate and calls its onicecandidate callback function. In the callback we send the ICE candidate to the other end of connection, using our sendMessage() function:

RTCPeerConnection生成一个ICE候选者并调用其onicecandidate回调函数。 在回调中,我们使用sendMessage()函数将ICE候选对象发送到连接的另一端:

connection.onicecandidate = event => {
  if (event.candidate) {
    sendMessage({
      type: 'candidate',
      candidate: event.candidate
    })
  }
}

On the server side we handle the candidate event by sending it to the other peer:

在服务器端,我们通过将candidate事件发送给另一个对等事件来处理它:

//...
case 'candidate':
  console.log('Sending candidate to:', data.otherUsername)

  if (users[data.otherUsername] != null) {
    sendTo(users[data.otherUsername], {
      type: 'candidate',
      candidate: data.candidate
    })
  }

  break

The other peer receives it on the client:

另一个对等方在客户端上收到它:

ws.onmessage = msg => {
  //...
  switch (data.type) {
    //...
    case 'candidate':
      handleCandidate(data.candidate)
      break
  }
}

const handleCandidate = candidate => {
  connection.addIceCandidate(new RTCIceCandidate(candidate))
}

We call RTCPeerConnection.addIceCandidate() to add the candidate locally.

我们调用RTCPeerConnection.addIceCandidate()在本地添加候选对象。

At this point the ICE exchange steps and session description are complete, negotiation is done and WebRTC can connect the two remote peers, using the connection mechanism that was automatically agreed upon.

至此,ICE交换步骤和会话描述完成,协商完成,WebRTC可以使用自动约定的连接机制连接两个远程对等方。

We now have 2 computers directly communicating to each other exchanging their webcam streams!

现在,我们有2台计算机,彼此直接通信,交换网络摄像头流!

断开连接 (Closing the connection)

The connection can be closed programmatically. We have a button “Close call” that we can click once the connection has been made:

可以通过编程方式关闭连接。 建立连接后,我们可以单击“关闭呼叫”按钮:

<button id="close-call">Close call</button>
document.querySelector('button#close-call').addEventListener('click', () => {
  sendMessage({
    type: 'close'
  })

  handleClose()
})

const handleClose = () => {
  otherUsername = null
  document.querySelector('video#remote').src = null

  connection.close()
  connection.onicecandidate = null
  connection.onaddstream = null
}

On the client side we remove the remote streaming and we close the RTCPeerConnection connection, setting the callback for its events to null.

在客户端,我们删除远程流,并关闭RTCPeerConnection连接,将其事件的回调设置为null。

We send the close message to the server, which in turn sends it to the remote peer:

我们将close消息发送到服务器,服务器又将其发送到远程对等方:

case 'close':
  console.log('Disconnecting from', data.otherUsername)
  users[data.otherUsername].otherUsername = null
  if (users[data.otherUsername] != null) {
    sendTo(users[data.otherUsername], { type: 'close' })
  }
  break

so in the client side we can call the handleClose() function:

因此,在客户端,我们可以调用handleClose()函数:

ws.onmessage = msg => {
  //...
  switch (data.type) {
    //...
    case 'close':
      handleClose()
      break
  }
}

The complete example is available on this Gist:

完整的示例可在以下要点中找到:

翻译自: https://flaviocopes.com/webrtc/

实时对讲 webrtc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值