方案类型 | 技术实现 | 是否免费 | 优点 | 缺点 | 适用场景 | 延迟范围 | 开发复杂度 |
---|---|---|---|---|---|---|---|
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微信小程序端:
html
<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端预览:
html
<!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>
