UniApp + Node.js 全栈实战:从零构建家庭安防监控系统
1. 项目概述与技术选型
想象一下,当你外出旅行时,只需打开手机就能实时查看家中的情况;或者当快递员按门铃时,远程确认对方身份——这些场景都可以通过自建安防监控系统实现。本教程将带你用UniApp和Node.js构建一个完整的远程监控解决方案,涵盖从摄像头调用到云端存储的全流程。
为什么选择这套技术栈?UniApp的跨平台特性让我们一套代码适配iOS、Android和Web,而Node.js轻量高效的特点非常适合处理实时视频流。相比市面上封闭的监控系统,自建方案具有以下优势:
- 数据自主可控:所有视频流经自己的服务器,避免隐私泄露风险
- 成本低廉:利用现有手机/平板作为监控设备,无需购买专业硬件
- 高度定制:可根据需求灵活调整功能,如添加AI人脸识别
技术架构分为三个核心层:
- 客户端:UniApp调用设备摄像头,采集并编码视频流
- 传输层:WebSocket实现低延迟传输,HTTP用于文件上传
- 服务端:Node.js处理流媒体转发与存储,Express提供REST API
2. 客户端开发:UniApp摄像头集成
2.1 基础环境搭建
首先创建UniApp项目并安装必要依赖:
# 通过HBuilderX创建项目 vue create -p dcloudio/uni-preset-vue monitor-app # 安装摄像头插件 npm install @dcloudio/uni-camera --save在pages.json中配置摄像头页面权限:
{ "pages": [ { "path": "pages/camera/index", "style": { "navigationBarTitleText": "监控视图", "app-plus": { "permissions": ["camera"] } } } ] }2.2 视频采集核心代码
创建pages/camera/index.vue,实现多摄像头切换与流媒体控制:
<template> <view class="container"> <camera ref="camera" device-position="back" flash="off" @error="onCameraError" style="width: 100%; height: 70vh;" /> <view class="control-panel"> <button @tap="switchCamera">切换镜头</button> <button :type="isRecording ? 'warn' : 'primary'" @tap="toggleRecording"> {{ isRecording ? '停止监控' : '开始监控' }} </button> </view> </view> </template> <script> export default { data() { return { isRecording: false, cameraPosition: 'back', recorder: null } }, methods: { async toggleRecording() { if (this.isRecording) { this.stopRecording() } else { await this.startRecording() } }, async startRecording() { try { const camera = this.$refs.camera this.recorder = await camera.startRecord({ quality: 'high', success: (res) => { console.log('录制开始', res) } }) this.isRecording = true } catch (err) { uni.showToast({ title: '启动失败: ' + err.message, icon: 'none' }) } }, async stopRecording() { const camera = this.$refs.camera const { tempFilePath } = await camera.stopRecord() this.uploadVideo(tempFilePath) this.isRecording = false }, switchCamera() { this.cameraPosition = this.cameraPosition === 'back' ? 'front' : 'back' this.$refs.camera.stopRecord() this.$nextTick(() => { this.$refs.camera.startRecord() }) }, async uploadVideo(filePath) { uni.uploadFile({ url: 'https://your-server.com/api/upload', filePath, name: 'video', success: (res) => { console.log('上传成功', res.data) } }) } } } </script>提示:真机调试时务必在manifest.json中配置摄像头权限声明,iOS还需要在Xcode中启用相机权限
3. 服务端开发:Node.js视频处理
3.1 Express服务器基础架构
创建服务端项目并安装核心依赖:
mkdir monitor-server && cd monitor-server npm init -y npm install express multer socket.io ffmpeg-static cors构建支持视频上传和实时转发的服务:
// server.js const express = require('express') const multer = require('multer') const path = require('path') const { exec } = require('child_process') const app = express() const server = require('http').createServer(app) const io = require('socket.io')(server, { cors: { origin: "*" } }) // 文件存储配置 const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/') }, filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)) } }) const upload = multer({ storage }) // REST API端点 app.post('/api/upload', upload.single('video'), (req, res) => { const file = req.file if (!file) return res.status(400).send('No file uploaded') // 转码为HLS格式 const outputPath = `streams/${file.filename.split('.')[0]}` const cmd = `ffmpeg -i ${file.path} \ -profile:v baseline -level 3.0 \ -start_number 0 -hls_time 10 -hls_list_size 0 \ -f hls ${outputPath}.m3u8` exec(cmd, (error) => { if (error) return res.status(500).send('转码失败') res.json({ url: `/stream/${file.filename.split('.')[0]}.m3u8` }) }) }) // WebSocket实时通信 io.on('connection', (socket) => { console.log('客户端连接:', socket.id) socket.on('stream', (data) => { // 广播给其他客户端 socket.broadcast.emit('stream', data) }) socket.on('disconnect', () => { console.log('客户端断开:', socket.id) }) }) server.listen(3000, () => { console.log('服务运行在 http://localhost:3000') })3.2 视频处理优化方案
针对不同场景,我们提供三种视频处理方案:
| 方案类型 | 延迟 | 适用场景 | 实现复杂度 | 存储需求 |
|---|---|---|---|---|
| 实时转发 | 低(<1s) | 即时监控 | 中 | 无 |
| HLS存储 | 中(10s+) | 历史回放 | 高 | 大 |
| 帧快照 | 低 | 移动侦测 | 低 | 小 |
实现帧提取与移动侦测的示例代码:
// motion-detection.js const Jimp = require('jimp') const fs = require('fs') async function detectMotion(currentFrame, prevFrame) { const threshold = 0.1 // 变化阈值 const current = await Jimp.read(currentFrame) const prev = await Jimp.read(prevFrame) let diffCount = 0 current.scan(0, 0, current.bitmap.width, current.bitmap.height, (x, y, idx) => { const currPixel = Jimp.intToRGBA(current.getPixelColor(x, y)) const prevPixel = Jimp.intToRGBA(prev.getPixelColor(x, y)) const diff = Math.abs(currPixel.r - prevPixel.r) / 255 + Math.abs(currPixel.g - prevPixel.g) / 255 + Math.abs(currPixel.b - prevPixel.b) / 255 if (diff > threshold) diffCount++ }) const changeRatio = diffCount / (current.bitmap.width * current.bitmap.height) return changeRatio > 0.05 // 5%以上区域变化视为移动 }4. 前后端联调与部署
4.1 联调关键问题解决
常见联调问题及解决方案:
跨域问题:
- 服务端启用CORS中间件
- 开发环境配置代理
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } }视频格式兼容性:
- 客户端统一使用MP4格式录制
camera.startRecord({ format: 'mp4', // ...其他参数 })大文件上传中断:
- 实现分片上传
- 添加重试机制
// 客户端分片上传示例 const chunkSize = 1024 * 1024 // 1MB const uploadChunk = async (file, start, end) => { const chunk = file.slice(start, end) const formData = new FormData() formData.append('chunk', chunk) formData.append('start', start) formData.append('total', file.size) return uni.uploadFile({ url: '/api/upload-chunk', filePath: chunk, name: 'chunk', formData: { start, total: file.size } }) }
4.2 生产环境部署指南
服务器部署方案对比:
| 平台 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 云主机 | 完全控制 | 维护成本高 | 高频访问 |
| Serverless | 自动扩展 | 冷启动延迟 | 间歇使用 |
| 边缘计算 | 低延迟 | 价格较高 | 多地分布 |
推荐使用PM2进行Node.js进程管理:
# 全局安装PM2 npm install pm2 -g # 启动服务 pm2 start server.js --name "monitor-server" # 设置开机启动 pm2 startup pm2 save性能优化配置:
# Nginx配置示例 server { listen 80; server_name yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; } location /stream { alias /path/to/streams; add_header Cache-Control no-cache; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } } }5. 功能扩展与进阶优化
5.1 实时通知系统
集成WebPush实现移动端通知:
// 服务端推送逻辑 const webpush = require('web-push') const vapidKeys = { publicKey: 'YOUR_PUBLIC_KEY', privateKey: 'YOUR_PRIVATE_KEY' } webpush.setVapidDetails( 'mailto:contact@example.com', vapidKeys.publicKey, vapidKeys.privateKey ) // 当检测到移动时 function onMotionDetected(subscription) { webpush.sendNotification(subscription, JSON.stringify({ title: '移动警报', body: '检测到可疑移动', icon: '/assets/alert.png' })).catch(err => console.error('推送失败:', err)) }5.2 视频分析增强
使用TensorFlow.js实现基础人脸检测:
<!-- 在UniApp中通过renderjs使用TF.js --> <script module="tf" lang="renderjs"> import * as tf from '@tensorflow/tfjs' import * as facemesh from '@tensorflow-models/facemesh' export default { async mounted() { this.model = await facemesh.load() this.detectFrame() }, methods: { async detectFrame() { const video = document.getElementById('camera-video') const predictions = await this.model.estimateFaces(video) if (predictions.length > 0) { this.$ownerInstance.callMethod('onFaceDetected') } requestAnimationFrame(this.detectFrame) } } } </script>5.3 多设备管理方案
实现设备绑定与切换的数据库设计:
// models/Device.js const mongoose = require('mongoose') const DeviceSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, name: String, type: { type: String, enum: ['camera', 'sensor'] }, lastActive: Date, streamUrl: String, settings: { resolution: { type: String, default: '720p' }, motionSensitivity: { type: Number, default: 5 } } }) module.exports = mongoose.model('Device', DeviceSchema)设备状态管理界面建议布局:
<template> <view class="device-grid"> <view v-for="device in devices" :key="device._id" class="device-card" @click="selectDevice(device)" > <image :src="device.thumbnail || '/static/default-camera.png'" /> <text>{{ device.name }}</text> <view class="status" :class="{ online: device.isOnline }"></view> </view> </view> </template> <style> .device-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; padding: 15px; } .device-card { position: relative; border-radius: 8px; overflow: hidden; } .status { position: absolute; top: 5px; right: 5px; width: 10px; height: 10px; border-radius: 50%; background: #ccc; } .status.online { background: #4CAF50; } </style>