mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00

* Use MQTT for start-stream debug messages * Fix ANSI colors * Refactor event URL management * Fix subscription detection * Improve event URL expiry handling by parsing Amazon S3 expire time * Convert to ESM/replace colors with chalk * Force colors for chalk * Migrate to ESM * Fix stop of keepalive stream * Add transcoded event selections * Update event URL on raw/trancoded toggle * Switch to per-camera livecall threads * Customized WebRTC functions Mostly copied from ring-client-api with port to pure Javascript, removal of unneeded features and additional debugging modified for use as worker thread with ring-mqtt. Allows easier testing with updated Werift versions. * Add nightlight enable/disable * Include nightlight state as attribute * Only pro versions have nightlight * Tweak battery level reporting for dual battery cameras * Release 5.1.0
133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
// This code is largely copied from ring-client-api, but converted from Typescript
|
|
// to straight Javascript and some code not required for ring-mqtt removed.
|
|
// Much thanks to @dgreif for the original code which is the basis for this work.
|
|
|
|
import { FfmpegProcess, reservePorts, RtpSplitter, } from '@homebridge/camera-utils'
|
|
import { firstValueFrom, ReplaySubject, Subject } from 'rxjs'
|
|
import pathToFfmpeg from 'ffmpeg-for-homebridge'
|
|
import { concatMap, take } from 'rxjs/operators'
|
|
import { Subscribed } from './subscribed.js'
|
|
|
|
function getCleanSdp(sdp) {
|
|
return sdp
|
|
.split('\nm=')
|
|
.slice(1)
|
|
.map((section) => 'm=' + section)
|
|
.join('\n')
|
|
}
|
|
|
|
export class StreamingSession extends Subscribed {
|
|
constructor(camera, connection) {
|
|
super()
|
|
this.camera = camera
|
|
this.connection = connection
|
|
this.onCallEnded = new ReplaySubject(1)
|
|
this.onUsingOpus = new ReplaySubject(1)
|
|
this.onVideoRtp = new Subject()
|
|
this.onAudioRtp = new Subject()
|
|
this.audioSplitter = new RtpSplitter()
|
|
this.videoSplitter = new RtpSplitter()
|
|
this.hasEnded = false
|
|
this.bindToConnection(connection)
|
|
}
|
|
|
|
bindToConnection(connection) {
|
|
this.addSubscriptions(
|
|
connection.onAudioRtp.subscribe(this.onAudioRtp),
|
|
connection.onVideoRtp.subscribe(this.onVideoRtp),
|
|
connection.onCallAnswered.subscribe((sdp) => {
|
|
this.onUsingOpus.next(sdp.toLocaleLowerCase().includes(' opus/'))
|
|
}),
|
|
connection.onCallEnded.subscribe(() => this.callEnded()))
|
|
}
|
|
|
|
async reservePort(bufferPorts = 0) {
|
|
const ports = await reservePorts({ count: bufferPorts + 1 })
|
|
return ports[0]
|
|
}
|
|
|
|
get isUsingOpus() {
|
|
return firstValueFrom(this.onUsingOpus)
|
|
}
|
|
|
|
async startTranscoding(ffmpegOptions) {
|
|
if (this.hasEnded) {
|
|
return
|
|
}
|
|
const videoPort = await this.reservePort(1)
|
|
const audioPort = await this.reservePort(1)
|
|
|
|
const ringSdp = await Promise.race([
|
|
firstValueFrom(this.connection.onCallAnswered),
|
|
firstValueFrom(this.onCallEnded),
|
|
])
|
|
|
|
if (!ringSdp) {
|
|
// Call ended before answered'
|
|
return
|
|
}
|
|
const usingOpus = await this.isUsingOpus
|
|
|
|
const ffmpegInputArguments = [
|
|
'-hide_banner',
|
|
'-protocol_whitelist',
|
|
'pipe,udp,rtp,file,crypto',
|
|
// Ring will answer with either opus or pcmu
|
|
...(usingOpus ? ['-acodec', 'libopus'] : []),
|
|
'-f',
|
|
'sdp',
|
|
...(ffmpegOptions.input || []),
|
|
'-i',
|
|
'pipe:'
|
|
]
|
|
|
|
const inputSdp = getCleanSdp(ringSdp)
|
|
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
.replace(/m=video \d+/, `m=video ${videoPort}`)
|
|
|
|
const ff = new FfmpegProcess({
|
|
ffmpegArgs: ffmpegInputArguments.concat(
|
|
...(ffmpegOptions.audio || ['-acodec', 'aac']),
|
|
...(ffmpegOptions.video || ['-vcodec', 'copy']),
|
|
...(ffmpegOptions.output || [])),
|
|
ffmpegPath: pathToFfmpeg,
|
|
exitCallback: () => this.callEnded()
|
|
})
|
|
|
|
this.addSubscriptions(this.onAudioRtp.pipe(concatMap((rtp) => {
|
|
return this.audioSplitter.send(rtp.serialize(), { port: audioPort })
|
|
})).subscribe())
|
|
|
|
this.addSubscriptions(this.onVideoRtp.pipe(concatMap((rtp) => {
|
|
return this.videoSplitter.send(rtp.serialize(), { port: videoPort })
|
|
})).subscribe())
|
|
|
|
this.onCallEnded.pipe(take(1)).subscribe(() => ff.stop())
|
|
|
|
ff.writeStdin(inputSdp)
|
|
|
|
// Request a key frame now that ffmpeg is ready to receive
|
|
this.requestKeyFrame()
|
|
}
|
|
|
|
callEnded() {
|
|
if (this.hasEnded) {
|
|
return
|
|
}
|
|
this.hasEnded = true
|
|
this.unsubscribe()
|
|
this.onCallEnded.next()
|
|
this.connection.stop()
|
|
this.audioSplitter.close()
|
|
this.videoSplitter.close()
|
|
}
|
|
|
|
stop() {
|
|
this.callEnded()
|
|
}
|
|
|
|
requestKeyFrame() {
|
|
this.connection.requestKeyFrame()
|
|
}
|
|
}
|