Chúng ta sẽ xây dựng 3 module:
- fRec: ghi video từ rtsp qua ứng dụng ffmpeg
- pMotionRecorder: lấy event từ Onvif camera qua phương thức pull
- sMotionRecorder: lấy event từ Onvif camera qua dữ liệu web
Để đơn giản, các module pMotionRecorder và sMotionRecorder được tối ưu cho dòng camera IMOU, tuy không phải model nào cũng tương thích.
- Script ghi hình imou.js như sau:
Input: username, password và hostname/ip của camera (version 10220215)
const motionRecorder = require('./lib/pMotionRecorder.js')
const info = {
//user, password của camera
camUser: 'admin',
camPass: '<password>',
//ip/hostname của camera
hostname: '<cam_ip>',
//--------- OPTIONS ---------//
//kiểu file video, mặc định mkv
// outType: 'mp4',
//thư mục chứa video, mặc định /camera
outFolder: '/mnt/imou',
//độ dài tối đa của clip (giây), mặc định 15
// clipInterval: 10,
//chụp ảnh, mặc định false
snapshot: true,
//in ra màn hình debug/normal(default)/quiet
logLevel: 'debug'
}
new motionRecorder (info)
Thay pMotionRecorder bằng sMotionRecorder nếu dùng module web server
- Gọi chạy từ crontab, @reboot xem chú thích
0 0 * * * /usr/local/bin/node /path/to/imou.js > ~/imou.log
Script fRec
//Record video from Onvif camera
//fRec.js - version 20220215"
//LNT <lnt@ly-le.info>
const childProcess = require('child_process')
const fs = require('fs')
class ffRecorder {
constructor (options) {
this.opts = options
if (!fs.existsSync(this.opts.outFolder))
fs.mkdirSync(this.opts.outFolder, { recursive: true });
}
mRecord() {
let clip = [
'-hide_banner',
'-rtsp_transport', 'tcp',
'-i', this.opts.url,
'-c', 'copy',
'-f', 'segment',
'-segment_time', this.opts.clipInterval,
'-strftime', 1,
this.opts.outFolder + '/' + '%Y%m%d-%H%M%S.' + this.opts.outType
]
let pict = [
'-r', 1/this.opts.clipInterval,
'-strftime', 1,
this.opts.outFolder + '/' + '%Y%m%d-%H%M%S.jpg'
]
let args = (this.opts.snapshot) ? clip.concat(pict) : clip
return childProcess.spawn('ffmpeg', args, {detached: false, stdio: 'ignore'})
}
startRecording() {
if (!this.wStream) {
this.wStream = this.mRecord()
if (this.opts.logLevel != 'quiet')
console.log(new Date().toLocaleTimeString(), 'Start recording...')
}
}
stopRecording() {
if (this.wStream) {
this.wStream.kill()
this.wStream = null
if (this.opts.logLevel != 'quiet')
console.log((new Date()).toLocaleTimeString(), 'Stop recording!')
}
}
}
module.exports = ffRecorder
Script pMotionRecorder
//Record video from Onvif camera
//pMotionRecorder.js - version 20220215"
//LNT <lnt@ly-le.info>
const fRec = require('./fRec.js')
, Cam = require('onvif').Cam
, flow = require('nimble')
class pMotionRecorder {
constructor(options){
let opts = options
if (!opts.logLevel) opts.logLevel = 'normal'
if (!opts.outType) opts.outType = 'mkv'
if (!opts.outFolder) opts.outFolder = '/camera'
if (!opts.snapshot) opts.snapshot = false
if (isNaN(opts.clipInterval)) opts.clipInterval = 15
this.rec = new fRec({
outFolder: opts.outFolder,
outType: opts.outType,
snapshot: opts.snapshot,
clipInterval: opts.clipInterval,
logLevel: opts.logLevel
})
let oCam = new Cam (
{
hostname: opts.hostname,
username: opts.camUser,
password: opts.camPass
},
(err) => {
if (err) { console.log(err); return }
const logLevel = opts.logLevel
if (logLevel != 'quiet') console.log('Connected to ONVIF Device')
flow.series([
(callback) => {
oCam.getDeviceInformation((err, info, xml) => {
if (logLevel == 'debug') console.log(info)
if (logLevel != 'silent') console.log('[', (new Date()).toLocaleDateString(), ']')
callback();
})
},
(callback) => {
oCam.getStreamUri((err, data, xml) => {
if (err) { console.log(err); return }
this.rec.opts.url = (data.uri).replace(oCam.hostname, oCam.username + ':' + oCam.password + '@' + oCam.hostname)
callback();
})
},
(callback) => {
oCam.on('event', (camMsg, xml) => {
let eTopic = camMsg.topic._.split(':').pop(),
eOperation = camMsg.message.message.$.PropertyOperation,
eValue = camMsg.message.message.data.simpleItem.$.Value
this.processEvent(eTopic, eOperation, eValue, logLevel)
callback()
})
}
])
}
)
}
processEvent (event, operation, value, logLevel) {
if (logLevel == 'debug')
console.log((new Date()).toLocaleTimeString(), event, '>>' , operation, '>>', value)
if (event == 'VideoSource/MotionAlarm' && operation == 'Changed') {
if (value) this.rec.startRecording(); else this.rec.stopRecording()
}
}
}
module.exports = pMotionRecorder
Script sMotionRecorder
Script này dùng module onvif mới nhất từ github, bản cũ hơn sẽ báo lỗi phương thức subscribe
//Record video from Onvif camera
//sMotionRecorder.js - version 20220215"
//LNT <lnt@ly-le.info>
const fRec = require('./fRec.js')
, Cam = require('onvif').Cam
, flow = require('nimble')
, http = require('http')
, { execSync } = require('child_process')
, ip = require('ip')
class sMotionRecorder {
constructor (options) {
let opts = options
if (!opts.logLevel) opts.logLevel = 'normal'
if (!opts.outType) opts.outType = 'mkv'
if (!opts.outFolder) opts.outFolder = '/camera'
if (!opts.snapshot) opts.snapshot = false
if (isNaN(opts.clipInterval)) opts.clipInterval = 15
const serverPort = 8899
const logLevel = opts.logLevel
this.rec = new fRec ({
outFolder: opts.outFolder,
outType: opts.outType,
snapshot:opts.snapshot,
clipInterval: opts.clipInterval,
logLevel: logLevel
})
this.processEvent = (event, operation, value) => {
if (logLevel == 'debug')
console.log((new Date()).toLocaleTimeString(), event, '>>' , operation, '>>', value)
if (event == 'VideoSource/MotionAlarm' && operation === 'Changed'){
if (value) this.rec.startRecording()
else this.rec.stopRecording()
}
}
let oCam = new Cam ({
hostname: opts.hostname,
port: 80,
username: opts.camUser,
password: opts.camPass,
timeout: 10000,
preserveAddress: true
},
(err) => {
if (err) { console.log(err); return; }
if (logLevel != 'quiet') console.log('Connected to ONVIF Device')
flow.series([
(callback) => {
oCam.getDeviceInformation((err, info, xml) => {
if (logLevel == 'debug') console.log(info)
if (logLevel != 'quiet') console.log('[', (new Date()).toLocaleDateString(), ']')
})
callback()
},
(callback) => {
let camId = Math.random().toString(36).substring(2, 10)
let receiveUrl = 'http://' + ip.address() + ':' + serverPort + '/events/' + camId
oCam.subscribe (
{url: receiveUrl},
(err, subscription, xml) => {
if (err) { console.log(err); return }
}
)
if (logLevel == 'debug') console.log('Subscribed to ' + receiveUrl)
callback()
},
(callback) => {
oCam.getStreamUri((err, data, xml) => {
if (err) { console.log(err); return }
this.rec.opts.url = (data.uri).replace(oCam.hostname, oCam.username + ':' + oCam.password + '@' + oCam.hostname)
})
callback();
}
])
})
http.createServer((req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', () => {
if (req.method == "POST") {
let eTopic = body.match(/tns1:([^<]+)/)[1],
eOperation = body.match(/PropertyOperation="([^"]+)/)[1],
eValue = body.match(/Name="State" Value="([^"]+)/)[1]
this.processEvent(eTopic, eOperation, eValue === 'true')
}
res.writeHead(200, { "Content-Type": "text/plain" });
})
}).listen(serverPort)
}
}
module.exports = sMotionRecorder
Chú thích
- Một ứng dụng nodejs thường dùng khá nhiều module, qua lời gọi
require('/path/to/module.js')
Ứng dụng sẽ báo lỗi nếu đường dẫn đến module không đúng.
- Camera IMOU không có giao diện web, khi đó dùng password là mã bảo mật in dưới đáy camera
- Để không phải sử dụng nhiều module ngoài, sMotionRecorder dùng Regular Expression để phân tích xml …
- Ứng dụng không dùng ffmpeg để lấy snapshot mà lấy qua camera nên giảm tải cho RPi rất nhiều.
- logLevel: ‘normal’ //mặc định
- Việc khởi tạo event service bình thường nhanh, nhưng có lúc chậm, khi đó script không nhận được event từ camera, tuy nhiên rồi sẽ hoạt động sau vài phút nếu camera mới reset.
- Để ứng dụng node.js chạy khi reboot, phải dùng pm2 hay forever… Các cách thông thường không tác dụng
# gọi chạy ứng dụng và ghi vào danh sách quản lý
pm2 start app_name
pm2 start --name imou /path/to/imou.js
# tự chạy các ứng dụng trong danh sách khi reboot
pm2 startup
Cài đặt
Phải cài đặt npm và nodejs trước, xem tại đây. Ngoài ra cần có ffmpeg.
- Tải về và giải nén gói onvif-motion-recorder
- Chuyển vào thư mục onvif-motion-recorder và chạy lệnh npm i
# onvif-motion-recorder, version 20220217
wget http://pi.ly-le.info/wp-content/uploads/2022/02/onvif-motion-recorder-v20220217.zip
unzip onvif-motion-recorder-v20220217.zip
cd onvif-motion-recorder
npm i
Sửa các option trong file imou.js, chủ yếu là camPass và hostname, rồi chạy bằng lệnh
node imou.js
- Với camera hiệu khác, sao chép imou.js thành tên file khác và sửa lại nội dung