基于WebRTC實(shí)現(xiàn)音視頻通話(huà)功能
隨著互聯(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
整個(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.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('連接成功...') // 向客戶(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)文章希望大家以后多多支持腳本之家!
- 基于WebRTC實(shí)現(xiàn)音視頻通話(huà)功能
- vue項(xiàng)目基于WebRTC實(shí)現(xiàn)一對(duì)一音視頻通話(huà)
- C# WebApi+Webrtc局域網(wǎng)音視頻通話(huà)實(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-10vue3中el-table實(shí)現(xiàn)表格合計(jì)行的示例代碼
這篇文章主要介紹了vue3中el-table實(shí)現(xiàn)表格合計(jì)行,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01Vue自定義過(guò)濾器格式化數(shù)字三位加一逗號(hào)實(shí)現(xiàn)代碼
這篇文章主要介紹了Vue自定義過(guò)濾器格式化數(shù)字三位加一逗號(hào)的實(shí)現(xiàn)代碼,需要的朋友可以參考下2018-03-03vue加載視頻流,實(shí)現(xiàn)直播功能的過(guò)程
這篇文章主要介紹了vue加載視頻流,實(shí)現(xiàn)直播功能的過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04vue表格n-form中自定義增加必填星號(hào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue表格n-form中自定義增加必填星號(hào),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-12-12vue-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-12vue跳轉(zhuǎn)同一路由報(bào)錯(cuò)的問(wèn)題及解決
這篇文章主要介紹了vue跳轉(zhuǎn)同一路由報(bào)錯(cuò)的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04vue播放flv、m3u8視頻流(監(jiān)控)的方法實(shí)例
隨著前端大屏頁(yè)面的逐漸壯大,客戶(hù)的...其中實(shí)時(shí)播放監(jiān)控的需求逐步增加,視頻流格式也是有很多種,用到最多的.flv、.m3u8,下面這篇文章主要給大家介紹了關(guān)于vue播放flv、m3u8視頻流(監(jiān)控)的相關(guān)資料,需要的朋友可以參考下2023-04-04