亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

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

背景

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

應(yīng)用場景

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

P2P通信原理

P2P 通信即點對點通信。

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

主要是下面這 3 個問題:

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

下面我們將逐個討論這 3 個問題。

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

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

對應(yīng)的服務(wù)器即“信令服務(wù)器 (signaling server)”,通常也有人將之稱為“房間服務(wù)器”,因為它不僅可以交換彼此的媒體信息和網(wǎng)絡(luò)信息,同樣也可以管理房間信息。

比如:

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

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

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

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

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

在 WebRTC 中:有一個專門的協(xié)議,稱為 Session Description Protocol(SDP),可以用于描述上述這類信息。

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

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

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

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

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

ICE 的工作原理如下:

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

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

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

常用的API

音視頻采集getUserMedia

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

核心對象 RTCPeerConnection

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

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

主要會用到以下幾個方法:

媒體協(xié)商方法:

  • createOffer
  • createAnswer
  • setLocalDesccription
  • setRemoteDesccription

重要事件:

  • onicecandidate
  • onaddstream

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

整個媒體協(xié)商過程可以簡化為三個步驟對應(yīng)上述四個媒體協(xié)商方法:

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

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

實踐

項目搭建

前端項目 項目使用vue3+ts,運行如下命令:

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) // 視頻通話中
const localVideo = ref<HTMLVideoElement>() // video標(biāo)簽實例,播放本人的視頻
const remoteVideo = ref<HTMLVideoElement>() // video標(biāo)簽實例,播放對方的視頻
// 發(fā)起方發(fā)起視頻請求
const callRemote = () => {
  console.log('發(fā)起視頻');
}
// 接收方同意視頻請求
const acceptCall = () => {
  console.log('同意視頻邀請');
}
// 掛斷視頻
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">等待對方接聽...</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">收到視頻邀請...</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í)行完上面的步驟就可以運行npm run dev來在本地啟動項目了

后端項目

創(chuàng)建一個webrtc-server的文件夾,執(zhí)行npm init ,一路回車即可,然后運行如下命令安裝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('連接成功...')
  // 向客戶端發(fā)送連接成功的消息
  sock.emit('connectionSuccess');
})
server.listen(3000, () => {
  console.log('服務(wù)器啟動成功');
});

package.json中添加start命令,使用nodemon啟動項目:

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

執(zhí)行完后運行npm run start即在3000端口可啟動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實例
  onMounted(() => {
    const sock = io('localhost:3000'); // 對應(yīng)服務(wù)的端口
    // 連接成功
    sock.on('connectionSuccess', () => {
      console.log('連接成功')
    });
    socket.value = sock;
  })
  // ...
</script>

發(fā)起視頻請求

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

房間:類比聊天窗口

連接成功時加入房間:

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

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

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

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

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

開始交換 SDP 信息和 candidate 信息: 用戶A創(chuàng)建創(chuàng)建RTCPeerConnection,添加本地音視頻流,生成offer,并且通過信令服務(wù)器將offer發(fā)送給用戶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);
// 通過信令服務(wù)器將offer發(fā)送給用戶B
socket.value?.emit('sendOffer', { offer, roomId })

用戶B收到用戶A的offer

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

用戶B需要創(chuàng)建自己的RTCPeerConnection,添加本地音視頻流,設(shè)置遠(yuǎn)端描述信息,生成answer,并且通過信令服務(wù)器發(fā)送給用戶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);
// 通過信令服務(wù)器將offer發(fā)送給用戶B
socket.value?.emit('sendOffer', { offer, roomId })

用戶A收到用戶B的answer

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

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

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

用戶B添加用戶A的candidate信息

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

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

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

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

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

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

// 監(jiān)聽onaddstream來獲取對方的音視頻流
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ù)端實現(xiàn)

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

前端實現(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
  })
  // 接收視頻請求
  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();
  });
}
// 開啟視頻
const callRemote = async () => {
  if (!remoteId.value) {
    alert('請輸入對方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實現(xiàn)音視頻通話

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

相關(guān)文章

最新評論