方案類型 | 技術實現 | 是否免費 | 優點 | 缺點 | 適用場景 | 延遲范圍 | 開發復雜度 |
---|---|---|---|---|---|---|---|
?WebSocket+圖片幀? | 定時拍照+Base64傳輸 | ? 完全免費 | 無需服務器 純前端實現 | 高延遲高流量 幀率極低 | 個人demo測試 超低頻監控 | 500ms-2s | ?? |
?RTMP推流? | TRTC/即構SDK推流 | ? 付費方案 (部分有免費額度) | 專業直播方案 支持高并發 | 需流媒體服務器 SDK可能收費 | 中小型直播場景 | 1-3s | ???? |
?開源WebRTC? | 自建coturn+mediasoup | ? 開源免費 | 超低延遲 完全可控 | 需自建信令服務器 維護成本高 | 技術團隊內網項目 | 200-500ms | ????? |
?商業WebRTC? | 騰訊TRTC/聲網Agora | ? 付費方案 (免費試用) | 企業級服務 全球節點 | 按流量/時長計費 綁定廠商 | 商業視頻通話應用 | 200-800ms | ???? |
?HLS切片方案? | FFmpeg切片+nginx | ? 服務器可自建免費 | 兼容所有瀏覽器 支持CDN分發 | 延遲10秒以上 | 非實時錄播場景 | 10s+ | ??? |
?UDP自定義協議? | 開發原生插件 | ? 協議層免費 ? 人力成本高 | 完全自定義優化 | 需原生開發能力 過審風險 | 軍工/工業特殊場景 | 200-500ms | ?????? |
免費方案選擇建議:
-
?完全零成本?:
- WebSocket圖片幀(僅適合原型驗證)
- 開源WebRTC(需技術儲備)
-
?輕度付費?:
- 騰訊云RTMP(免費10GB/月流量)
- 阿里云直播(免費20GB/月流量)
-
?企業級推薦?:
- 聲網Agora(首月贈送1萬分鐘)
- 即構科技(首月免費)
下面我將介紹WebSocket+圖片幀的實現方法:
?
?WebSocket + 圖片幀傳輸方案詳解?
該方案是 ?Uniapp微信小程序 + PC端視頻實時預覽? 的一種 ?低成本、純前端實現? 的技術方案,適用于 ?低幀率、非嚴格實時? 的場景。
?🔹 方案原理?
-
?小程序端?:
- 使用?
<camera>
?組件獲取實時畫面。 - 通過?
uni.createCameraContext().takePhoto()
??定時拍照?(如300ms/次)。 - 將圖片轉為 ?Base64? 格式,通過 ?WebSocket? 發送到服務器。
- 使用?
-
?PC端?:
- 建立 WebSocket 連接,接收 Base64 圖片數據。
- 使用?
<img>
?或?<canvas>
??連續渲染圖片,模擬視頻流效果。
uniapp微信小程序端:
<template><view><camera :device-position="devicePosition" :flash="flash" @error="error" style="width:100%; height:300px;"></camera><button @click="startPushing">開始推流</button><button @click="stopPushing">停止推流</button><button @click="switchFlash">切換閃光燈</button><button @click="flipCamera">翻轉攝像頭</button><button style="font-size: 24rpx;">webscoket連接狀態:{{pushState}}</button></view>
</template><script>
export default {data() {return {pushState: "未連接",devicePosition: 'front',flash: 'off',timer: null,ws: null}},methods: {flipCamera() {this.devicePosition = this.devicePosition === 'back' ? 'front' : 'back';},switchFlash() {this.flash = this.flash === 'off' ? 'torch' : 'off';},startPushing() {// 如果已連接,則不再重復連接if (this.pushState === '連接成功') return;const randomToken = new Date().getTime();const url = 'ws://192.168.1.34:7097/liveWebSocket?linkInfo=a-' + randomToken;this.ws = uni.connectSocket({url,success: () => {console.log('正在嘗試連接WebSocket', url);}});this.ws.onOpen(() => {uni.showToast({ title: '連接成功' });this.pushState = '連接成功';this.startCapture();});this.ws.onError((err) => {uni.showToast({ title: '連接異常', icon: 'none' });this.pushState = '連接異常';this.stopPushing();});this.ws.onClose(() => {this.pushState = '已關閉';this.stopPushing();});},stopPushing() {if (this.timer) {clearInterval(this.timer);this.timer = null;}if (this.ws) {this.ws.close();this.ws = null;this.pushState = "未連接";}},startCapture() {const context = uni.createCameraContext(this);// 調整為300ms間隔,減輕設備壓力this.timer = setInterval(() => {context.takePhoto({quality: 'low',success: (res) => {this.processAndSendImage(res.tempImagePath);},fail: (err) => {console.error('拍照失敗:', err);}});}, 300);},processAndSendImage(tempImagePath) {uni.getFileSystemManager().readFile({filePath: tempImagePath,encoding: 'base64',success: (res) => {const base64Image = `data:image/jpeg;base64,${res.data}`;if (this.ws) {this.ws.send({data: base64Image,success: () => {console.log('圖片發送成功');this.cleanTempFile(tempImagePath);},fail: (err) => {console.warn('圖片發送失敗:', err);}});}},fail: (err) => {console.warn('讀取圖片失敗:', err);}});},cleanTempFile(filePath) {setTimeout(() => {uni.getFileSystemManager().removeSavedFile({filePath,success: () => {console.log('臨時文件已刪除');},fail: (err) => {console.warn('刪除臨時文件失敗:', err);}});}, 2000);},error(e) {console.error('攝像頭錯誤:', e);}},onUnload() {this.stopPushing();}
}
</script>
pc端預覽:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>管理員監控頁面</title><script src="./vue2.js"></script>
</head><body><div id="app"><button @click="toSend">開始請求</button><div v-if="videos && videos.length> 0" style="display: flex;"><div v-for="item in videos" :key="item.sessionId" style="margin: 10px;display: flex;flex-flow: column;":id="item.sessionId">狀態:{{item.status}}<img :src="item.videoSrc" style="width: 200px; height: 200px; border: 1px solid red;" alt=""></div></div><div style="background-color: green;margin: 20px 0;display: flex;width: 50%;word-wrap: break-word">接口數據:<div v-html="datas"></div></div><div style="background-color: red;width: 50%">視頻列表:<template v-if="videos && videos.length> 0"><p v-for="item2 in videos">{{item2}}</p></template></div></div><script>new Vue({el: '#app',data: {datas: "",videos: [// {// sessionId: '1',// status: '未連接',// videoSrc: '' //圖片幀// }]},mounted() {},methods: {// 開始請求toSend() {//斷開所有webscoket連接if (this.videos && this.videos.length > 0) {this.videos.forEach(item => {if (item.ws) {item.ws.close();}});}this.datas = "";this.videos = [];// 請求直播人員列表fetch('http://192.168.1.34:7097/liveWebStock/getAcceptList').then(response => response.json()).then(data => {if (data.code == 200) {// console.log(6666, data.data); this.datas = data.data;// 初始化每個視頻流對象并建立 WebSocket this.videos = data.data.map(item => ({...item,status: '未連接',videoSrc: '',ws: null}));// 建立 WebSocket 連接this.videos.forEach(item => {this.initWebSocket(item.sessionId);});}}).catch(error => {console.error('請求直播人員列表失敗:', error);});},initWebSocket(sessionId) {if (!sessionId) return;const wsUrl = `ws://192.168.1.34:7097/liveWebSocket?linkInfo=b-${sessionId}`;const index = this.videos.findIndex(v => v.sessionId === sessionId);if (index === -1) return;const ws = new WebSocket(wsUrl);ws.onopen = () => {this.$set(this.videos, index, {...this.videos[index],status: '已連接到服務器',ws});};// 處理接收到的數據ws.onmessage = (event) => {console.log("接收到base64圖片", event);// 假設是 base64 數據const base64Data = event.data;const url = base64Data;this.$set(this.videos, index, {...this.videos[index],videoSrc: url});};ws.onerror = (error) => {this.$set(this.videos, index, {...this.videos[index],status: `WebSocket 錯誤: ${error.message}`});console.error(`WebSocket 錯誤 (${sessionId}):`, error);};ws.onclose = () => {this.$set(this.videos, index, {...this.videos[index],status: 'WebSocket 連接已關閉'});};}}});</script>
</body></html>