我的车被划了,看我实现简易监控拿捏他!node+DroidCam+ffmpeg
某天我骑着我的小电驴下班回到我那出租屋,习惯性的看了一眼我那停在门口的二手奥拓,突然发现有点不对劲,走近一看引擎盖上多了一大条划痕,顿时恶向胆边生,是谁!!!为此我决定用现有条件做一套简易的监控系统来应对日后的情况,于是有了这篇文章。
一 准备工作
由于是要做监控,硬件是必不可少的,所以首先想到的就是闲置的手机了,找了一台安卓8.1的古董出来,就决定是你了。因为之前在公司使用过
DroidCam这款软件用来进行webRTC的开发,所以这次就顺理成章的装了这款软件,连上家里的wifi后打开就相当于有了一台简易的视频服务器。那么硬件搞定了,接下来的就是软件了。梳理下来的话只有以下几点了
- 拉取DroidCam上的视频流
- 将拉取到的内容做存储
由于本人是个前端,因此这里就顺理成章的使用node来作为软件实现的第一方案了。
二 获取视频流,啊?怎么是这玩意儿
怎么获取它传过来的视频流呢?看了一下上打开的软件界面,发现给了两个地址,ip端口 和 ip端口/video,不出意料的这两个里面肯定是有能用的东西,挨个打开后发现不带video的地址是相当于一个控制台,带video的是视频的接口地址。那就好办了,我满怀激动的以为一切都很容易的时候,打开控制台一看,咦,这是啥玩意儿?它的所谓的视频是现在img标签里的,这在之前可是没见过哦,再看一眼接口地址,咦,这是一个长链接?点开详情看了一眼,好吧,又学到新东西了。它的Content-Type是multipart/x-mixed-replace;boundary='xxxx'
,这是啥呀,搜索了一下资料后如下。
MJPEG(Motion Joint Photographic Experts Gr0up)是一种视频压缩格式,其中每一帧图像都分别使用JPEG编码,不使用帧间编码,压缩率通常在20:1-50:1范围内。它的原理是把视频镜头拍成的视频分解成一张张分离的jpg数据发送到客户端。当客户端不断显示图片,即可形成相应的图像。
大致意思懂了,就是这就是一张张的图像呗。后面又看了一下服务端是如何生成这玩意儿的,这里就不细说了。
知道了是啥东西,那就要想怎么把它搞出来了
三 使用ffmpeg 获取每一帧
ffmpeg相信大家都不陌生,使用前需要先在本机上安装,安装方法的话这里就不赘述了。
安装后在系统环境变量高级设置中,增加path变量的值为ffmpeg在电脑上的路径。后续就可以使用了。
随便新建一个js文件
const fs = require('fs')
const path = require('path')
//截取的视频帧的存储路径和命名方式
const outputFilePattern = path.join(__dirname + '/newFrame', 'd.jpg');
//视频服务器地址
const mjpegUrl = 'http://192.168.2.101:4747/video?1920x1080';
//通过child_process的spawn调用外部文件,执行ffmpeg,并传入参数
//下方代码执行后在连接到服务后不手动停止的情况下期间会不断的在指定目录下生成获取到的图片
const ffmpeg = require('child_process').spawn('ffmpeg', [
'-i',
mjpegUrl,
'-vf',
'fps=24',//设置帧率
'-q:v',
'1', // 调整此值以更改输出质量(较低的值表示更好的质量)
outputFilePattern // %d 将被替换为帧编号
], { windowsHide: true });//调用时不显示cmd窗口
//错误监听
ffmpeg.on('error', function (err) {
throw err;
});
//关闭监听
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
});
//数据
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
//执行合并图片操作
//....
});
上述代码运行后如果能正常连接上服务的话你会在指定目录下看到不断生成的图片。
四 将图片生成为视频
光有图片是不够的,我最终的预期是生成视频以供查看,所以添加以下的代码将图片合并为视频
//上面生成图片后存放的位置
let filePath = path.join(__dirname + '/newFrame', 'd.jpg');
let comd = [
'-framerate',
'24',
'-i',
filePath,
'-c:v',
'libx264',
'-pix_fmt',
'yuv420p',
`${__dirname}/outVideo/${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${new Date().getDate().toString().padStart(2, '0')}_${new Date().getHours().toString().padStart(2, '0')}${new Date().getMinutes().toString().padStart(2, '0')}${new Date().getSeconds().toString().padStart(2, '0')}.mp4`
]
const ffmpeg = require('child_process').spawn('ffmpeg', comd,{ windowsHide: true });
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
});
我这里定的是每2000张图片组合成视频,因此将第三步
中的
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
});
改成
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
//打印结果 =>>frame= 1474 fps= 14 q=1.0 size=N/A time=00:01:01.41 bitrate=N/A speed=0.57x
let arr = data.toString().split('fps')
try {
//获取frame数量用来计数
frameCount = arr[0].split('=')[1].trim()
console.log(frameCount)
//为什么这里用大于而不是等于呢,因为获取frame可能不是总会计数到我们想要的值,踩过坑,注意
if (frameCount > 2000) {
console.log('数量满足')
//关闭本次获取流
ffmpeg.kill('SIGKILL');
//这里执行合并文件的操作
//...
}
} catch (e) { }
});
到这里如果你一切顺利的话就能在指定的文件夹里看到合并完成后的MP4视频了。
五 合并完成后删除上次获取的图片
将第四步
的
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
});
改为
ffmpeg.on('close', async function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
try {
await fsE.emptyDir('your folderPath');
console.log(`已清空文件夹`);
//重新执行第二步
//...
} catch (err) {
console.error(`无法清空文件夹: ${err}`);
}
});
这里的fsE
是 const fsE = require('fs-extra');
,需要安装并且导入
到这里为止,整个基本的流程就完成了
六 总结
整个程序到目前为止已经能基本满足我的需求,但是还存在不足,比如频繁的往硬盘上读写文件、容错处理等等,后续我的想法是把图片保存到内存中,在满足条件后再写入硬盘,减少文件的I/O操作,加入对人体的识别,接入之前写过的邮件通知,有人靠近自动记录时间点并发送到邮箱。当然了,我这个肯定比不了市面上的那些成熟产品,就是自己写着好玩的,请各位大佬轻喷!有错误和意见欢迎指正!
来源:juejin.cn/post/7419887017164767268