篇文章開始會實現一個一對一WebRTC和多對多的WebRTC,以及基于屏幕共享的錄制。本篇會實現信令和前端部分,信令使用fastity來搭建,前端部分使用Vue3來實現。
WebRTC全稱Web Real-Time Communication,是一種實時音視頻的技術,它的優勢是低延時。
廢話不多說,現在開始搭建環境,首先是需要開啟socket服務,采用的是fastify來進行搭建。詳情可以見文檔地址,本例使用的是3.x來啟動的。接下來安裝fastify-socket.io3.0.0插件,詳細配置可以見文檔,此處不做詳細解釋。接下來是搭建Vue3,使用 vite 腳手架搭建簡單的demo。
要求:前端服務運行在localhost或者https下。node需要redis進行數據緩存
C++音視頻開發WebRTC學習資料:點擊領取→音視頻開發(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
要實現實時音視頻第一步當然是要能獲取到視頻流,在這里我們使用瀏覽器提供的API,MediaDevices來進行攝像頭流的捕獲
第一個要介紹的API是enumerateDevices,是請求一個可用的媒體輸入和輸出設備的列表,例如麥克風,攝像機,耳機設備等。直接在控制臺執行API,獲取的設備如圖
我們注意到里面返回的設備ID和label是空的,這是由于瀏覽器的安全策略限制,必須授權攝像頭或麥克風才能允許返回設備ID和設備標簽,接下來我們介紹如何請求攝像頭和麥克風
這個API顧名思義,就是去獲取用戶的Meida的,那我們直接執行這個API來看看效果
ps: 由于掘金的代碼片段的iframe沒有配置allow="display-capture *;microphone *; camera *"屬性,需要手動打開詳情查看效果
通過上述例子我們可以獲取到本機的音視頻畫面,并且可以播放在video標簽里,那么我們可以在獲取了用戶的流之后,重新再獲取一次設備列表看看發生了什么變化
在獲取了音視頻之后,獲取的設備列表的詳細信息已經出現,我們就可以獲取指定設備的音視頻數據,
這里介紹一下getUserMedia的參數constraints,
interface MediaTrackConstraintSet {
// 畫面比例
aspectRatio?: ConstrainDouble;
// 設備ID,可以從enumerateDevices中獲取
deviceId?: ConstrainDOMString;
// 攝像頭前后置模式,一般適用于手機
facingMode?: ConstrainDOMString;
// 幀率,采集視頻的目標幀率
frameRate?: ConstrainDouble;
// 組ID,用一個設備的輸入輸出的組ID是同一個
groupId?: ConstrainDOMString;
// 視頻高度
height?: ConstrainULong
// 視頻寬度
width?: ConstrainULong;
}
interface MediaTrackConstraintSet {
// 是否開啟AGC自動增益,可以在原有音量上增加額外的音量
autoGainControl?: ConstrainBoolean;
// 聲道配置
channelCount?: ConstrainULong;
// 設備ID,可以從enumerateDevices中獲取
deviceId?: ConstrainDOMString;
// 是否開啟回聲消除
echoCancellation?: ConstrainBoolean;
// 組ID,用一個設備的輸入輸出的組ID是同一個
groupId?: ConstrainDOMString;
// 延遲大小
latency?: ConstrainDouble;
// 是否開啟降噪
noiseSuppression?: ConstrainBoolean;
// 采樣率單位Hz
sampleRate?: ConstrainULong;
// 采樣大小,單位位
sampleSize?: ConstrainULong;
// 本地音頻在本地揚聲器播放
suppressLocalAudioPlayback?: ConstrainBoolean;
}
C++音視頻開發WebRTC學習資料:點擊領取→音視頻開發(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
當我們采集到了音視頻數據,接下來就是要建立鏈接,在開始之前需要科普一下WebRTC的工作方式,我們常見有三種WebRTC的網絡結構
在這里由于設備的限制,我們采用Mesh的方案來進行開發
我們建立一對一的鏈接需要知道后流程是怎么流轉的,接下來上一張圖,便可以清晰的了解
這里是由ClientA發起B來接受A的視頻數據。上圖總結可以為A創建本地視頻流,把視頻流添加到PeerConnection里面 創建一個Offer給B,B收到Offer以后,保存這個offer,并響應這個Offer給A,A收到B的響應后保存A的遠端響應,進行NAT穿透,完成鏈接建立。
話已經講了這么多,我們該怎么建立呢,光說不做假把式,接下來,用我們的項目創建一個來試試
首先啟動fastify服務,接下來在Vue項目安裝socket.io-client@4然后連接服務端的socket
import { v4 as uuid } from 'uuid';
import { io, Socket } from 'socket.io-client';
const myUserId=ref(uuid());
let socket: Socket;
socket=io('http://127.0.0.1:7070', {
query: {
// 房間號,由輸入框輸入獲得
room: room.value,
// userId通過uuid獲取
userId: myUserId.value,
// 昵稱,由輸入框輸入獲得
nick: nick.value
}
});
可以查看chrome的控制臺,檢查ws的鏈接情況,如果出現跨域,請查看socket.io的server配置并開啟cors配置。
開始創建RTCPeerConnection,這里采用google的公共stun服務
const peerConnect=new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302"
}
]
})
根據上面的流程圖我們下一步要做的事情是用上面的方式獲取視頻流,并將獲取到的流添加到RTCPeerConnection中,并創建offer,把這個offer設置到這個rtcPeer中,并把offer發送給socket服務
let localStream: MediaStream;
stream.getTracks().forEach((track)=> {
peerConnect.addTrack(track, stream)
})
const offer=await peerConnect.createOffer();
await peerConnect.setLocalDescription(offer);
socket.emit('offer', { creatorUserId: myUserId.value, sdp: offer }, (res: any)=> {
console.log(res);
});
socket 服務收到了這份offer后需要給B發送A的offer
fastify.io.on('connection', async (socket)=> {
socket.on('offer', async (offer, callback)=> {
socket.emit('offer', offer);
callback({
status: "ok"
})
})
})
B需要監聽socket里面的offer事件并創建RTCPeerConnection,將這個offer設置到遠端,接下來來創建響應。并且將這個響應設置到本地,發送answer事件回復給A
socket.on('offer', async (offer: { sdp: RTCSessionDescriptionInit, creatorUserId: string })=> {
const peerConnect=new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302"
}
]
})
await peerConnect.setRemoteDescription(offer.sdp);
const answer=await peerConnect.createAnswer();
await peerConnect.setLocalDescription(answer);
socket.emit('answer', { sdp: answer }, (res: any)=> {
console.log(res);
})
})
服務端廣播answer
socket.on('offer', async (offer, callback)=> {
socket.emit('offer', offer);
callback({
status: "ok"
})
})
A監聽到socket里面的answer事件,需要將剛才的自己的RTCpeer添加遠端描述
socket.on('answer', async (data: { sdp: RTCSessionDescriptionInit })=> {
await peerConnect.setRemoteDescription(data.sdp)
})
接下來A會獲取到ICE候選信息,需要發送給B
peerConnect.onicecandidate=(candidateInfo: RTCPeerConnectionIceEvent)=> {
if (candidateInfo.candidate) {
socket.emit('ICE-candidate', { sdp: candidateInfo.candidate }, (res: any)=> {
console.log(res);
})
}
}
廣播消息是同理這里就不再贅述了,B獲取到了A的ICE,需要設置候選
socket.on('ICE-candidate', async (data: { sdp: RTCIceCandidate })=> {
await peerConnect.addIceCandidate(data.sdp)
})
接下來B也會獲取到ICE候選信息,同理需要發送給A,待A設置完成之后便可以建立鏈接,代碼同上,B接下來會收到流添加的事件,這個事件會有兩次,分別是音頻和視頻的數據
C++音視頻開發WebRTC學習資料:點擊領取→音視頻開發(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
peerConnect.ontrack=(track: RTCTrackEvent)=> {
if (track.track.kind==='video') {
const video=document.createElement('video');
video.srcObject=track.streams[0];
video.autoplay=true;
video.style.setProperty('width', '400px');
video.style.setProperty('aspect-ratio', '16 / 9');
video.setAttribute('id', track.track.id)
document.body.appendChild(video)
}
if (track.track.kind==='audio') {
const audio=document.createElement('audio');
audio.srcObject=track.streams[0];
audio.autoplay=true;
audio.setAttribute('id', track.track.id)
document.body.appendChild(audio)
}
}
到這里你就可以見到兩個視頻建立的P2P鏈接了。到這里為止只是建立了視頻的一對一鏈接,但是我們可以通過這些操作進行復制,就能進行多對多的連接了。
在開始我們需要知道,一個人和另一個人建立連接雙方都需要創建自己的peerConnection。對于多人的情況,首先我們需要知道進入的房間里面當前的人數,給每個人都創建一個RtcPeer,同時收到的人也回復這個offer給發起的人。對于后進入的人,需要讓已經創建音視頻的人給后進入的人創建新的offer。
基于上面的流程,我們現在先實現一個成員列表的接口
在我們登錄socket服務的時候我們在query參數里面有房間號,userId和昵稱,我們可以通過redis記錄對應的房間號的登錄和登出,從而實現成員列表。
可以在某一個人登錄的時候獲取一下redis對應房間的成員列表,如果沒有這個房間,就把這個人丟進新的房間,并且存儲到redis中,方便其他人登錄這個房間的時候知道現在有多少人。
fastify.io.on('connection', async (socket)=> {
const room=socket.handshake.query.room;
const redis=fastify.redis;
let userList;
// 獲取當前房間的數據
await getUserList()
async function getUserList() {
const roomUser=await redis.get(room);
if (roomUser) {
userList=new Map(JSON.parse(roomUser))
} else {
userList=new Map();
}
}
async function setRedisRoom() {
await redis.set(room, JSON.stringify([...userList]))
}
function rmUser(userId) {
userList.delete(userId);
}
if (room) {
// 將這人加入到對應的socket房間
socket.join(room);
await setRedisRoom();
// 廣播有人加入了
socket.to(room).emit('join', userId);
}
// 這個人斷開了鏈接需要將這個人從redis中刪除
socket.on('disconnect', async (socket)=> {
await getUserList();
rmUser(userId);
await setRedisRoom();
})
})
到上面為止,我們實現了成員的記錄、廣播和刪除。接下來是需要實現一個成員列表的接口,提供給前端項目調用。
fastify.get('/userlist', async function (request, reply) {
const redis=fastify.redis;
return await redis.get(request.query.room);
})
由于需要給每個人發送offer,需要對上面的初始化函數進行封裝。
/**
* 創建RTCPeerConnection
* @param creatorUserId 創建者id,本人
* @param recUserId 接收者id
*/
const initPeer=async (creatorUserId: string, recUserId: string)=> {
const peerConnect=new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302"
}
]
})
return peerConnect;
})
由于存在多份rtc的映射關系,我們這里可以用Map來實現映射的保存
const peerConnectList=new Map();
const initPeer=()=> {
// ice,track,new Peer等其他代碼
......
peerConnectList.set(`${creatorUserId}_${recUserId}`, peerConnect);
}
上面實現了成員列表。接下來進入了對應的房間后需要輪詢獲取對應的成員列表
let userList=ref([]);
const intoRoom=()=> {
//其他代碼
......
setInterval(()=>{
axios.get('/userlist', { params: { room: room.value }}).then((res)=>{
userList.value=res.data
})
}, 1000)
}
在我們獲取到視頻流的時候,可以對在線列表里除了自己的人都創建一個RTCpeer,來進行一對一連接,從而達到多對多連接的效果。
// 過濾自己
const emitList=userList.value.filter((item)=> item[0] !==myUserId.value);
for (const item of emitList) {
// item[0]就是目標人的userId
const peer=await initPeer(myUserId.value, item[0]);
await createOffer(item[0], peer);
}
const createOffer=async (recUserId: string, peerConnect: RTCPeerConnection, stream: MediaStream=localStream)=> {
if (!localStream) return;
stream.getTracks().forEach((track)=> {
peerConnect.addTrack(track, stream)
})
const offer=await peerConnect.createOffer();
await peerConnect.setLocalDescription(offer);
socket.emit('offer', { creatorUserId: myUserId.value, sdp: offer, recUserId }, (res: any)=> {
console.log(res);
});
}
那么在socket服務中我們怎么只給對應的人進行事件廣播,不對其他人進行廣播,我們可以用找到這個人userId對應的socketId,進而只給這一個人廣播事件。
// 首先獲取IO對應的nameSpace
const IONameSpace=fastify.io.of('/');
// 發送Offer給對應的人
socket.on('offer', async (offer, callback)=> {
// 重新從reids獲取用戶列表
await getUserList();
// 找到目標的UserId的數據
const user=userList.get(offer.recUserId);
if (user) {
// 找到對應的socketId
const io=IONameSpace.sockets.get(user.sockId);
if (!io) return;
io.emit('offer', offer);
callback({
status: "ok"
})
}
})
其他人需要監聽socket的事件,每個人都需要處理對應自己的offer。
socket.on('offer', handleOffer);
const handleOffer=async (offer: { sdp: RTCSessionDescriptionInit, creatorUserId: string, recUserId: string })=> {
const peer=await initPeer(offer.creatorUserId, offer.recUserId);
await peer.setRemoteDescription(offer.sdp);
const answer=await peer.createAnswer();
await peer.setLocalDescription(answer);
socket.emit('answer', { recUserId: myUserId.value, sdp: answer, creatorUserId: offer.creatorUserId }, (res: any)=> {
console.log(res);
})
}
接下來的步驟其實就是和一對一是一樣的了,后面還需要發起offer的人處理對應peer的offer、以及ICE候選,還有流進行掛載播放。
socket.on('answer', handleAnswer)
// 應答方回復
const handleAnswer=async (data: { sdp: RTCSessionDescriptionInit, recUserId: string, creatorUserId: string })=> {
const peer=peerConnectList.get(`${data.creatorUserId}_${data.recUserId}`);
if (!peer) {
console.warn('handleAnswer peer 獲取失敗')
return;
}
await peer.setRemoteDescription(data.sdp)
}
......處理播放,處理ICE候選
到目前為止,就實現了一個基于mesh的WebRTC的多對多通信
C++音視頻開發WebRTC學習資料:點擊領取→音視頻開發(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
這個API是在MediaDevices里面的一個方法,是用來獲取屏幕共享的。
這個 MediaDevices 接口的 getDisplayMedia() 方法提示用戶去選擇和授權捕獲展示的內容或部分內容(如一個窗口)在一個 MediaStream 里. 然后,這個媒體流可以通過使用 MediaStream Recording API 被記錄或者作為WebRTC 會話的一部分被傳輸。
await navigator.mediaDevices.getDisplayMedia()
MediaRecorder
獲取到屏幕共享流后,需要使用 MediaRecorder這個api來對流進行錄制,接下來我們先獲取屏幕流,同時創建一個MeidaRecord類
let screenStream: MediaStream;
let mediaRecord: MediaRecorder;
let blobMedia: (Blob)[]=[];
const startLocalRecord=async ()=> {
blobMedia=[];
try {
screenStream=await navigator.mediaDevices.getDisplayMedia();
screenStream.getVideoTracks()[0].addEventListener('ended', ()=> {
console.log('用戶中斷了屏幕共享');
endLocalRecord()
})
mediaRecord=new MediaRecorder(screenStream, { mimeType: 'video/webm' });
mediaRecord.ondataavailable=(e)=> {
if (e.data && e.data.size > 0) {
blobMedia.push(e.data);
}
};
// 500是每隔500ms進行一個保存數據
mediaRecord.start(500)
} catch(e) {
console.log(`屏幕共享失敗->${e}`);
}
}
獲取到了之后可以使用 Blob 進行處理
const replayLocalRecord=async ()=> {
if (blobMedia.length) {
const scVideo=document.querySelector('#screenVideo') as HTMLVideoElement;
const blob=new Blob(blobMedia, { type:'video/webm' })
if(scVideo) {
scVideo.src=URL.createObjectURL(blob);
}
} else {
console.log('沒有錄制文件');
}
}
const downloadLocalRecord=async ()=> {
if (!blobMedia.length) {
console.log('沒有錄制文件');
return;
}
const blob=new Blob(blobMedia, { type: 'video/webm' });
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url;
a.download=`錄屏_${Date.now()}.webm`;
a.click();
}
這里有一個基于Vue2的完整例子
ps: 由于掘金的代碼片段的iframe沒有配置allow="display-capture *;microphone *; camera *"屬性,需要手動打開詳情查看效果
后續將會更新,WebRTC的自動化測試,視頻畫中畫,視頻截圖等功能
續分享wordpress建站教程。做wordpress外貿建站的用戶都喜歡在網站中插入youtube視頻,所以今天就分享如何給網站插入YouTube視頻。
wordpress中其實默認都是可以插入YouTube視頻,但是大家在使用時可能還是會遇到一些問題,有可能插入不成功,所以接下來悅然wordpress建站給大家分享幾種方法。
作者:悅然WordPress建站
首先,我們需要打開YouTube視頻,然后點【SHARE】,它會提示多種分享方式,插入網站中我們主要會用到短鏈接或Embed代碼這兩種方式。
在wordpress古騰堡編輯器中已經包含了YouTube區塊,所以我們可以直接添加一個YouTube區塊,然后填寫YouTube視頻分享的短鏈接(視頻頁面的完整鏈接也可以),點【嵌入】即可。
這個方法很簡單的,但是該方法可能會受到主題、插件、網絡、服務器等影響,導致嵌入不成功。
如果方法1不能用,那么也不用糾結,我們可以使用HTML代碼插入YouTube視頻。復制前面準備工作中提到的Embed代碼。
如果你使用的是古騰堡編輯器,可以添加一個HTML區塊,把Embed代碼粘貼進去。
然后我們點預覽就可以看到嵌入的YouTube視頻了。不過這里要注意的是你的網絡需要能連接到YouTube才行。
HMTL插入YouTube視頻的方法基本是通用的,所以在經典編輯器中,這個方法也適用。
如果你使用woocommerce插件制作網站,在添加產品時想加上YouTube區塊該怎么辦呢?woocommerce的產品編輯頁面默認是經典編輯器樣式的,所以不能直接添加HTML區塊。
此時我們可以在編輯器頁面點【文本】,這里可以直接插入HTML代碼,我們把前面復制的Embed代碼粘貼進去,如上圖所示。粘貼完成后我們再點【可視化】
如上圖,這樣YouTube視頻就嵌入成功了。
還有一些編輯器可能沒有【文本】那個選項,比如在一些多供應商系統的編輯器中,此時我們就沒辦法直接插入HTML代碼了。但是這些編輯器實際上是支持HMTL的,所以我們可以從其它地方把視頻復制進來。
比如我們可以在有HTML功能的編輯器中先添加好YouTube視頻,如上圖,悅然wordpress建站先在古騰堡區塊編輯器中添加好視頻(使用HTML或短鏈接方式插入都行),添加成功后,我們選中整個視頻,按CTRL+C,然后在不支持HTML的編輯器按CTRL+V粘貼,此時你會發現整個視頻都粘貼進來了。
可以實現嵌入YouTube視頻的wordpress插件有很多,比如Embed Plus for YouTube、EmbedPress等等。插件的使用就不講了,安裝就可以使用。
以上就是今天分享的wordpress建站教程,分享了幾種插入YouTube視頻的方法,希望對您有所幫助。
給視頻加邊框,可以使用Vue的樣式綁定功能來實現。
首先,在Vue組件的模板中,可以使用`<video>`標簽來嵌入視頻,然后通過設置樣式來給視頻加邊框。例如:
```html
<template>
<div class="video-container">
<video class="video" src="your-video-url"></video>
</div>
</template>
<style>
.video-container {
width: 400px; /* 設置視頻容器的寬度 */
height: 300px; /* 設置視頻容器的高度 */
border: 1px solid #ccc; /* 設置邊框樣式 */
}
.video {
width: 100%; /* 設置視頻的寬度為容器的寬度 */
height: 100%; /* 設置視頻的高度為容器的高度 */
}
</style>
```
上述代碼中,`video-container`類定義了視頻容器的樣式,包括寬度、高度和邊框樣式。`video`類定義了視頻的樣式,包括寬度和高度,設置為100%表示與容器的寬度和高度一致。
然后,在Vue組件的JavaScript代碼中,可以將視頻的URL綁定到`src`屬性上,例如:
```javascript
data() {
return {
videoUrl: 'your-video-url'
}
}
```
然后,在模板中使用`v-bind`指令將`videoUrl`綁定到`src`屬性上,例如:
```html
<video class="video" v-bind:src="videoUrl"></video>
```
這樣,當`videoUrl`的值發生變化時,視頻的URL也會跟著變化,從而播放不同的視頻。
完整的示例代碼如下:
```html
<template>
<div class="video-container">
<video class="video" v-bind:src="videoUrl"></video>
</div>
</template>
<script>
export default {
data() {
return {
videoUrl: 'your-video-url'
}
}
}
</script>
<style>
.video-container {
width: 400px;
height: 300px;
border: 1px solid #ccc;
}
.video {
width: 100%;
height: 100%;
}
</style>
```
注意,上述代碼中的`your-video-url`需要替換為實際的視頻URL。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。