欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于WebRTC實(shí)現(xiàn)音視頻通話(huà)功能

 更新時(shí)間:2024年05月31日 11:45:50   作者:黑馬程序員官方  
WebRTC作為一種開(kāi)放標(biāo)準(zhǔn)的實(shí)時(shí)通信協(xié)議,能輕松實(shí)現(xiàn)瀏覽器之間的實(shí)時(shí)音視頻通信,本次主要分享基于WebRTC的音視頻通話(huà)技術(shù),講解WebRTC原理和音視頻傳輸?shù)汝P(guān)鍵概念,通過(guò)案例實(shí)踐,帶大家掌握如何搭建一個(gè)音視頻通話(huà)應(yīng)用,需要的朋友可以參考下

隨著互聯(lián)網(wǎng)的發(fā)展,實(shí)時(shí)音視頻通話(huà)功能已經(jīng)成為遠(yuǎn)程辦公、社交娛樂(lè)和在線(xiàn)教育等領(lǐng)域中不可或缺的一項(xiàng)重要功能。WebRTC作為一種開(kāi)放標(biāo)準(zhǔn)的實(shí)時(shí)通信協(xié)議,能輕松實(shí)現(xiàn)瀏覽器之間的實(shí)時(shí)音視頻通信。
本次主要分享基于WebRTC的音視頻通話(huà)技術(shù),講解WebRTC原理和音視頻傳輸?shù)汝P(guān)鍵概念,
通過(guò)案例實(shí)踐,帶大家掌握如何搭建一個(gè)音視頻通話(huà)應(yīng)用。

背景

隨著互聯(lián)網(wǎng)技術(shù)的飛速發(fā)展,實(shí)時(shí)音視頻通話(huà)已經(jīng)成為在線(xiàn)教育、遠(yuǎn)程辦公、社交媒體等領(lǐng)域的核心且常用的功能。WebRTC(Web Real-Time Communication)作為一項(xiàng)開(kāi)放的實(shí)時(shí)通信標(biāo)準(zhǔn),為開(kāi)發(fā)者提供了快速構(gòu)建實(shí)時(shí)音視頻通話(huà)系統(tǒng)的能力。在本課程中,我們將從0到1使用 WebRTC 構(gòu)建一個(gè)基于 P2P 架構(gòu)的音視頻通話(huà)的應(yīng)用案例。

應(yīng)用場(chǎng)景

  • 點(diǎn)對(duì)點(diǎn)視頻聊天:如 微信視頻 等實(shí)時(shí)視頻通話(huà)應(yīng)用。
  • 多人視頻會(huì)議:企業(yè)級(jí)多人視頻會(huì)議系統(tǒng),如飛書(shū)、釘釘、騰訊會(huì)議等。
  • 在線(xiàn)教育:如騰訊課堂、網(wǎng)易云課堂等。
  • 直播:游戲直播、課程直播等。

P2P通信原理

P2P 通信即點(diǎn)對(duì)點(diǎn)通信。

要實(shí)現(xiàn)兩個(gè)客戶(hù)端的實(shí)時(shí)音視頻通信,并且這兩個(gè)客戶(hù)端可能處于不同網(wǎng)絡(luò)環(huán)境,使用不同的設(shè)備,都需要解決哪些問(wèn)題?

主要是下面這 3 個(gè)問(wèn)題:

  • 如何發(fā)現(xiàn)對(duì)方?
  • 不同的音視頻編解碼能力如何溝通?
  • 如何聯(lián)系上對(duì)方?

下面我們將逐個(gè)討論這 3 個(gè)問(wèn)題。

如何發(fā)現(xiàn)對(duì)方?

在 P2P 通信的過(guò)程中,雙方需要交換一些元數(shù)據(jù)比如媒體信息、網(wǎng)絡(luò)數(shù)據(jù)等等信息,我們通常稱(chēng)這一過(guò)程叫做“信令(signaling)”。

對(duì)應(yīng)的服務(wù)器即“信令服務(wù)器 (signaling server)”,通常也有人將之稱(chēng)為“房間服務(wù)器”,因?yàn)樗粌H可以交換彼此的媒體信息和網(wǎng)絡(luò)信息,同樣也可以管理房間信息。

比如:

1)通知彼此 who 加入了房間;2)who 離開(kāi)了房間 3)告訴第三方房間人數(shù)是否已滿(mǎn)是否可以加入房間。

為了避免出現(xiàn)冗余,并最大限度地提高與已有技術(shù)的兼容性,WebRTC 標(biāo)準(zhǔn)并沒(méi)有規(guī)定信令方法和協(xié)議。在本課程中會(huì)使用websocket來(lái)搭建一個(gè)信令服務(wù)器

不同的音視頻編解碼能力如何溝通?

不同瀏覽器對(duì)于音視頻的編解碼能力是不同的。

比如: 以日常生活中的例子來(lái)講,小李會(huì)講漢語(yǔ)和英語(yǔ),而小王會(huì)講漢語(yǔ)和法語(yǔ)。為了保證雙方都可以正確的理解對(duì)方的意思,最簡(jiǎn)單的辦法即取他們都會(huì)的語(yǔ)言,也就是漢語(yǔ)來(lái)溝通。

在 WebRTC 中:有一個(gè)專(zhuān)門(mén)的協(xié)議,稱(chēng)為 Session Description Protocol(SDP),可以用于描述上述這類(lèi)信息。

因此:參與音視頻通訊的雙方想要了解對(duì)方支持的媒體格式,必須要交換 SDP 信息。而交換 SDP 的過(guò)程,通常稱(chēng)之為媒體協(xié)商。

如何聯(lián)系上對(duì)方?

其實(shí)就是網(wǎng)絡(luò)協(xié)商的過(guò)程,即參與音視頻實(shí)時(shí)通信的雙方要了解彼此的網(wǎng)絡(luò)情況,這樣才有可能找到一條相互通訊的鏈路。

理想的網(wǎng)絡(luò)情況是每個(gè)客戶(hù)端都有自己的私有公網(wǎng) IP 地址,這樣的話(huà)就可以直接進(jìn)行點(diǎn)對(duì)點(diǎn)連接。實(shí)際上呢,出于網(wǎng)絡(luò)安全和其他原因的考慮,大多數(shù)客戶(hù)端之間都是在某個(gè)局域網(wǎng)內(nèi),需要網(wǎng)絡(luò)地址轉(zhuǎn)換(NAT)。

在 WebRTC 中我們使用 ICE 機(jī)制建立網(wǎng)絡(luò)連接。ICE 協(xié)議通過(guò)一系列的技術(shù)(如 STUN、TURN 服務(wù)器)幫助通信雙方發(fā)現(xiàn)和協(xié)商可用的公共網(wǎng)絡(luò)地址,從而實(shí)現(xiàn) NAT 穿越。

ICE 的工作原理如下:

  • 首先,通信雙方收集本地網(wǎng)絡(luò)地址(包括私有地址和公共地址)以及通過(guò) STUN 和 TURN 服務(wù)器獲取的候選地址。
  • 接下來(lái),雙方通過(guò)信令服務(wù)器交換這些候選地址。
  • 通信雙方使用這些候選地址進(jìn)行連接測(cè)試,確定最佳的可用地址。
  • 一旦找到可用的地址,通信雙方就可以開(kāi)始實(shí)時(shí)音視頻通話(huà)。

在 WebRTC 中網(wǎng)絡(luò)信息通常用candidate來(lái)描述

針對(duì)上面三個(gè)問(wèn)題的總結(jié):就是通過(guò) WebRTC 提供的 API 獲取各端的媒體信息 SDP 以及 網(wǎng)絡(luò)信息 candidate ,并通過(guò)信令服務(wù)器交換,進(jìn)而建立了兩端的連接通道完成實(shí)時(shí)視頻語(yǔ)音通話(huà)。

常用的API

音視頻采集getUserMedia

// 獲取本地音視頻流
const getLocalStream = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({ // 獲取音視頻流
    audio: true,
    video: true
  })
  localVideo.value!.srcObject = stream
  localVideo.value!.play()
  return stream
}

核心對(duì)象 RTCPeerConnection

RTCPeerConnection 作為創(chuàng)建點(diǎn)對(duì)點(diǎn)連接的 API,是我們實(shí)現(xiàn)音視頻實(shí)時(shí)通信的關(guān)鍵。

const peer = new RTCPeerConnection({
  // iceServers: [
  //   { url: "stun:stun.l.google.com:19302" }, // 谷歌的公共服務(wù)
  //   {
  //     urls: "turn:***",
  //     credential: "***",
  //     username: "***",
  //   },
  // ],
});

主要會(huì)用到以下幾個(gè)方法:

媒體協(xié)商方法:

  • createOffer
  • createAnswer
  • setLocalDesccription
  • setRemoteDesccription

重要事件:

  • onicecandidate
  • onaddstream

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-xQgwOnyO-1688028103102)(./03.png)]

整個(gè)媒體協(xié)商過(guò)程可以簡(jiǎn)化為三個(gè)步驟對(duì)應(yīng)上述四個(gè)媒體協(xié)商方法:

  • 呼叫端創(chuàng)建 Offer(createOffer)并將 offer 消息(內(nèi)容是呼叫端的 SDP 信息)通過(guò)信令服務(wù)器傳送給接收端,同時(shí)調(diào)用 setLocalDesccription 將含有本地 SDP 信息的 Offer 保存起來(lái)
  • 接收端收到對(duì)端的 Offer 信息后調(diào)用 setRemoteDesccription 方法將含有對(duì)端 SDP 信息的 Offer 保存起來(lái),并創(chuàng)建 Answer(createAnswer)并將 Answer 消息(內(nèi)容是接收端的 SDP 信息)通過(guò)信令服務(wù)器傳送給呼叫端
  • 呼叫端收到對(duì)端的 Answer 信息后調(diào)用 setRemoteDesccription 方法將含有對(duì)端 SDP 信息的 Answer 保存起來(lái)

經(jīng)過(guò)上述三個(gè)步驟,則完成了 P2P 通信過(guò)程中的媒體協(xié)商部分,實(shí)際上在呼叫端以及接收端調(diào)用setLocalDesccription 同時(shí)也開(kāi)始了收集各端自己的網(wǎng)絡(luò)信息(candidate),然后各端通過(guò)監(jiān)聽(tīng)事件 onicecandidate 收集到各自的 candidate 并通過(guò)信令服務(wù)器傳送給對(duì)端,進(jìn)而打通 P2P 通信的網(wǎng)絡(luò)通道,并通過(guò)監(jiān)聽(tīng) onaddstream 事件拿到對(duì)方的視頻流進(jìn)而完成了整個(gè)視頻通話(huà)過(guò)程。

實(shí)踐

項(xiàng)目搭建

前端項(xiàng)目 項(xiàng)目使用vue3+ts,運(yùn)行如下命令:

npm create vite@latest webrtc-client -- --template vue-ts

并且引入tailwindcss:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

在生成的 tailwind.config.js 配置文件中添加所有模板文件的路徑。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

修改style.css中的內(nèi)容如下:

@tailwind base;
@tailwind components;
@tailwind utilities;

自定義修改App.vue中的內(nèi)容如下:

<script lang="ts" setup>
import { ref } from 'vue'
const called = ref<boolean>(false) // 是否是接收方
const caller = ref<boolean>(false) // 是否是發(fā)起方
const calling = ref<boolean>(false) // 呼叫中
const communicating = ref<boolean>(false) // 視頻通話(huà)中
const localVideo = ref<HTMLVideoElement>() // video標(biāo)簽實(shí)例,播放本人的視頻
const remoteVideo = ref<HTMLVideoElement>() // video標(biāo)簽實(shí)例,播放對(duì)方的視頻
// 發(fā)起方發(fā)起視頻請(qǐng)求
const callRemote = () => {
  console.log('發(fā)起視頻');
}
// 接收方同意視頻請(qǐng)求
const acceptCall = () => {
  console.log('同意視頻邀請(qǐng)');
}
// 掛斷視頻
const hangUp = () => {
  console.log('掛斷視頻');
}
</script>
<template>
  <div class="flex items-center flex-col text-center p-12 h-screen">
    <div class="relative h-full mb-4">
      <video
        ref="localVideo" 
        class="w-96 h-full bg-gray-200 mb-4 object-cover"
      ></video>
      <video
        ref="remoteVideo"
        class="w-32 h-48 absolute bottom-0 right-0 object-cover"
      ></video>
      <div v-if="caller && calling" class="absolute top-2/3 left-36 flex flex-col items-center">
        <p class="mb-4 text-white">等待對(duì)方接聽(tīng)...</p>
        <img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer" alt="">
      </div>
      <div v-if="called && calling" class="absolute top-2/3 left-32 flex flex-col items-center">
        <p class="mb-4 text-white">收到視頻邀請(qǐng)...</p>
        <div class="flex">
          <img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer mr-4" alt="">
          <img @click="acceptCall" src="/accept.svg" class="w-16 cursor-pointer" alt="">
        </div>
      </div>
    </div>
    <div class="flex gap-2 mb-4">
      <button 
        class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white" 
        @click="callRemote"
      >發(fā)起視頻</button>
      <button 
        class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white" 
        @click="hangUp"
      >掛斷視頻</button>
    </div>
  </div>
</template>

執(zhí)行完上面的步驟就可以運(yùn)行npm run dev來(lái)在本地啟動(dòng)項(xiàng)目了

后端項(xiàng)目

創(chuàng)建一個(gè)webrtc-server的文件夾,執(zhí)行npm init ,一路回車(chē)即可,然后運(yùn)行如下命令安裝socket.ionodemon

npm install socket.io nodemon

創(chuàng)建index.js的文件,并添加如下內(nèi)容:

const socket = require('socket.io');
const http = require('http');
const server = http.createServer()
const io = socket(server, {
  cors: {
    origin: '*' // 配置跨域
  }
});
io.on('connection', sock => {
  console.log('連接成功...')
  // 向客戶(hù)端發(fā)送連接成功的消息
  sock.emit('connectionSuccess');
})
server.listen(3000, () => {
  console.log('服務(wù)器啟動(dòng)成功');
});

package.json中添加start命令,使用nodemon啟動(dòng)項(xiàng)目:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "nodemon index.js"
},

執(zhí)行完后運(yùn)行npm run start即在3000端口可啟動(dòng)node服務(wù)了

前端連接信令服務(wù)器

前端需要安裝socket.io-client, 并連接信令服務(wù)器:

<script setup lang="ts">
  // App.vue
  import { ref, onMounted, onUnmounted } from 'vue'
	import { io, Socket } from "socket.io-client";
  // ...
  const socket = ref<Socket>() // Socket實(shí)例
  onMounted(() => {
    const sock = io('localhost:3000'); // 對(duì)應(yīng)服務(wù)的端口
    // 連接成功
    sock.on('connectionSuccess', () => {
      console.log('連接成功')
    });
    socket.value = sock;
  })
  // ...
</script>

發(fā)起視頻請(qǐng)求

角色:用戶(hù)A–發(fā)起方,用戶(hù)B–接收方

房間:類(lèi)比聊天窗口

連接成功時(shí)加入房間:

// 前端代碼
const roomId = '001'
sock.on('connectionSuccess', () => {
  console.log('連接服務(wù)器成功...');
  sock.emit('joinRoom', roomId) // 前端發(fā)送加入房間事件
})
// 服務(wù)端代碼
sock.on('joinRoom', (roomId) => {
  sock.join(roomId) // 加入房間
})

用戶(hù)A發(fā)起視頻請(qǐng)求并通知用戶(hù)B: 用戶(hù)A發(fā)起視頻請(qǐng)求,并且通過(guò)信令服務(wù)器通知用戶(hù)B

// 發(fā)起方發(fā)起視頻請(qǐng)求
const callRemote = async () => {
  console.log('發(fā)起視頻');
  caller.value = true;
  calling.value = true;
  await getLocalStream()
	// 向信令服務(wù)器發(fā)送發(fā)起請(qǐng)求的事件
  socket.value?.emit('callRemote', roomId)
}

用戶(hù)B同意視頻請(qǐng)求,并且通過(guò)信令服務(wù)器通知用戶(hù)A

// 接收方同意視頻請(qǐng)求
const acceptCall = () => {
  console.log('同意視頻邀請(qǐng)');
  socket.value?.emit('acceptCall', roomId)
}

開(kāi)始交換 SDP 信息和 candidate 信息: 用戶(hù)A創(chuàng)建創(chuàng)建RTCPeerConnection,添加本地音視頻流,生成offer,并且通過(guò)信令服務(wù)器將offer發(fā)送給用戶(hù)B

// 創(chuàng)建RTCPeerConnection
peer.value = new RTCPeerConnection()
// 添加本地音視頻流
peer.value.addStream(localStream.value)
// 生成offer
const offer = await peer.value.createOffer({
  offerToReceiveAudio: 1,
  offerToReceiveVideo: 1
})
console.log('offer', offer);
// 設(shè)置本地描述的offer
await peer.value.setLocalDescription(offer);
// 通過(guò)信令服務(wù)器將offer發(fā)送給用戶(hù)B
socket.value?.emit('sendOffer', { offer, roomId })

用戶(hù)B收到用戶(hù)A的offer

sock.on('sendOffer', (offer) => {
  if (called.value) { // 判斷接收方
    console.log('收到offer', offer);
  }
})

用戶(hù)B需要?jiǎng)?chuàng)建自己的RTCPeerConnection,添加本地音視頻流,設(shè)置遠(yuǎn)端描述信息,生成answer,并且通過(guò)信令服務(wù)器發(fā)送給用戶(hù)A

// 創(chuàng)建RTCPeerConnection
peer.value = new RTCPeerConnection()
// 添加本地音視頻流
peer.value.addStream(localStream.value)
// 生成offer
const offer = await peer.value.createOffer({
  offerToReceiveAudio: 1,
  offerToReceiveVideo: 1
})
console.log('offer', offer);
// 設(shè)置本地描述的offer
await peer.value.setLocalDescription(offer);
// 通過(guò)信令服務(wù)器將offer發(fā)送給用戶(hù)B
socket.value?.emit('sendOffer', { offer, roomId })

用戶(hù)A收到用戶(hù)B的answer

sock.on('sendAnswer', (answer) => {
  if (caller.value) { // 判斷是否是發(fā)送方
    // 設(shè)置遠(yuǎn)端answer信息
    peer.value.setRemoteDescription(answer);
  }
})

用戶(hù)A獲取candidate信息并且通過(guò)信令服務(wù)器發(fā)送candidate給用戶(hù)B

// 通過(guò)監(jiān)聽(tīng)onicecandidate事件獲取candidate信息
peer.value.onicecandidate = (event: any) => {
  if (event.candidate) {
    console.log('用戶(hù)A獲取candidate信息', event.candidate);
    // 通過(guò)信令服務(wù)器發(fā)送candidate信息給用戶(hù)B
    socket.value?.emit('sendCandidate', {
      roomId,
      candidate: event.candidate
    })
  }
}

用戶(hù)B添加用戶(hù)A的candidate信息

// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
  await peer.value.addIceCandidate(candidate);
})

用戶(hù)B獲取candidate信息并且通過(guò)信令服務(wù)器發(fā)送candidate給用戶(hù)A(如上)

peer.value.onicecandidate = (event: any) => {
  if (event.candidate) {
    console.log('用戶(hù)B獲取candidate信息', event.candidate);
    // 通過(guò)信令服務(wù)器發(fā)送candidate信息給用戶(hù)A
    socket.value?.emit('sendCandidate', {
      roomId,
      candidate: event.candidate
    })
  }
}

用戶(hù)A添加用戶(hù)B的candidate信息(如上)

// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
  await peer.value.addIceCandidate(candidate);
})

接下來(lái)用戶(hù)A和用戶(hù)B就可以進(jìn)行P2P通信流

// 監(jiān)聽(tīng)onaddstream來(lái)獲取對(duì)方的音視頻流
peer.value.onaddstream = (event: any) => {
  calling.value = false;
  communicating.value = true;
  remoteVideo.value!.srcObject = event.stream
  remoteVideo.value!.play()
}

掛斷視頻

// 掛斷視頻
const hangUp = () => {
  console.log('掛斷視頻');
  socket.value?.emit('hangUp', roomId)
}
// 狀態(tài)復(fù)原
const reset = () => {
  called.value = false
  caller.value = false
  calling.value = false
  communicating.value = false
  peer.value = null
  localVideo.value!.srcObject = null
  remoteVideo.value!.srcObject = null
  localStream.value = undefined
}

拓展:peerjs

文檔:https://peerjs.com/docs/#start

服務(wù)端實(shí)現(xiàn)

// 使用peer搭建信令服務(wù)器
const { PeerServer } = require('peer');
const peerServer = PeerServer({ port: 3001, path: '/myPeerServer' });

前端實(shí)現(xiàn)

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Peer } from "peerjs";
const url = ref<string>()
const localVideo = ref<HTMLVideoElement>()
const remoteVideo = ref<HTMLVideoElement>()
const peerId = ref<string>()
const remoteId = ref<string>()
const peer = ref<any>()
const caller = ref<boolean>(false)
const called = ref<boolean>(false)
const callObj = ref<any>(false)
onMounted(() => {
  // 
  peer.value = new Peer({ // 連接信令服務(wù)器
    host: 'localhost',
    port: 3001,
    path: '/myPeerServer'
  });
  peer.value.on('open', (id: string) => {
    peerId.value = id
  })
  // 接收視頻請(qǐng)求
  peer.value.on('call', async (call: any) => {
    called.value = true
    callObj.value = call
  });
})
// 獲取本地音視頻流
async function getLocalStream(constraints: MediaStreamConstraints) {
  // 獲取媒體流
  const stream = await navigator.mediaDevices.getUserMedia(constraints)
  // 將媒體流設(shè)置到 video 標(biāo)簽上播放
  localVideo.value!.srcObject = stream;
  localVideo.value!.play();
  return stream
}
const acceptCalled = async () => {
  // 接收視頻
  const stream = await getLocalStream({ video: true, audio: true })
  callObj.value.answer(stream);
  callObj.value.on('stream', (remoteStream: any) => {
    called.value = false
    // 將遠(yuǎn)程媒體流添加到 video 元素中
    remoteVideo.value!.srcObject = remoteStream;
    remoteVideo.value!.play();
  });
}
// 開(kāi)啟視頻
const callRemote = async () => {
  if (!remoteId.value) {
    alert('請(qǐng)輸入對(duì)方ID')
    return
  }
  const stream = await getLocalStream({ video: true, audio: true })
  // 將本地媒體流發(fā)送給遠(yuǎn)程 Peer
  const call = peer.value.call(remoteId.value, stream);
  caller.value = true
  call.on('stream', (remoteStream: any) => {
    caller.value = false
    // 將遠(yuǎn)程媒體流添加到 video 元素中
    remoteVideo.value!.srcObject = remoteStream;
    remoteVideo.value!.play();
  });
}
</script>

視頻教程 基于WebRTC實(shí)現(xiàn)音視頻通話(huà)

到此這篇關(guān)于基于WebRTC實(shí)現(xiàn)音視頻通話(huà)的文章就介紹到這了,更多相關(guān)WebRTC音視頻通話(huà)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論