Merge pull request #888 from tsightler/dev

Release v5.7.0
This commit is contained in:
tsightler
2024-08-12 14:01:13 -04:00
committed by GitHub
29 changed files with 1421 additions and 2311 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization
* text=auto
*.sh text

View File

@@ -1,4 +1,4 @@
FROM alpine:3.18
FROM alpine:3.20
ENV LANG="C.UTF-8" \
PS1="$(whoami)@$(hostname):$(pwd)$ " \
@@ -11,7 +11,7 @@ ENV LANG="C.UTF-8" \
COPY . /app/ring-mqtt
RUN S6_VERSION="v3.2.0.0" && \
BASHIO_VERSION="v0.16.2" && \
GO2RTC_VERSION="v1.9.2" && \
GO2RTC_VERSION="v1.9.4" && \
APK_ARCH="$(apk --print-arch)" && \
apk add --no-cache tar xz git libcrypto3 libssl3 musl-utils musl bash curl jq tzdata nodejs npm mosquitto-clients && \
curl -L -s "https://github.com/just-containers/s6-overlay/releases/download/${S6_VERSION}/s6-overlay-noarch.tar.xz" | tar -Jxpf - -C / && \
@@ -42,7 +42,9 @@ RUN S6_VERSION="v3.2.0.0" && \
exit 1;; \
esac && \
curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}" && \
cp "/app/ring-mqtt/bin/go2rtc_linux_${GO2RTC_ARCH}" /usr/local/bin/go2rtc && \
chmod +x /usr/local/bin/go2rtc && \
rm -rf /app/ring-mqtt/bin && \
curl -J -L -o /tmp/bashio.tar.gz "https://github.com/hassio-addons/bashio/archive/${BASHIO_VERSION}.tar.gz" && \
mkdir /tmp/bashio && \
tar zxvf /tmp/bashio.tar.gz --strip 1 -C /tmp/bashio && \

BIN
bin/go2rtc_linux_amd64 Normal file

Binary file not shown.

BIN
bin/go2rtc_linux_arm Normal file

Binary file not shown.

BIN
bin/go2rtc_linux_arm64 Normal file

Binary file not shown.

View File

@@ -66,7 +66,7 @@ export default class BeamOutdoorPlug extends RingSocketDevice {
case 'on':
case 'off': {
const duration = 32767
const data = Boolean(command === 'on') ? { lightMode: 'on', duration } : { lightMode: 'default' }
const data = command === 'on' ? { lightMode: 'on', duration } : { lightMode: 'default' }
this[outletId].sendCommand('light-mode.set', data)
break;
}

View File

@@ -126,7 +126,7 @@ export default class Beam extends RingSocketDevice {
if (this.isLightGroup && this.groupId) {
this.device.location.setLightGroup(this.groupId, Boolean(command === 'on'), duration)
} else {
const data = Boolean(command === 'on') ? { lightMode: 'on', duration } : { lightMode: 'default' }
const data = command === 'on' ? { lightMode: 'on', duration } : { lightMode: 'default' }
this.device.sendCommand('light-mode.set', data)
}
break;

View File

@@ -5,13 +5,20 @@ import { StreamingSession } from '../lib/streaming/streaming-session.js'
const deviceName = workerData.deviceName
const doorbotId = workerData.doorbotId
let liveStream = false
let streamStopping = false
parentPort.on("message", async(data) => {
const streamData = data.streamData
switch (data.command) {
case 'start':
if (!liveStream) {
if (streamStopping) {
parentPort.postMessage({type: 'log_error', data: "Live stream could not be started because it is in stopping state"})
parentPort.postMessage({type: 'state', data: 'failed'})
} else if (!liveStream) {
startLiveStream(streamData)
} else {
parentPort.postMessage({type: 'log_error', data: "Live stream could not be started because there is already an active stream"})
parentPort.postMessage({type: 'state', data: 'active'})
}
break;
case 'stop':
@@ -87,11 +94,19 @@ async function startLiveStream(streamData) {
}
async function stopLiveStream() {
liveStream.stop()
await new Promise(res => setTimeout(res, 2000))
if (liveStream) {
parentPort.postMessage({type: 'log_info', data: 'Live stream failed to stop on request, deleting anyway...'})
parentPort.postMessage({type: 'state', data: 'inactive'})
liveStream = false
if (!streamStopping) {
streamStopping = true
let stopTimeout = 10
liveStream.stop()
do {
await new Promise(res => setTimeout(res, 200))
if (liveStream) {
parentPort.postMessage({type: 'log_info', data: 'Live stream failed to stop on request, deleting anyway...'})
parentPort.postMessage({type: 'state', data: 'inactive'})
liveStream = false
}
stopTimeout--
} while (liveStream && stopTimeout)
streamStopping = false
}
}

View File

@@ -485,19 +485,17 @@ export default class Camera extends RingPolledDevice {
async processNotification(pushData) {
let dingKind
// Is it a motion or doorbell ding? (for others we do nothing)
switch (pushData.action) {
case 'com.ring.push.HANDLE_NEW_DING':
switch (pushData.android_config?.category) {
case 'com.ring.pn.live-event.ding':
dingKind = 'ding'
break
case 'com.ring.push.HANDLE_NEW_motion':
case 'com.ring.pn.live-event.motion':
dingKind = 'motion'
break
default:
this.debug(`Received push notification of unknown type ${pushData.action}`)
return
}
const ding = pushData.ding
ding.created_at = Math.floor(Date.now()/1000)
this.debug(`Received ${dingKind} push notification, expires in ${this.data[dingKind].duration} seconds`)
// Is this a new Ding or refresh of active ding?
@@ -505,19 +503,19 @@ export default class Camera extends RingPolledDevice {
this.data[dingKind].active_ding = true
// Update last_ding and expire time
this.data[dingKind].last_ding = ding.created_at
this.data[dingKind].last_ding_time = utils.getISOTime(ding.created_at*1000)
this.data[dingKind].last_ding = Math.floor(pushData.data?.event?.eventito?.timestamp/1000)
this.data[dingKind].last_ding_time = pushData.data?.event?.ding?.created_at
this.data[dingKind].last_ding_expires = this.data[dingKind].last_ding+this.data[dingKind].duration
// If motion ding and snapshots on motion are enabled, publish a new snapshot
if (dingKind === 'motion') {
this.data[dingKind].is_person = Boolean(ding.detection_type === 'human')
this.data[dingKind].is_person = Boolean(pushData.data?.event?.ding?.detection_type === 'human')
if (this.data.snapshot.motion) {
this.refreshSnapshot('motion', ding.image_uuid)
this.refreshSnapshot('motion', pushData?.img?.snapshot_uuid)
}
} else if (this.data.snapshot.ding) {
// If doorbell press and snapshots on ding are enabled, publish a new snapshot
this.refreshSnapshot('ding', ding.image_uuid)
this.refreshSnapshot('ding', pushData?.img?.snapshot_uuid)
}
// Publish MQTT active sensor state

View File

@@ -101,7 +101,7 @@ export default class Chime extends RingPolledDevice {
this.data.volume = volumeState
}
const snoozeState = Boolean(this.device.data.do_not_disturb.seconds_left) ? 'ON' : 'OFF'
const snoozeState = this.device.data.do_not_disturb.seconds_left ? 'ON' : 'OFF'
if (snoozeState !== this.data.snooze || isPublish) {
this.mqttPublish(this.entity.snooze.state_topic, snoozeState)
this.data.snooze = snoozeState

View File

@@ -112,7 +112,7 @@ export default class Lock extends RingPolledDevice {
}
this.mqttPublish(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.publishAttributeEntities(attributes)
} catch(error) {
} catch {
this.debug('Could not publish attributes due to no health data')
}
}

View File

@@ -1,5 +1,5 @@
import RingSocketDevice from './base-socket-device.js'
import { allAlarmStates, RingDeviceType } from 'ring-client-api'
import { allAlarmStates } from 'ring-client-api'
import utils from '../lib/utils.js'
import state from '../lib/state.js'

View File

@@ -164,6 +164,7 @@ export default class Thermostat extends RingSocketDevice {
switch(mode) {
case 'off':
this.mqttPublish(this.entity.thermostat.action_topic, mode)
// Fall through
case 'cool':
case 'heat':
case 'auto':
@@ -262,11 +263,12 @@ export default class Thermostat extends RingSocketDevice {
const presetMode = value.toLowerCase()
switch(presetMode) {
case 'auxillary':
case 'none':
case 'none': {
const mode = presetMode === 'auxillary' ? 'aux' : 'heat'
this.device.setInfo({ device: { v1: { mode } } })
this.mqttPublish(this.entity.thermostat.preset_mode_state_topic, presetMode.replace(/^./, str => str.toUpperCase()))
break;
}
default:
this.debug('Received invalid preset mode command')
}

View File

@@ -1,3 +1,22 @@
## v5.7.0
This release migrates to the new FCM HTTP v1 API for push notifications as the legacy FCM/GCM APIs have been deprecated for some time and shutdown of those legacy APIs started in late July 2024. While the transition to this new API should be transparent for most users, the under the hood changes are signfiicant including an entirely new push notification format. While the goal is to make this transition as seemless as possible, it is impossible to guarantee 100% success. If you experience issues with motion/ding notification from cameras, doorbells or intercoms after upgrading to this version, please follow the standard push notification troubleshooting steps as follows:
1) Open the ring-mqtt web UI and note the device name
2) Stop the ring-mqtt addon/container
3) Navigate to the Ring Control Center using the Ring App or Ring Web console
4) Locate the device with the matching device name from step 1 in Authorized Client Devices and delete it
5) Restart the addon and use the addon web UI to re-authenticate to the Ring API
**Minor Enhancements**
- A significant amount of work has gone into improving the reliability of streaming, especially the live stream. In prior versions there were various failure scenarios that could lead to states where future streaming requests would not succeed and the only option was to restart the entire addon/container. Hours of testing have gone into this version and many such issues have been addressed.
**Dependency Updates**
- ring-client-api v13.0.1
- go2rtc v1.9.4 (custom build to fix a hang on exit issue)
- Alpine Linux 3.20.2
- NodeJS v20.15.1
- s6-overlay v3.2.0.0
## v5.6.7
This release is intended to address an ongoing instability with websocket connections by using a newer API endpoint for requesting tickets.

17
eslint.config.js Normal file
View File

@@ -0,0 +1,17 @@
import globals from "globals";
import pluginJs from "@eslint/js";
export default [
{
languageOptions: {
globals: globals.node
}
},
pluginJs.configs.recommended,
{
rules: {
"no-prototype-builtins": "off"
}
}
];

View File

@@ -12,11 +12,11 @@ async function getRefreshToken(systemId) {
let generatedToken
const email = await requestInput('Email: ')
const password = await requestInput('Password: ')
const restClient = new RingRestClient({
email,
password,
const restClient = new RingRestClient({
email,
password,
controlCenterDisplayName: `ring-mqtt-${systemId.slice(-5)}`,
systemId: systemId
systemId: systemId
})
try {
await restClient.getCurrentAuth()
@@ -28,12 +28,12 @@ async function getRefreshToken(systemId) {
}
}
while(!generatedToken) {
while(!generatedToken) {
const code = await requestInput('2FA Code: ')
try {
generatedToken = await restClient.getAuth(code)
return generatedToken.refresh_token
} catch(err) {
} catch {
throw('Failed to validate the entered 2FA code. (error: invalid_code)')
}
}
@@ -43,11 +43,11 @@ const main = async() => {
let refresh_token
let stateData = {}
// If running in Docker set state file path as appropriate
const stateFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
const stateFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
? '/data/ring-state.json'
: dirname(fileURLToPath(new URL(import.meta.url)))+'/ring-state.json'
const configFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
const configFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
? '/data/config.json'
: dirname(fileURLToPath(new URL(import.meta.url)))+'/config.json'
@@ -109,7 +109,7 @@ const main = async() => {
console.log('New config file written to '+configFile)
} catch (err) {
console.log('Failed to create new config file at '+stateFile)
conslog.log(err)
console.log(err)
}
}
}

View File

@@ -31,10 +31,11 @@ export default new class Config {
await this.loadConfigFile()
this.doMqttDiscovery()
break;
default:
default: {
const configPath = dirname(fileURLToPath(new URL('.', import.meta.url)))+'/'
this.file = (process.env.RINGMQTT_CONFIG) ? configPath+process.env.RINGMQTT_CONFIG : configPath+'config.json'
await this.loadConfigFile()
}
}
// If there's still no configured settings, force some defaults.

View File

@@ -53,9 +53,9 @@ export default new class Go2RTC {
config.streams = {}
for (const camera of cameras) {
config.streams[`${camera.deviceId}_live`] =
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} live ${camera.deviceTopic} {output}`
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} live ${camera.deviceTopic} {output}#killsignal=15`
config.streams[`${camera.deviceId}_event`] =
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} event ${camera.deviceTopic} {output}`
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} event ${camera.deviceTopic} {output}#killsignal=15`
}
try {
await writeFileAtomic(configFile, yaml.dump(config, { lineWidth: -1 }))
@@ -91,13 +91,13 @@ export default new class Go2RTC {
const stdoutLine = readline.createInterface({ input: this.go2rtcProcess.stdout })
stdoutLine.on('line', (line) => {
// Replace time in go2rtc log messages with tag
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3}([^\s]+) /, chalk.green('[go2rtc] ')))
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3} /, chalk.green('[go2rtc] ')))
})
const stderrLine = readline.createInterface({ input: this.go2rtcProcess.stderr })
stderrLine.on('line', (line) => {
// Replace time in go2rtc log messages with tag
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3}([^\s]+) /, '[go2rtc] '))
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3} /, chalk.green('[go2rtc] ')))
})
}

View File

@@ -1,5 +1,5 @@
import exithandler from './exithandler.js'
import mqtt from './mqtt.js'
export * from './exithandler.js'
export * from './mqtt.js'
import state from './state.js'
import ring from './ring.js'
import utils from './utils.js'

View File

@@ -241,6 +241,7 @@ export default new class RingMqtt {
case 'not-supported':
// Save unsupported device type for log output later
unsupportedDevices.push(device.deviceType)
// fall through
case 'ignore':
ringDevice=false
break

View File

@@ -87,9 +87,7 @@ export class WeriftPeerConnection extends Subscribed {
this.addSubscriptions(merge(this.onRequestKeyFrame, interval(4000)).subscribe(() => {
videoTransceiver.receiver
.sendRtcpPLI(track.ssrc)
.catch((e) => {
// debug(e)
})
.catch()
}))
this.requestKeyFrame()
})
@@ -131,9 +129,7 @@ export class WeriftPeerConnection extends Subscribed {
}
close() {
this.pc.close().catch((e) => {
//debug
})
this.pc.close().catch()
this.unsubscribe()
}
}

View File

@@ -150,7 +150,7 @@ export class WebrtcConnection extends Subscribed {
return
case 'pong':
return
case 'notification':
case 'notification': {
const { text } = message.body
if (text === 'camera_connected') {
this.onCameraConnected.next()
@@ -162,6 +162,7 @@ export class WebrtcConnection extends Subscribed {
return
}
break
}
case 'close':
this.callEnded()
return
@@ -219,7 +220,7 @@ export class WebrtcConnection extends Subscribed {
})
this.ws.close()
}
catch (_) {
catch {
// ignore any errors since we are stopping the call
}
this.hasEnded = true

View File

@@ -100,7 +100,7 @@ export default new class TokenApp {
const code = req.body.code
try {
generatedToken = await restClient.getAuth(code)
} catch(err) {
} catch {
generatedToken = false
const errormsg = 'The 2FA code was not accepted, please verify the code and try again.'
debug(errormsg)

3311
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ring-mqtt",
"version": "5.6.7",
"version": "5.7.0",
"type": "module",
"description": "Ring Devices via MQTT",
"main": "ring-mqtt.js",
@@ -9,19 +9,24 @@
"body-parser": "^1.20.2",
"chalk": "^5.3.0",
"date-fns": "^3.6.0",
"debug": "^4.3.5",
"debug": "^4.3.6",
"express": "^4.19.2",
"is-online": "^10.0.0",
"js-yaml": "^4.1.0",
"minimist": "^1.2.8",
"mqtt": "^5.7.0",
"ring-client-api": "12.1.1",
"mqtt": "^5.9.1",
"ring-client-api": "^13.0.1",
"rxjs": "^7.8.1",
"werift": "^0.19.3",
"werift": "^0.19.4",
"write-file-atomic": "^5.0.1"
},
"devDependencies": {
"eslint": "^7.32.0"
"@eslint/js": "^9.8.0",
"eslint": "^9.8.0",
"globals": "^15.9.0"
},
"overrides": {
"@eneris/push-receiver": "4.1.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node
import Main from './lib/main.js'
export * from './lib/main.js'

100
scripts/monitor-stream.sh Normal file
View File

@@ -0,0 +1,100 @@
#!/bin/bash
# Activate video stream on Ring cameras via ring-mqtt
# Intended only for use as on-demand script for rtsp-simple-server
# Requires mosquitto MQTT clients package to be installed
# Uses ring-mqtt internal IPC broker for communications with main process
# Provides status updates and termintates stream on script exit
# Required command line arguments
device_id=${1} # Camera device Id
type=${2} # Stream type ("live" or "event")
base_topic=${3} # Command topic for Camera entity
rtsp_pub_url=${4} # URL for publishing RTSP stream
client_id="${device_id}_${type}" # Id used to connect to the MQTT broker, camera Id + event type
activated="false"
reason="none"
[[ ${type} = "live" ]] && base_topic="${base_topic}/stream" || base_topic="${base_topic}/event_stream"
json_attribute_topic="${base_topic}/attributes"
command_topic="${base_topic}/command"
debug_topic="${base_topic}/debug"
# Set some colors for debug output
red='\e[0;31m'
yellow='\e[0;33m'
green='\e[0;32m'
blue='\e[0;34m'
reset='\e[0m'
cleanup() {
local ffpids=$(pgrep -f "ffmpeg.*${rtsp_pub_url}" | grep -v ^$$\$)
[ -n "$ffpids" ] && kill -9 $ffpids
local pids=$(pgrep -f "mosquitto_sub.*${client_id}_sub" | grep -v ^$$\$)
[ -n "$pids" ] && kill $pids
exit 0
}
# go2rtc does not pass stdout through from child processes so send debug logs
# via main process using MQTT messages
logger() {
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "${1}"
}
# Trap signals so that the MQTT command to stop the stream can be published on exit
trap cleanup INT TERM
# This loop starts mosquitto_sub with a subscription on the camera stream topic that sends all received
# messages via file descriptor to the read process. On initial startup the script publishes the message
# 'ON-DEMAND' to the stream command topic which lets ring-mqtt know that an RTSP client has requested
# the stream. Stream state is determined via the the detailed stream state messages received via the
# json_attributes_topic:
#
# "inactive" = There is no active video stream and none currently requested
# "activating" = A video stream has been requested and is initializing but has not yet started
# "active" = The stream was requested successfully and an active stream is currently in progress
# "failed" = A live stream was requested but failed to start
mosquitto_sub -q 1 -i "${client_id}_sub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" |
while read message; do
# Otherwise it should be a JSON message from the stream state attribute topic so extract the detailed stream state
stream_state=`echo ${message} | jq -r '.status'`
case ${stream_state,,} in
activating)
if [ ${activated} = "false" ]; then
logger "State indicates ${type} stream is activating"
fi
;;
active)
if [ ${activated} = "false" ]; then
logger "State indicates ${type} stream is active"
activated="true"
fi
;;
deactivate)
if [ ${activated} = "true" ]; then
reason='deactivate'
fi
;;
inactive)
if [ ${reason} = "deactivate" ] ; then
logmsg="State indicates ${type} stream is inactive"
else
logmsg=$(echo -en "${yellow}State indicates ${type} stream has gone unexpectedly inactive${reset}")
fi
logger "${logmsg}"
reason='inactive'
cleanup
;;
failed)
logmsg=$(echo -en "${red}ERROR - State indicates ${type} stream failed to activate${reset}")
logger "${logmsg}"
reason='failed'
cleanup
;;
*)
logmsg=$(echo -en "${red}ERROR - Received unknown ${type} stream state on topic ${blue}${json_attribute_topic}${reset}")
logger "${logmsg}"
;;
esac
done
cleanup

View File

@@ -1,9 +1,17 @@
#!/bin/bash
# Activate video stream on Ring cameras via ring-mqtt
# Intended only for use as on-demand script for rtsp-simple-server
# Activate Ring camera video stream via ring-mqtt
#
# This script is intended for use only with ring-mqtt
# and go2rtc.
#
# Requires mosquitto MQTT clients package to be installed
# Uses ring-mqtt internal IPC broker for communications with main process
# Provides status updates and termintates stream on script exit
# Uses ring-mqtt internal IPC broker for communication with
# ring-mqtt process
#
# Spawns stream control in background due to issues with
# process exit hanging go2rtc. Script then just monitors
# for control script to exit or, if script is killed,
# sends commands to control script prior to exiting
# Required command line arguments
device_id=${1} # Camera device Id
@@ -11,10 +19,20 @@ type=${2} # Stream type ("live" or "event")
base_topic=${3} # Command topic for Camera entity
rtsp_pub_url=${4} # URL for publishing RTSP stream
client_id="${device_id}_${type}" # Id used to connect to the MQTT broker, camera Id + event type
activated="false"
# If previous run hasn't exited yet, just perform a short wait and exit with error
if test -f /tmp/ring-mqtt-${device_id}.lock; then
sleep .1
exit 1
else
touch /tmp/ring-mqtt-${device_id}.lock
fi
script_dir=$(dirname "$0")
${script_dir}/monitor-stream.sh ${1} ${2} ${3} ${4} &
# Build the MQTT topics
[[ ${type} = "live" ]] && base_topic="${base_topic}/stream" || base_topic="${base_topic}/event_stream"
json_attribute_topic="${base_topic}/attributes"
command_topic="${base_topic}/command"
debug_topic="${base_topic}/debug"
@@ -26,76 +44,39 @@ green='\e[0;32m'
blue='\e[0;34m'
reset='\e[0m'
stop() {
# Interrupted by signal so send command to stop stream
# Send message to monitor script that stream was requested to stop so that it doesn't log a warning
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" -m {\"status\":\"deactivate\"}
# Send ring-mqtt the command to stop the stream
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "Deactivating ${type} stream due to signal from RTSP server (no more active clients or publisher ended stream)"
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "OFF"
# Send kill signal to monitor script and wait for it to exit
local pids=$(jobs -pr)
[ -n "$pids" ] && kill $pids
wait
cleanup
}
# If control script is still runnning send kill signal and exit
cleanup() {
if [ -z ${reason} ]; then
# If no reason defined, that means we were interrupted by a signal, send the command to stop the live stream
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "Deactivating ${type} stream due to signal from RTSP server (no more active clients or publisher ended stream)"
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "OFF"
fi
# Kill the spawed mosquitto_sub process or it will stay listening forever
kill $(pgrep -f "mosquitto_sub.*${client_id}_sub" | grep -v ^$$\$)
rm -f /tmp/ring-mqtt-${device_id}.lock
# For some reason sleeping for 100ms seems to keep go2rtc from hanging
exit 0
}
# go2rtc does not pass stdout through from child processes so send debug logs
# via main process using MQTT messages
# Send debug logs via main process using MQTT messages
logger() {
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "${1}"
}
# Trap signals so that the MQTT command to stop the stream can be published on exit
trap cleanup INT TERM QUIT
trap stop INT TERM EXIT
# This loop starts mosquitto_sub with a subscription on the camera stream topic that sends all received
# messages via file descriptor to the read process. On initial startup the script publishes the message
# 'ON-DEMAND' to the stream command topic which lets ring-mqtt know that an RTSP client has requested
# the stream. Stream state is determined via the the detailed stream state messages received via the
# json_attributes_topic:
#
# "inactive" = There is no active video stream and none currently requested
# "activating" = A video stream has been requested and is initializing but has not yet started
# "active" = The stream was requested successfully and an active stream is currently in progress
# "failed" = A live stream was requested but failed to start
while read -u 10 message
do
# If start message received, publish the command to start stream
if [ ${message} = "START" ]; then
logger "Sending command to activate ${type} stream ON-DEMAND"
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "ON-DEMAND ${rtsp_pub_url}"
else
# Otherwise it should be a JSON message from the stream state attribute topic so extract the detailed stream state
stream_state=`echo ${message} | jq -r '.status'`
case ${stream_state,,} in
activating)
if [ ${activated} = "false" ]; then
logger "State indicates ${type} stream is activating"
fi
;;
active)
if [ ${activated} = "false" ]; then
logger "State indicates ${type} stream is active"
activated="true"
fi
;;
inactive)
logmsg=$(echo -en "${yellow}State indicates ${type} stream has gone inactive${reset}")
logger "${logmsg}"
reason='inactive'
cleanup
;;
failed)
logmsg=$(echo -en "${red}ERROR - State indicates ${type} stream failed to activate${reset}")
logger "${logmsg}"
reason='failed'
cleanup
;;
*)
logmsg=$(echo -en "${red}ERROR - Received unknown ${type} stream state on topic ${blue}${json_attribute_topic}${reset}")
logger "${logmsg}"
;;
esac
fi
done 10< <(mosquitto_sub -q 1 -i "${client_id}_sub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" & echo "START")
logger "Sending command to activate ${type} stream ON-DEMAND"
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "ON-DEMAND ${rtsp_pub_url}" &
cleanup
exit 0
wait
cleanup

View File

@@ -22,22 +22,40 @@ else
echo "The ring-mqtt-${BRANCH} branch has been updated."
APK_ARCH="$(apk --print-arch)"
GO2RTC_VERSION="v1.9.2"
GO2RTC_VERSION="v1.9.4"
case "${APK_ARCH}" in
x86_64)
GO2RTC_ARCH="amd64";;
GO2RTC_ARCH="amd64"
;;
aarch64)
GO2RTC_ARCH="arm64";;
GO2RTC_ARCH="arm64"
;;
armv7|armhf)
GO2RTC_ARCH="arm";;
GO2RTC_ARCH="arm"
;;
*)
echo >&2 "ERROR: Unsupported architecture '$APK_ARCH'"
exit 1;;
exit 1
;;
esac
rm -f /usr/local/bin/go2rtc
curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}"
#curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}"
cp "/app/ring-mqtt-${BRANCH}/bin/go2rtc_linux_${GO2RTC_ARCH}" /usr/local/bin/go2rtc
chmod +x /usr/local/bin/go2rtc
case "${APK_ARCH}" in
x86_64)
apk del npm nodejs
apk add libstdc++
cd /opt
wget https://unofficial-builds.nodejs.org/download/release/v20.16.0/node-v20.16.0-linux-x64-musl.tar.gz
mkdir nodejs
tar -zxvf *.tar.gz --directory /opt/nodejs --strip-components=1
ln -s /opt/nodejs/bin/node /usr/local/bin/node
ln -s /opt/nodejs/bin/npm /usr/local/bin/npm
;;
esac
cp -f "/app/ring-mqtt-${BRANCH}/init/s6/services.d/ring-mqtt/run" /etc/services.d/ring-mqtt/run
chmod +x /etc/services.d/ring-mqtt/run
fi