Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
cache: npm

- name: Install dependencies
run: npm ci

- name: Type-check / build
run: npm run build

- name: Run hermetic tests
run: npm test
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
node_modules/
dist/
recordings/
*.log

# Raw image-generator output (the finished hero lives in assets/).
nanobanana-output/

# Secrets — the refresh token grants full account access. Never commit it.
.ring-token.json
.env
config.local.json

# Stray image-generator outputs in assets (keep only curated hero.jpg)
assets/*.png
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Eli Fayerman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
36 changes: 36 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Ring Local Recorder
Copyright (c) 2026 Eli Fayerman
Licensed under the MIT License (see LICENSE).

================================================================================
NOT AFFILIATED WITH RING
================================================================================

This project is an independent, community-built tool. It is NOT affiliated with,
authorized, maintained, sponsored, or endorsed by Ring LLC, Amazon.com, Inc., or
any of their subsidiaries or affiliates. "Ring" and related marks are trademarks
of their respective owners and are used here only for identification purposes.

================================================================================
THIRD-PARTY COMPONENTS
================================================================================

This software depends on, but does not redistribute, the following packages.
They are installed from the npm registry at install time under their own
licenses:

- ring-client-api (MIT): https://github.com/dgreif/ring
Provides the Ring account/streaming integration. Uses Ring's unofficial
API; see the legal notes in README.md.
- rxjs (Apache-2.0)
- werift (MIT): WebRTC transport
- @homebridge/camera-utils (MIT)
- @eneris/push-receiver (MIT): push notification delivery

ffmpeg:
Recording relies on ffmpeg. By default the binary is provided at install time
by the "ffmpeg-for-homebridge" package; alternatively you may point the tool at
a system ffmpeg via the RING_FFMPEG environment variable. ffmpeg is licensed
under the LGPL/GPL depending on how it was built. This project does NOT bundle
or redistribute any ffmpeg binary; it invokes whatever ffmpeg is present on
the host. See https://ffmpeg.org/legal.html.
177 changes: 177 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Ring Camera Recorder

<p align="center">
<img src="assets/hero.jpg" alt="Ring Camera Recorder: self-hosted local recording for your Ring cameras" width="100%">
</p>

Self-hosted tool that **locally records the live view from Ring cameras you own**. It captures
the freely available live stream (not Ring's paid cloud storage), so it works **without a Ring
Protect subscription**. It records your own cameras and your own footage. Read the
[Legal & responsible use](#legal--responsible-use) section before using or sharing it.

> **You are recording the live stream, not downloading cloud history.** Without a Ring
> Protect subscription, Ring stores nothing in the cloud, so there is no event history to
> retrieve. This tool grabs the *live view* feed (the same one the app shows you for free)
> and writes it to disk, either on demand or automatically when a motion/doorbell event
> fires. That is the only no-subscription path.

## What it does

- Authenticates to your Ring account once (email + password + 2FA) and stores a
**refresh token**. Your password is never saved or hardcoded.
- Lists your cameras.
- Records the live stream to `recordings/{camera}_{ISO8601}.mp4`.
- Auto-records a clip when a camera reports **motion** or a doorbell **ding**.
- Manual "record now for N seconds" and continuous modes.
- Optional retention: delete clips older than N days.

## Requirements

- Node.js >= 20 (tested on 22).
- `ffmpeg` available on `PATH` (`ffmpeg -version` should work).
- A Ring account with at least one camera.

## Setup

```bash
npm install

# One-time interactive login. Prompts for email, password, and the 2FA code Ring
# texts/emails you, then writes the refresh token to .ring-token.json.
npm run auth

# Optional: copy and edit the config.
cp config.example.json config.json

# List your cameras (confirms auth works and shows names/ids for the `cameras` filter).
npm run list
```

## Usage

```bash
# Long-running service: watches for motion/ding and auto-records per config.json.
npm run build && npm start
# (during development you can skip the build step with: npm run dev)

# Record a single clip right now (default camera selection, default length):
npm run record -- --camera "Front Door" --seconds 30

# Full end-to-end verification (run after `npm run auth`): authenticates, lists
# cameras, captures a clip, probes it for video/audio, and (optionally) waits for
# a real motion/ding to confirm auto-recording. Trigger motion when prompted.
npm run verify -- --watch-motion 60
```

## Tests

```bash
npm test # builds, then runs hermetic logic tests (no Ring account needed)
```

Covers the filename convention, retention pruning, and the motion/ding trigger state
machine (rising-edge detection, no-overlap-while-recording, cooldown suppression,
doorbell-only-on-doorbots). The live network path is exercised separately by
`npm run verify` once you have a token.

## Configuration

Config is layered: built-in defaults < `config.json` < `config.local.json` < environment
variables. See `config.example.json`. Key fields:

| Field | Default | Meaning |
|-------|---------|---------|
| `tokenPath` | `.ring-token.json` | Where the refresh token lives. |
| `outputDir` | `recordings` | Where clips are written. |
| `clipLengthSeconds` | `30` | Default clip length for events + manual record. |
| `cameras` | `"all"` | `"all"` or an array of name substrings / numeric ids. |
| `recordOnMotion` | `true` | Auto-record on motion events. |
| `recordOnDing` | `true` | Auto-record on doorbell press. |
| `motionCooldownSeconds` | `20` | Min gap between auto-recordings per camera. |
| `retentionDays` | `null` | Delete clips older than N days. `null` = keep all. |
| `retentionSweepMinutes` | `60` | How often retention runs while the service is up. |

Environment overrides: `RING_TOKEN_PATH`, `RING_OUTPUT_DIR`, `RING_CLIP_SECONDS`,
`RING_RETENTION_DAYS`, `RING_DEBUG=1`.

## Triggering policy

When a motion/ding event fires, `src/events.ts` decides whether to start a clip. The
default `shouldTrigger()` policy:

- **Skips** if a recording for that camera is already running (no overlapping ffmpeg jobs).
- Enforces `motionCooldownSeconds` between the *start* of consecutive clips.

If you want "extend while motion persists" instead of fixed-length clips, or a different
cooldown, edit `shouldTrigger()`; it's deliberately isolated for that. One
no-subscription caveat: a clip can only begin *after* the event arrives, so there's no
pre-roll buffer (you lose the ~1-2s before the trigger).

## Running as a background service

- **systemd**: see `deploy/ring-camera-recorder.service` (header has install steps).
- **pm2**: `pm2 start deploy/ecosystem.config.cjs`.

Run `npm run auth` manually first: the login is interactive and the service only reads
the saved token.

## Limitations (no subscription)

- **Live-only.** No access to past clips / cloud event history; those require Ring Protect.
- Motion/ding **push notifications are still delivered free**, which is what triggers
auto-recording.
- Battery-powered cameras spin up the radio for each live stream, so frequent
motion-triggered recording will drain the battery faster than passive monitoring.
- Ring may throttle accounts that open live streams very aggressively; keep clip lengths
and cooldowns reasonable for 24/7 use, or use a locally-streaming camera instead.

## Security

The refresh token in `.ring-token.json` grants full access to your Ring account. It is
gitignored, written with `0600` permissions, and rotated tokens are persisted
automatically. Treat that file like a password.

`npm audit` reports a high-severity advisory in a transitive WebRTC dependency (`ip` via
`werift`). It is **not** in this project's code, is low-risk for personal use, and
**cannot be fixed without breaking streaming**. Do not run `npm audit fix --force`. See
[`SECURITY.md`](./SECURITY.md) for the full assessment and how to report issues.

## Contributing

Issues and pull requests are welcome. This is a small, best-effort project maintained in
spare time. Keep PRs focused, and note that the Ring integration depends on an
unofficial upstream API that can break without notice (see [`SECURITY.md`](./SECURITY.md)).
For anything security-related, follow `SECURITY.md` rather than opening a public issue.

By contributing, you agree your contributions are licensed under the MIT License (see
[`LICENSE`](./LICENSE)).

## Legal & responsible use

**This is not legal advice.** Use this tool at your own risk. By using it you accept
responsibility for complying with the laws and agreements that apply to you.

- **Your own cameras only.** Use this exclusively with cameras on a Ring account you
control, authenticating with your own credentials. It is a tool for recording *your own*
footage, not for accessing anyone else's account, cameras, or data.

- **Audio recording / consent.** Clips include **audio**. Many jurisdictions have
two-party / all-party consent ("wiretapping") laws. In some U.S. states and other
countries, recording other people's audio without consent can carry **civil and even
criminal** liability, independent of Ring. Recording your own property is generally fine;
capturing other people's conversations may not be. Know your local laws before recording,
and consider disabling audio where required.

- **Ring Terms of Service.** This relies on `ring-client-api`, which uses Ring's
**unofficial** API. Automated/unofficial access may violate Ring's / Amazon's Terms of
Service, and **Ring could suspend accounts** that use it. There is no guarantee of
continued access; Ring can change or block the API at any time (e.g. the 2026
`/oauth/v2` auth migration; see the upstream `dgreif/ring` issues).

- **Not affiliated with Ring.** This project is independent and is **not** affiliated with,
authorized, endorsed, or sponsored by Ring LLC or Amazon. "Ring" is a trademark of its
owner, used here only to describe interoperability. See [`NOTICE`](./NOTICE).

- **No warranty.** Provided "as is" under the [MIT License](./LICENSE), without warranty of
any kind. The authors are not liable for account suspensions, lost recordings, missed
events, or any other damages arising from use.
41 changes: 41 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Security

## Reporting a vulnerability

Please report security issues privately via GitHub's "Report a vulnerability"
(Security Advisories) on this repository, or by opening a minimal issue that does
**not** include any secrets, tokens, or personal footage. We'll respond as soon as
we reasonably can. This is a volunteer, best-effort project (see the no-warranty
note in [LICENSE](./LICENSE)).

## Handling of credentials

- This tool never stores your Ring password. The interactive login exchanges it for
a **refresh token**, written to `.ring-token.json` with `0600` permissions.
- That token grants full access to your Ring account; treat it like a password.
- `.ring-token.json`, `.env`, and `config.local.json` are git-ignored. **Never commit
them.** Run `git status` before your first commit to confirm.
- Rotated tokens are persisted automatically; the token value is never written to logs.
- `RING_DEBUG=1` enables verbose `ring-client-api` logging, which may include sensitive
request details. Use it only for local troubleshooting, never in shared logs.

## Known dependency advisory (`npm audit`)

`npm audit` reports high-severity findings that all trace to a single transitive
dependency: the `ip` package's `isPublic` SSRF miscategorization
([GHSA-2p57-rm9w-gvfp](https://github.com/advisories/GHSA-2p57-rm9w-gvfp)), pulled in
via `werift` / `werift-ice` (the WebRTC NAT-traversal stack) beneath `ring-client-api`.

- **Not in this project's code.** Nothing here calls `ip` directly; it lives in the
WebRTC ICE candidate layer.
- **Do NOT run `npm audit fix --force`.** Its only "fix" downgrades `ring-client-api`
to `9.13.0` (pre-WebRTC), which breaks streaming entirely. This project intentionally
pins `ring-client-api@14.3.0`.
- **Cannot be resolved downstream.** The current latest `ring-client-api` still depends
on the affected versions; it will clear once `werift` updates upstream.
- **Low practical risk for this use case.** The `ip` SSRF is relevant when an attacker
controls connection targets and the app relies on `ip.isPublic()` for SSRF protection.
Here, the WebRTC session connects to *your own* Ring account over authenticated,
DTLS-encrypted media; there is no untrusted-peer attack surface in a personal recorder.

We track this and will bump `ring-client-api` when an upstream release clears the advisory.
Binary file added assets/hero.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tokenPath": ".ring-token.json",
"outputDir": "recordings",
"clipLengthSeconds": 30,
"cameras": "all",
"recordOnMotion": true,
"recordOnDing": true,
"motionCooldownSeconds": 20,
"retentionDays": 14,
"retentionSweepMinutes": 60
}
23 changes: 23 additions & 0 deletions deploy/ecosystem.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// pm2 process definition. Usage:
// npm install && npm run build
// npm run auth # interactive, once
// pm2 start deploy/ecosystem.config.cjs
// pm2 logs ring-camera-recorder
// pm2 save && pm2 startup # survive reboots
module.exports = {
apps: [
{
name: 'ring-camera-recorder',
script: 'dist/index.js',
cwd: __dirname + '/..',
autorestart: true,
restart_delay: 10000,
max_restarts: 20,
env: {
// RING_OUTPUT_DIR: '/var/lib/ring-recordings',
// RING_RETENTION_DAYS: '14',
// RING_DEBUG: '1',
},
},
],
};
34 changes: 34 additions & 0 deletions deploy/ring-camera-recorder.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# systemd unit for Ring Camera Recorder.
#
# Install:
# 1. Build first: npm install && npm run build
# 2. Edit User/paths below to match your machine.
# 3. sudo cp deploy/ring-camera-recorder.service /etc/systemd/system/
# 4. sudo systemctl daemon-reload
# 5. sudo systemctl enable --now ring-camera-recorder
# 6. journalctl -u ring-camera-recorder -f # follow logs
#
# Note: run `npm run auth` MANUALLY once (it is interactive) before enabling the
# service. The service only reads the already-saved refresh token.

[Unit]
Description=Ring Camera Recorder (local live recording)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
# Replace with the unprivileged user and the absolute path where you cloned this repo.
User=youruser
WorkingDirectory=/home/youruser/ring-camera-recorder
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=10
# Token rotation writes back to the token file, so the working dir must be writable.
# Environment overrides (optional) — these win over config.json:
# Environment=RING_OUTPUT_DIR=/var/lib/ring-recordings
# Environment=RING_RETENTION_DAYS=14
# Environment=RING_DEBUG=1

[Install]
WantedBy=multi-user.target
Loading
Loading