基于WebRTC實(shí)現(xiàn)音視頻通話功能
隨著互聯(lián)網(wǎng)的發(fā)展,實(shí)時(shí)音視頻通話功能已經(jīng)成為遠(yuǎn)程辦公、社交娛樂(lè)和在線教育等領(lǐng)域中不可或缺的一項(xiàng)重要功能。WebRTC作為一種開(kāi)放標(biāo)準(zhǔn)的實(shí)時(shí)通信協(xié)議,能輕松實(shí)現(xiàn)瀏覽器之間的實(shí)時(shí)音視頻通信。
本次主要分享基于WebRTC的音視頻通話技術(shù),講解WebRTC原理和音視頻傳輸?shù)汝P(guān)鍵概念,
通過(guò)案例實(shí)踐,帶大家掌握如何搭建一個(gè)音視頻通話應(yīng)用。
背景
隨著互聯(lián)網(wǎng)技術(shù)的飛速發(fā)展,實(shí)時(shí)音視頻通話已經(jīng)成為在線教育、遠(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í)音視頻通話系統(tǒng)的能力。在本課程中,我們將從0到1使用 WebRTC 構(gòu)建一個(gè)基于 P2P 架構(gòu)的音視頻通話的應(yīng)用案例。
應(yīng)用場(chǎng)景
- 點(diǎn)對(duì)點(diǎn)視頻聊天:如 微信視頻 等實(shí)時(shí)視頻通話應(yīng)用。
- 多人視頻會(huì)議:企業(yè)級(jí)多人視頻會(huì)議系統(tǒng),如飛書(shū)、釘釘、騰訊會(huì)議等。
- 在線教育:如騰訊課堂、網(wǎng)易云課堂等。
- 直播:游戲直播、課程直播等。
P2P通信原理
P2P 通信即點(diǎn)對(duì)點(diǎn)通信。

要實(shí)現(xiàn)兩個(gè)客戶端的實(shí)時(shí)音視頻通信,并且這兩個(gè)客戶端可能處于不同網(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ù)等等信息,我們通常稱這一過(guò)程叫做“信令(signaling)”。
對(duì)應(yīng)的服務(wù)器即“信令服務(wù)器 (signaling server)”,通常也有人將之稱為“房間服務(wù)器”,因?yàn)樗粌H可以交換彼此的媒體信息和網(wǎng)絡(luò)信息,同樣也可以管理房間信息。
比如:
1)通知彼此 who 加入了房間;2)who 離開(kāi)了房間 3)告訴第三方房間人數(shù)是否已滿是否可以加入房間。
為了避免出現(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è)專門(mén)的協(xié)議,稱為 Session Description Protocol(SDP),可以用于描述上述這類信息。
因此:參與音視頻通訊的雙方想要了解對(duì)方支持的媒體格式,必須要交換 SDP 信息。而交換 SDP 的過(guò)程,通常稱之為媒體協(xié)商。
如何聯(lián)系上對(duì)方?
其實(shí)就是網(wǎng)絡(luò)協(xié)商的過(guò)程,即參與音視頻實(shí)時(shí)通信的雙方要了解彼此的網(wǎng)絡(luò)情況,這樣才有可能找到一條相互通訊的鏈路。
理想的網(wǎng)絡(luò)情況是每個(gè)客戶端都有自己的私有公網(wǎng) IP 地址,這樣的話就可以直接進(jìn)行點(diǎn)對(duì)點(diǎn)連接。實(shí)際上呢,出于網(wǎng)絡(luò)安全和其他原因的考慮,大多數(shù)客戶端之間都是在某個(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í)音視頻通話。

在 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ǔ)音通話。
常用的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)]](http://img.jbzj.com/file_images/article/202405/2024053111330783.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è)視頻通話過(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) // 視頻通話中
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 ,一路回車即可,然后運(yùn)行如下命令安裝socket.io和nodemon:
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ù)器啟動(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)求
角色:用戶A–發(fā)起方,用戶B–接收方
房間:類比聊天窗口
連接成功時(shí)加入房間:
// 前端代碼
const roomId = '001'
sock.on('connectionSuccess', () => {
console.log('連接服務(wù)器成功...');
sock.emit('joinRoom', roomId) // 前端發(fā)送加入房間事件
})
// 服務(wù)端代碼
sock.on('joinRoom', (roomId) => {
sock.join(roomId) // 加入房間
})用戶A發(fā)起視頻請(qǐng)求并通知用戶B: 用戶A發(fā)起視頻請(qǐng)求,并且通過(guò)信令服務(wù)器通知用戶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)
}用戶B同意視頻請(qǐng)求,并且通過(guò)信令服務(wù)器通知用戶A
// 接收方同意視頻請(qǐng)求
const acceptCall = () => {
console.log('同意視頻邀請(qǐng)');
socket.value?.emit('acceptCall', roomId)
}開(kāi)始交換 SDP 信息和 candidate 信息: 用戶A創(chuàng)建創(chuàng)建RTCPeerConnection,添加本地音視頻流,生成offer,并且通過(guò)信令服務(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);
// 通過(guò)信令服務(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需要?jiǎng)?chuàng)建自己的RTCPeerConnection,添加本地音視頻流,設(shè)置遠(yuǎn)端描述信息,生成answer,并且通過(guò)信令服務(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);
// 通過(guò)信令服務(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信息并且通過(guò)信令服務(wù)器發(fā)送candidate給用戶B
// 通過(guò)監(jiān)聽(tīng)onicecandidate事件獲取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
console.log('用戶A獲取candidate信息', event.candidate);
// 通過(guò)信令服務(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信息并且通過(guò)信令服務(wù)器發(fā)送candidate給用戶A(如上)
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
console.log('用戶B獲取candidate信息', event.candidate);
// 通過(guò)信令服務(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);
})接下來(lái)用戶A和用戶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)音視頻通話
到此這篇關(guān)于基于WebRTC實(shí)現(xiàn)音視頻通話的文章就介紹到這了,更多相關(guān)WebRTC音視頻通話內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于WebRTC實(shí)現(xiàn)音視頻通話功能
- vue項(xiàng)目基于WebRTC實(shí)現(xiàn)一對(duì)一音視頻通話
- C# WebApi+Webrtc局域網(wǎng)音視頻通話實(shí)例
- 使用VUE和webrtc-streamer實(shí)現(xiàn)實(shí)時(shí)視頻播放(監(jiān)控設(shè)備-rtsp)
- WebRTC媒體權(quán)限申請(qǐng)getUserMedia實(shí)例詳解
- 5分鐘搭建一個(gè)WebRTC視頻聊天
- 在Ubuntu上搭建一個(gè)基于webrtc的多人視頻聊天服務(wù)實(shí)例代碼詳解
- 詳解python的webrtc庫(kù)實(shí)現(xiàn)語(yǔ)音端點(diǎn)檢測(cè)
相關(guān)文章
vue模板配置與webstorm代碼格式規(guī)范設(shè)置
這篇文章主要介紹了vue模板配置與webstorm代碼格式規(guī)范設(shè)置詳細(xì)的相關(guān)資料,需要的朋友可以參考一下文章得具體內(nèi)容,希望對(duì)你有所幫助2021-10-10
vue3中el-table實(shí)現(xiàn)表格合計(jì)行的示例代碼
這篇文章主要介紹了vue3中el-table實(shí)現(xiàn)表格合計(jì)行,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
Vue自定義過(guò)濾器格式化數(shù)字三位加一逗號(hào)實(shí)現(xiàn)代碼
這篇文章主要介紹了Vue自定義過(guò)濾器格式化數(shù)字三位加一逗號(hào)的實(shí)現(xiàn)代碼,需要的朋友可以參考下2018-03-03
vue加載視頻流,實(shí)現(xiàn)直播功能的過(guò)程
這篇文章主要介紹了vue加載視頻流,實(shí)現(xiàn)直播功能的過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
vue表格n-form中自定義增加必填星號(hào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue表格n-form中自定義增加必填星號(hào),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-12-12
vue-echarts高度縮小時(shí)autoresize失效的原因和解決辦法
Vue-Echarts是一個(gè)基于ECharts封裝的輕量級(jí)、易用的圖表組件庫(kù),它允許你在Vue.js應(yīng)用中方便地集成ECharts,這是一個(gè)強(qiáng)大而直觀的數(shù)據(jù)可視化庫(kù),本文給大家介紹了vue-echarts高度縮小時(shí)autoresize失效的原因和解決辦法,需要的朋友可以參考下2024-12-12
vue跳轉(zhuǎn)同一路由報(bào)錯(cuò)的問(wèn)題及解決
這篇文章主要介紹了vue跳轉(zhuǎn)同一路由報(bào)錯(cuò)的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
vue播放flv、m3u8視頻流(監(jiān)控)的方法實(shí)例
隨著前端大屏頁(yè)面的逐漸壯大,客戶的...其中實(shí)時(shí)播放監(jiān)控的需求逐步增加,視頻流格式也是有很多種,用到最多的.flv、.m3u8,下面這篇文章主要給大家介紹了關(guān)于vue播放flv、m3u8視頻流(監(jiān)控)的相關(guān)資料,需要的朋友可以參考下2023-04-04

