mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-27 05:05:54 +08:00
246 lines
8.5 KiB
JavaScript
246 lines
8.5 KiB
JavaScript
import { spawn } from 'child_process'
|
|
import utils from '../../lib/utils.js'
|
|
import pathToFfmpeg from 'ffmpeg-for-homebridge'
|
|
|
|
export class SnapshotStream {
|
|
constructor(device) {
|
|
this.mqttCamera = device
|
|
this.status = 'inactive'
|
|
this.interval = null
|
|
this.livesnaps = {
|
|
active: false,
|
|
session: false,
|
|
timeout: 0
|
|
}
|
|
this.keepalive = {
|
|
active: false,
|
|
session: false
|
|
}
|
|
this.rtsp = {
|
|
active: false,
|
|
session: false
|
|
}
|
|
this.snapshot = {
|
|
active: false,
|
|
session: false
|
|
}
|
|
}
|
|
|
|
async start(rtspPublishUrl) {
|
|
if (!this.mqttCamera.data.snapshot.image) {
|
|
this.mqttCamera.debug('Snapshot stream failed to start - No available snapshot')
|
|
this.status = 'failed'
|
|
this.mqttCamera.publishStreamState()
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.startSnapshotStream(rtspPublishUrl)
|
|
} catch {
|
|
this.mqttCamera.debug('Snapshot stream failed to start - Failed to spawn ffmpeg')
|
|
this.status = 'failed'
|
|
this.mqttCamera.publishStreamState()
|
|
}
|
|
}
|
|
|
|
async startSnapshotStream(rtspPublishUrl) {
|
|
this.rtsp.session = spawn(pathToFfmpeg, [
|
|
'-f', 'mpegts',
|
|
'-probesize', '32k',
|
|
'-analyzeduration', '0',
|
|
'-fflags', '+discardcorrupt+genpts',
|
|
'-r', '5',
|
|
'-ss', '0.2',
|
|
'-i', 'pipe:',
|
|
'-c:v', 'copy',
|
|
'-avioflags', 'direct',
|
|
'-flags', '+global_header',
|
|
'-f', 'rtsp',
|
|
'-rtsp_transport', 'tcp',
|
|
rtspPublishUrl
|
|
])
|
|
|
|
// Handle process exit
|
|
this.rtsp.session.on('close', () => {
|
|
this.mqttCamera.debug('Snapshot stream transcoding session has ended')
|
|
this.stop()
|
|
})
|
|
|
|
// Return a promise that resolves when the process is ready
|
|
return new Promise((resolve, reject) => {
|
|
this.rtsp.session.on('spawn', async () => {
|
|
this.snapshot.session = spawn(pathToFfmpeg, [
|
|
'-hide_banner',
|
|
'-f', 'image2pipe',
|
|
'-probesize', '32k',
|
|
'-analyzeduration', '0',
|
|
'-r', '20',
|
|
'-i', 'pipe:',
|
|
'-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2',
|
|
'-sws_flags', 'lanczos',
|
|
'-c:v', 'libx264',
|
|
'-b:v', '3M',
|
|
'-r', '5',
|
|
'-g', '1',
|
|
'-preset', 'ultrafast',
|
|
'-tune', 'zerolatency',
|
|
'-avioflags', 'direct',
|
|
'-f', 'mpegts',
|
|
'pipe:1'
|
|
])
|
|
|
|
this.snapshot.session.on('spawn', async () => {
|
|
this.startSnapshotInterval()
|
|
this.mqttCamera.debug('Snapshot stream transcoding session has started')
|
|
this.status = 'active'
|
|
this.mqttCamera.publishStreamState()
|
|
resolve()
|
|
})
|
|
|
|
this.snapshot.session.stdout.once('data', () => {
|
|
this.snapshot.session.stdout.pipe(this.rtsp.session.stdin)
|
|
})
|
|
})
|
|
|
|
this.rtsp.session.on('error', reject)
|
|
})
|
|
}
|
|
|
|
async startLivesnaps(duration) {
|
|
if (this.livesnaps.active) { return }
|
|
this.livesnaps.active = true
|
|
|
|
this.mqttCamera.debug('Starting a live snapshot stream for camera')
|
|
|
|
// We need a live stream, but don't want to rely on global keepalive so we start one here
|
|
this.keepalive.session = spawn(pathToFfmpeg, [
|
|
'-i', `rtsp://${this.mqttCamera.rtspCredentials}localhost:8554/${this.mqttCamera.deviceId}_live`,
|
|
'-map', '0:a:0',
|
|
'-c:a', 'copy',
|
|
'-f', 'null',
|
|
'/dev/null'
|
|
])
|
|
|
|
this.keepalive.session.on('spawn', async () => {
|
|
this.keepalive.active = true
|
|
|
|
const liveStream = this.mqttCamera.streams.live
|
|
const liveStreamStartTimeout = Date.now() + 5000
|
|
while (!liveStream.altVideoData && Date.now() < liveStreamStartTimeout) {
|
|
await utils.msleep(50)
|
|
}
|
|
|
|
if (liveStream.altVideoData) {
|
|
this.livesnaps.session = spawn(pathToFfmpeg, [
|
|
'-hide_banner',
|
|
'-protocol_whitelist', 'pipe,udp,rtp,fd,file,crypto',
|
|
'-flags', 'low_delay',
|
|
'-probesize', '32K',
|
|
'-analyzeduration', '0',
|
|
'-fflags', '+genpts',
|
|
'-r', '20',
|
|
'-f', 'sdp',
|
|
'-i', 'pipe:',
|
|
'-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2',
|
|
'-sws_flags', 'lanczos',
|
|
'-c:v', 'libx264',
|
|
'-b:v', '3M',
|
|
'-preset', 'ultrafast',
|
|
'-tune', 'zerolatency',
|
|
'-r', '5',
|
|
'-g', '1',
|
|
'-avioflags', 'direct',
|
|
'-f', 'mpegts',
|
|
'pipe:1'
|
|
])
|
|
|
|
this.livesnaps.session.on('spawn', async () => {
|
|
liveStream.unbindAltVideoPorts()
|
|
this.livesnaps.session.stdin.write(liveStream.altVideoData.sdp)
|
|
this.livesnaps.session.stdin.end()
|
|
})
|
|
|
|
this.livesnaps.session.stdout.once('data', () => {
|
|
this.snapshot.session.stdout.unpipe(this.rtsp.session.stdin)
|
|
this.livesnaps.session.stdout.pipe(this.rtsp.session.stdin)
|
|
})
|
|
|
|
this.livesnaps.session.on('close', async () => {
|
|
liveStream.bindAltVideoPorts()
|
|
this.mqttCamera.debug('The live snapshot stream has stopped')
|
|
this.mqttCamera.updateSnapshot('interval')
|
|
this.livesnaps.session.stdout.unpipe(this.rtsp.session.stdin)
|
|
this.snapshot.session.stdout.pipe(this.rtsp.session.stdin)
|
|
Object.assign(this.livesnaps, { active: false, session: false, image: false })
|
|
})
|
|
|
|
// The livesnap stream will stop after the specified duration
|
|
const livesnapsTimeout = Date.now() + 5000 + (duration * 1000)
|
|
while (this.livesnaps.active && (Date.now() < livesnapsTimeout)) {
|
|
await utils.sleep(1)
|
|
}
|
|
} else {
|
|
this.mqttCamera.debug('The live snapshot stream failed starting the live stream')
|
|
}
|
|
|
|
if (this.livesnaps.active && this.livesnaps.session) {
|
|
this.livesnaps.session?.kill()
|
|
}
|
|
|
|
if (this.keepalive.active && this.keepalive.session) {
|
|
this.keepalive.session?.kill()
|
|
}
|
|
})
|
|
|
|
this.keepalive.session.on('close', async () => {
|
|
this.keepalive.active = false
|
|
this.keepalive.session = false
|
|
})
|
|
}
|
|
|
|
startSnapshotInterval() {
|
|
this.interval = setInterval(async () => {
|
|
if (this.status === 'active') {
|
|
try {
|
|
this.snapshot.session?.stdin?.write(this.mqttCamera.data.snapshot.image)
|
|
} catch {
|
|
this.mqttCamera.debug('Writing image to snapshot stream failed')
|
|
this.stop()
|
|
}
|
|
} else {
|
|
this.stop()
|
|
}
|
|
}, 50)
|
|
}
|
|
|
|
async stop() {
|
|
clearInterval(this.interval)
|
|
this.interval = null
|
|
this.status = 'inactive'
|
|
this.mqttCamera.publishStreamState()
|
|
|
|
if (!this.rtsp.session) return
|
|
|
|
const rtspSession = this.rtsp.session
|
|
this.rtsp.session = false
|
|
|
|
try {
|
|
rtspSession.stdin.end()
|
|
await Promise.race([
|
|
new Promise((resolve) => {
|
|
rtspSession.stdin.once('finish', () => {
|
|
rtspSession.kill()
|
|
resolve()
|
|
})
|
|
}),
|
|
new Promise((_, reject) => {
|
|
setTimeout(() => {
|
|
reject()
|
|
}, 2000)
|
|
})
|
|
])
|
|
} catch {
|
|
rtspSession.kill('SIGKILL')
|
|
}
|
|
}
|
|
} |