mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
Release 5.0.0
This commit is contained in:
@@ -127,16 +127,29 @@ Of course there are other possible automation options as well, and, even without
|
||||
**Q) Streams keep starting all the time even when I'm not viewing anything**
|
||||
**A)** In Home Assistant, do not use the "Preload Stream" option and make sure the Camera View setting in the Picture Glance card is set to "auto" instead of "live". Otherwise the card will attempt to start streams in the background for faster startup when you bring up the card. This is fine for local cameras but, because Ring cameras do not send motion events during streams, having streams running all the time will cause motion events to be missed and, since all streaming goes through Ring servers on the Internet, you will use a lot of bandwidth as well.
|
||||
|
||||
**Q) Why does the live stream stop after ~10 minutes?**
|
||||
**A)** Ring enforces a time limit on active live streams and terminates them, typically at approximately 10 minutes, although sometimes significantly less and sometimes a little more. Currently, you'll need to refresh to manually start the stream again but it is NOT recommended to attempt to stream 24 hours. I say currently because Ring has hinted that continuous live streaming is something they are working on, but, for now, the code honors the exiting limits and does not just immediately retry.
|
||||
There have also been reports of certain components connecting to live streams with when no password is set on the RTSP server. To solve this simple make sure that a username/password is set on the stream so that services which automatically discover RTSP endpoints cannot connect and start streams.
|
||||
|
||||
**Q) Why is the stream delayed/lagged?**
|
||||
**A)** Likely this is due to the streaming technology used by Home Assistant that fully streams over HTTP/HTTPS. While the technology is extremely reliable and widely compatible with various web browsers and network setups, it typically adds betwee 5-8 seconds of delay and sometimes as many as 10-15 seconds. The best solution for Home Assistant is to use a custom UI card like the excellent [WebRTC Camera](https://github.com/AlexxIT/WebRTC) which will allow you to use your browsers native video playback capabilities, although this technology will likely require special configuration if you want to play back while outside of your network without using a VPN. However, when configured, it provides typically ~1-2 seconds of latency at most so it's the best option for getting as close to real-time viewing as possible within Home Assistant. Other options that offer lower latency viewing is to use an external media player capable of RTSP playback. VLC works well, but note that it buffers 1 second of video by default, although you can tweak this to reduce the delay.
|
||||
**Q) Why does the live stream stop after ~10 minutes?**
|
||||
**A)** Ring enforces a time limit on active live streams and terminates them, typically at approximately 10 minutes, although sometimes significantly less and sometimes a little more. Currently, you'll need to refresh to manually start the stream again but it is NOT recommended to attempt to stream 24 hours. I say currently because Ring has hinted that continuous live streaming is something they are working on, but, for now, the code honors the exiting limits and does not just immediately reconnect to the stream.
|
||||
|
||||
**Q) Why is the stream delayed/lagged?**
|
||||
**A)** The code path for streaming adds less than one second of latency, however, Home Assistant uses the HLS streaming protocol which splits the existing stream into chunks for delivery over HTTP/HTTPS. While HLS streaming is extremely reliable and widely compatible with various web browsers and network setups, it typically adds 8-10 seconds of delay and sometimes significantly more. Starting with Home Assistant 2022.2, the streaming component defaults to using low-latency HLS (LL-HLS) which helps to reduce latency to 3-5 seconds in most cases, however, this can lead to an increase in artifacts and video stuttering, see the question below on video artifact/stuttering for more details.
|
||||
|
||||
For the lowest latency vieweing, the best solution in Home Assistant is to use a custom UI card like the excellent [WebRTC Camera](https://github.com/AlexxIT/WebRTC) which will allow you to use your browsers native video playback capabilities. Although WebRTC may require special configuration if you want to play back while outside of your network without using a VPN, it by far the highest quality, lowest latency streaming option available in the Home Assistant UI, generally providing mostly artifact free viewing with latency in the 1-2 second range.
|
||||
|
||||
Other options that offer low-latency viewing is to use an external media player capable of RTSP playback. VLC works well, but note that it buffers 1 second of video by default, although you can tweak this to reduce the delay.
|
||||
|
||||
**Q) Why do I have video artifacts and/or stuttering in the stream?**
|
||||
**A)** There are two likely sources of artifacts/stuttering, I'll outline both below:
|
||||
- Ring streams seems to include a signficant number of minor encoding errors, especially at the start of streams, but I'm not really sure why. At first I thought this was a bug in the ring-client-api RTP handling, but then I realized that the same artifacts were completely reproducible when playing back the recorded videos downloaded from Ring, even if you feed them directly into Home Assistant via the FFmpeg camera source. More interestingly, the stutters and pauses were always in the exact same places. Attempting to decode the file with FFmpeg produced lots messages about minor decoding errors, however, if you view the same file in a media client like VLC, the artifacts seem very minor, even difficult to see in some case, but are much more obvious in the Home Assistant video playback. I believe the reason they are more pronounced in Home Assistant is due to the way Home Assistant converts the incoming AVC stream to HLS on the fly. My understanding is that, to save CPU since Home Assistant commonly runs on fairly low powered device, the stream component does not transcode the incoming stream, but simply chops the stream on I-frame boundaries to convert them to HLS segments. It seems this process amplifies the impact of these minor encoding errors. While it's possible to transcode the stream with ffmpeg within ring-mqtt, the cost is far higher CPU usage so for now I'm just feeding the stream as is and living with the minor stuttering that usually happens mostly in the first few seconds of the recording.
|
||||
- The second issue mostly impacts the live stream because the live stream uses UDP to send RTP streams and these streams are processed by ring-client-api inside of the NodeJS process before being sent via a pipe to ffmpeg. While Node is quite fast for an interpreted language like Javascript, it's still not exactly the most efficient for real-time stream processing so you need a reasonable amount of CPU available. Having a good CPU and a solid networking setup that does not drop UDP packets is critical to reliable function of the live stream. If you have mulitple cameras, or a system with limited CPU/RAM (RPi3 for example) then you should limit concurrent live streams to just a few at a time for the most stable video. In my testing a RPi3 struggles to support more than about 4 concurrent live streams, an RPi4 can handle about 6 concurrent live streams, and a decent Intel based machine can handle about 2-3 live streams per-core. These numbers assume that the load on the CPU from other components is minimal.
|
||||
- Ring streams include a signficant number of minor encoding anomolies, especially in the first 5 seconds of the stream. At first I thought this was a bug in stream handling in ring-mqtt, but it turns out identical anomolies exist in the video recording file downloaded from Ring servers, so it clearly indicates the error is in the stream encoding from the camera. These anomolies go largely unnoticed in media players that use the native stream, however, they cause issue with the Home Assistant stream component which uses specific markers to split the stream into the HLS segments and this is made even worse by the introduction of LL-HLS in Home Assistant 2022.2, which further chunks the streams into even smaller parts. Options are to disable LL-HLS in the congiguration, which minimizes artifacts but increases latency, or try the following settings to minimize (not eliminate) the artifacts while keeping most of the benefit of LL-HLS:
|
||||
```
|
||||
stream:
|
||||
ll_hls: true
|
||||
segment_duration: 3.625
|
||||
part_duration: 1.45
|
||||
```
|
||||
|
||||
- The second issue mostly impacts the live stream which uses RTP over UDP and these packets are then processed by ring-client-api inside of the NodeJS process before being sent via a pipe to ffmpeg. While Node is quite fast for an interpreted language like Javascript, it's still not exactly the most efficient for real-time stream processing so you need a reasonable amount of CPU available. Having a good CPU and a solid networking setup that does not drop UDP packets is critical to reliable function of the live stream. If you have mulitple cameras, or a system with limited CPU/RAM (RPi3 for example) then it will be difficult to support more than a handful a streams concurrently. Testing shows that an RPi3 struggles to support more than about 4 concurrent streams while an RPi4 can handle 6-7 concurrent live streams, and a decent Intel based machine can handle about 2-3 live streams per-core. These numbers assume that the load on the CPU from other components is minimal.
|
||||
|
||||
**Q) Why do I see high memory usage?**
|
||||
**A)** Support for live streaming uses rtsp-simple-server, which is a binary process running in addition to the normal node process used by ring-mqtt. When idle, this process uses quite minimal memory (typically <20MB). However, every stream has at least one FFmpeg process to read the incoming stream and publish it to the server. Total memory usage is typically about 25-30MB per each active stream on top of the base memory usage of the addon. Also, when using Home Assistant, the Home Assistant memory usage will also increase for each stream.
|
||||
@@ -145,7 +158,7 @@ Of course there are other possible automation options as well, and, even without
|
||||
**A)** This is a limitaiton of Ring cameras as they do not detect/send motion events while a stream/recording is active. The code itself has no limitations in this regard.
|
||||
|
||||
**Q) Why do I have so many recordings on my Ring App?**
|
||||
**A)** If you have a Ring Protet subscrition then all "live streams" are actually recording sessions as well, so every time you start a live view of your camera you will see a recording in the Ring app.
|
||||
**A)** If you have a Ring Protect subscrition then all "live streams" are actually recording sessions as well, so every time you start a live view of your camera you will see a recording in the Ring app.
|
||||
|
||||
### How it works - the gory details
|
||||
The concept for streaming is actually very simple and credit for the original idea must go to gilliginsisland's post on the [ring-hassio project](https://github.com/jeroenterheerdt/ring-hassio/issues/51). While I was already working on a concept implementation for ring-mqtt, when I read that post I realized that it was actually a strong model that could be married with ring-mqtt to support live streaming in a way that made a lot of sense. The post described a method to use rtsp-simple-server and it's ability to run a script on demand to directly run a node instance that leveraged ring-client-api to connect to Ring and start the live stream. Since ring-mqtt already ran in node and used ring-client-api, and already contained code for starting streams, I decided that instead of starting a separate node script, which had high startup and memory overhead, I could just have rtsp-simple-server start a simple shell script that used MQTT to signal ring-mqtt to start the stream on demand while also allowing streams to be started and stopped manually via MQTT commands.
|
||||
|
@@ -91,12 +91,11 @@ class Config {
|
||||
}
|
||||
|
||||
async updateConfig() {
|
||||
const configData = this.data
|
||||
if (configData.hasOwnProperty('ring_token')) {
|
||||
if (this.config.runMode === 'standard' && this.data.hasOwnProperty('ring_token')) {
|
||||
try {
|
||||
debug ('Updating config file to remove legacy ring_token value...')
|
||||
delete configData.ring_token
|
||||
await writeFileAtomic(this.file, JSON.stringify(configData, null, 4))
|
||||
delete this.data.ring_token
|
||||
await writeFileAtomic(this.file, JSON.stringify(this.data, null, 4))
|
||||
debug('Successfully saved updated config file: '+this.file)
|
||||
} catch (err) {
|
||||
debug('Failed to save updated config file: '+this.file)
|
||||
|
17
lib/state.js
17
lib/state.js
@@ -19,6 +19,7 @@ class State {
|
||||
this.file = (this.config.runMode === 'standard')
|
||||
? require('path').dirname(require.main.filename)+'/ring-state.json'
|
||||
: this.file = '/data/ring-state.json'
|
||||
|
||||
await this.loadStateData()
|
||||
}
|
||||
|
||||
@@ -35,6 +36,17 @@ class State {
|
||||
debug(err.message)
|
||||
debug('Saved state file exist but could not be parsed!')
|
||||
}
|
||||
} else {
|
||||
await this.initStateData()
|
||||
}
|
||||
}
|
||||
|
||||
async initStateData() {
|
||||
if (this.config.runMode === 'standard' && this.config.hasOwnProperty('ring_token')) {
|
||||
debug(colors.brightYellow('File '+this.file+' not found, creating new state file using existing ring_token from config file.'))
|
||||
this.data.systemId = (createHash('sha256').update(randomBytes(32)).digest('hex'))
|
||||
this.updateToken(this.config.ring_token, true)
|
||||
await this.config.updateConfig()
|
||||
} else {
|
||||
debug(colors.brightYellow('File '+this.file+' not found. No saved state data available.'))
|
||||
}
|
||||
@@ -44,7 +56,6 @@ class State {
|
||||
async updateToken(newRefreshToken, oldRefreshToken) {
|
||||
if (oldRefreshToken) {
|
||||
this.data.ring_token = newRefreshToken
|
||||
|
||||
try {
|
||||
await writeFileAtomic(this.file, JSON.stringify(this.data, null, 2))
|
||||
debug('Successfully saved updated refresh token in state file: '+this.file)
|
||||
@@ -52,10 +63,6 @@ class State {
|
||||
debug('Failed to save updated refresh token in state file: '+this.file)
|
||||
debug(err.message)
|
||||
}
|
||||
|
||||
if (this.config.runMode === 'standard') {
|
||||
await this.config.updateConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
ring-mqtt.js
87
ring-mqtt.js
@@ -416,7 +416,6 @@ function startMqtt(mqttClient, ringClient) {
|
||||
|
||||
// Main code loop
|
||||
const main = async(generatedToken) => {
|
||||
let ringAuth = new Object()
|
||||
let ringClient
|
||||
let mqttClient
|
||||
if (!state.valid) {
|
||||
@@ -437,16 +436,14 @@ const main = async(generatedToken) => {
|
||||
}
|
||||
|
||||
// If no refresh tokens were found, either exit or start Web UI for token generator
|
||||
if (!config.data.ring_token && !state.data.ring_token) {
|
||||
if (!state.data.ring_token) {
|
||||
if (config.runMode === 'docker') {
|
||||
debug(colors.brightRed('No refresh token was found in state file and RINGTOKEN is not configured.'))
|
||||
debug(colors.brightRed('No refresh token was found in state file, please generate a token with ring-auth-cli.js.'))
|
||||
process.exit(2)
|
||||
} else {
|
||||
if (config.runMode === 'addon') {
|
||||
debug(colors.brightRed('No refresh token was found in saved state file or config file.'))
|
||||
debug(colors.brightRed('Use the web interface to generate a new token.'))
|
||||
} else {
|
||||
debug(colors.brightRed('Use the web interface to generate a new token.'))
|
||||
debug(colors.brightRed('No refresh token was found in saved state file.'))
|
||||
debug(colors.brightRed('Use the web interface to generate a new token.'))
|
||||
if (config.runMode === 'standard') {
|
||||
tokenApp.start()
|
||||
}
|
||||
}
|
||||
@@ -458,26 +455,27 @@ const main = async(generatedToken) => {
|
||||
await utils.sleep(10)
|
||||
}
|
||||
|
||||
// Define some basic parameters for connection to Ring API
|
||||
if (config.data.enable_cameras) {
|
||||
ringAuth = {
|
||||
cameraStatusPollingSeconds: 20,
|
||||
cameraDingsPollingSeconds: 2
|
||||
}
|
||||
}
|
||||
|
||||
if (config.data.enable_modes) { ringAuth.locationModePollingSeconds = 20 }
|
||||
if (!(config.data.location_ids === undefined || config.data.location_ids == 0)) {
|
||||
ringAuth.locationIds = config.data.location_ids
|
||||
}
|
||||
ringAuth.controlCenterDisplayName = (config.runMode === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt'
|
||||
|
||||
// If there is a saved or generated refresh token, try to connect using it first
|
||||
if (state.data.ring_token) {
|
||||
const ringAuth = {
|
||||
refreshToken: state.data.ring_token,
|
||||
systemId: state.data.systemId,
|
||||
controlCenterDisplayName: (config.runMode === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt',
|
||||
...config.data.enable_cameras ? {
|
||||
cameraStatusPollingSeconds: 20,
|
||||
cameraDingsPollingSeconds: 2
|
||||
} : {},
|
||||
...config.data.enable_modes ? {
|
||||
locationModePollingSeconds: 20
|
||||
} : {},
|
||||
...(!(config.data.location_ids === undefined || config.data.location_ids == 0)) ? {
|
||||
locationIds: config.data.location_ids
|
||||
} : {},
|
||||
}
|
||||
|
||||
const tokenSource = generatedToken ? "generated" : "saved"
|
||||
debug('Attempting connection to Ring API using '+tokenSource+' refresh token.')
|
||||
ringAuth.refreshToken = state.data.ring_token
|
||||
ringAuth.systemId = state.data.systemId
|
||||
debug(`Attempting connection to Ring API using ${generatedToken ? "generated" : "saved"} refresh token.`)
|
||||
|
||||
try {
|
||||
ringClient = new RingApi(ringAuth)
|
||||
await ringClient.getProfile()
|
||||
@@ -487,37 +485,6 @@ const main = async(generatedToken) => {
|
||||
debug(colors.brightYellow('Unable to connect to Ring API using '+tokenSource+' refresh token.'))
|
||||
}
|
||||
}
|
||||
|
||||
// If Ring API is not already connected, try using refresh token from config file or RINGTOKEN variable
|
||||
if (!ringClient && config.data.ring_token) {
|
||||
const debugMsg = config.runMode === 'docker' ? 'RINGTOKEN environment variable.' : 'refresh token from file: '+config.file
|
||||
debug('Attempting connection to Ring API using '+debugMsg)
|
||||
ringAuth.refreshToken = config.data.ring_token
|
||||
try {
|
||||
ringClient = new RingApi(ringAuth)
|
||||
await ringClient.getProfile()
|
||||
} catch(error) {
|
||||
ringClient = null
|
||||
debug(colors.brightRed(error.message))
|
||||
debug(colors.brightRed('Could not create the API instance. This could be because the Ring servers are down/unreachable'))
|
||||
debug(colors.brightRed('or maybe all available refresh tokens are invalid.'))
|
||||
if (config.runMode === 'addon') {
|
||||
debug('Restart the addon to try again or use the web interface to generate a new token.')
|
||||
} else {
|
||||
debug('Please check the configuration and network settings, or generate a new refresh token, and try again.')
|
||||
process.exit(2)
|
||||
}
|
||||
}
|
||||
} else if (!ringClient && !config.data.ring_token) {
|
||||
// No connection with Ring API using saved token and no configured token to try
|
||||
if (config.runMode === 'docker') {
|
||||
debug(colors.brightRed('Could not connect with saved refresh token and RINGTOKEN is not configured.'))
|
||||
process.exit(2)
|
||||
} else if (config.runMode === 'addon') {
|
||||
debug(colors.brightRed('Could not connect with saved refresh token and no refresh token exist in config file.'))
|
||||
debug(colors.brightRed('Please use the web interface to generate a new token or restart the addon to try the existing token again.'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ringClient) {
|
||||
@@ -551,6 +518,14 @@ const main = async(generatedToken) => {
|
||||
debug( colors.red('Couldn\'t authenticate to MQTT broker. Please check the broker and configuration settings.'))
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
debug(colors.brightRed('Failed to connect to Ring API using the refresh token in the saved state file.'))
|
||||
if (config.runMode === 'docker') {
|
||||
debug(colors.brightRed('Restart the container to try again or generate a new token with ring-auth-cli.js.'))
|
||||
process.exit(2)
|
||||
} else {
|
||||
debug(colors.brightRed(`Use the web UI to generate a new token or restart the ${this.runMode === 'addon' ? 'addon' : 'script'} to try again with the existing token.`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user