Skip to content

node_2026: Docker-based RPi4 streaming node with Pisound HAT#82

Open
veirs wants to merge 5 commits into
mainfrom
node_2026
Open

node_2026: Docker-based RPi4 streaming node with Pisound HAT#82
veirs wants to merge 5 commits into
mainfrom
node_2026

Conversation

@veirs

@veirs veirs commented May 19, 2026

Copy link
Copy Markdown

Summary

This adds node_2026/, a new self-contained implementation of the hydrophone streaming node targeting Raspberry Pi 4 with a Pisound HAT. It runs the full audio stack — JACK, ffmpeg, and the Python uploaders — inside a Docker container, replacing the previous approach of installing those dependencies directly on the host OS.

The existing node/ directory is untouched. This is a parallel implementation for evaluation.

What's different from the existing node

Fully containerized

  • JACK2, ffmpeg, Python, and boto3 all run inside the container (Debian Trixie base)
  • The host only needs Docker installed — setup.sh handles that
  • No host-side jackd, ffmpeg, or Python venv required

Automatic recovery

  • restart: always + Docker enabled at boot replaces the systemd service
  • The container restarts itself on crash; no manual intervention needed on reboot

Internet outage recovery (catchup_s3.py)

  • Segments that fail to upload during an outage stay on disk
  • When connectivity returns, catchup_s3.py generates a VOD manifest (catchup.m3u8) for the gap and uploads all stranded segments at low priority (nice -n 10, 2s between uploads) so catch-up does not compete with the live stream
  • Disk guard: if stranded segments exceed 500 MB, oldest are deleted first to protect the SD card

JACK in Docker fixes

  • shm_size: 256m — Docker's default 64 MB /dev/shm is too small for JACK's shared memory
  • jackd -m flag disables memory locking (mlock fails inside containers regardless of ulimit settings)

RMS monitoring

  • upload_s3.py computes and logs RMS level for every .ts segment
  • Very low RMS triggers a warning, making silent-capture failures immediately visible in logs

Test mode

  • NO_UPLOAD=true in .env runs the full pipeline (JACK → ffmpeg → HLS segments) without touching S3, for validating a new node before going live

Log rotation

  • Docker log driver capped at 3 × 10 MB so logs do not fill the SD card over time

Files

File Description
Dockerfile Debian Trixie, JACK2, ffmpeg, Python venv
docker-compose.yml Service config with device passthrough, shm_size, log rotation
stream_sync.sh Startup: time sync, ALSA discovery, JACK, ffmpeg, uploaders
upload_s3.py Real-time HLS segment uploader with RMS monitoring
catchup_s3.py Internet outage recovery uploader (new)
setup.sh One-time Pi setup: Docker install, IPv6 fix, audio group
.env.template Config template — copy to .env and fill in credentials
README.txt Full setup and operation guide

Testing

Tested on Raspberry Pi 4 (4 GB) running Raspberry Pi OS Trixie with a Pisound HAT. Verified:

  • JACK starts and connects to the Pisound ALSA device
  • ffmpeg captures audio and generates HLS segments
  • Container restarts cleanly after docker compose restart and after reboot
  • NO_UPLOAD=true mode accumulates segments locally without errors

How to evaluate

git clone https://github.com/orcasound/orcanode.git
cd orcanode/node_2026
cp .env.template .env
# fill in .env with your node config and AWS credentials
# set NO_UPLOAD=true to test without S3
bash setup.sh
# after reboot:
docker compose up -d --build
docker compose logs -f

Summary by CodeRabbit

  • New Features

    • Containerized Raspberry Pi hydrophone node with automated hardware discovery, capture, encoding (HLS + optional FLAC research mode), and startup orchestration.
    • Robust upload tooling: real-time uploader, offline catch‑up uploader with disk‑guard and silence checks, and a manifest repair utility for incomplete HLS sessions.
  • Documentation

    • Detailed deployment and operations guide plus a dedicated manifest-repair HOWTO.
  • Chores

    • Added environment template and repository ignore rules; improved container compose and setup automation.

Review Change Stack

Fully containerized implementation of the hydrophone streaming node.
JACK, ffmpeg, and Python uploaders all run inside the container;
the host only needs Docker. Includes internet outage recovery via
catchup_s3.py and automatic restart via Docker restart: always.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Dockerized hydrophone node: image, compose, Pi provisioning, entrypoint orchestration (JACK + ffmpeg), live and catch-up S3 uploaders, manifest-repair tooling, environment templates, and deployment/troubleshooting documentation.

Changes

Orcasound Hydrophone Streaming Node

Layer / File(s) Summary
Build configuration and env defaults
node_2026/.dockerignore, node_2026/.gitignore, node_2026/.env.template
Docker and git ignore rules plus .env.template providing AWS, node identity, audio, logging, and timing defaults.
Docker image and Pi provisioning
node_2026/Dockerfile, node_2026/docker-compose.yml, node_2026/setup.sh
Debian/Trixie image with ALSA/JACK/ffmpeg and Python venv; compose defines privileged streaming service (audio passthrough, shm/rtprio/ulimit, Chrony/time mounts); setup.sh provisions Raspberry Pi OS, installs Docker, and applies IPv4/hosts workaround, cron, and group changes.
Audio pipeline initialization and JACK orchestration
node_2026/stream_sync.sh
Entrypoint validates binaries and clock, discovers ALSA device, starts jackd, launches ffmpeg (HLS ± FLAC), connects JACK ports, prepares HLS dirs, and starts uploader subprocesses.
Live HLS segment upload via inotify monitoring
node_2026/upload_s3.py
Watches HLS directory for closed/moved files, computes RMS for .ts segments, uploads files to S3 via boto3, and removes local copies; uploads latest.txt on first segment.
Catch-up uploader for internet outage recovery
node_2026/catchup_s3.py
Scans for stale .ts segments, enforces disk-guard to cap stranded data, writes catch-up HLS manifests, uploads manifests and segments to S3 with connectivity checks and throttling, and deletes uploaded local files.
Manifest repair tool and docs
node_2026/repair_manifests.py, node_2026/README_REPAIR.md
CLI tool to rebuild complete live.m3u8 playlists from S3 segments for past sessions; README documents algorithm, usage, and accuracy expectations.
Deployment and troubleshooting documentation
node_2026/README.txt
Step-by-step first-boot, .env configuration, setup script usage, docker compose instructions, verification steps, outage-recovery behavior, and troubleshooting notes.

Sequence Diagrams

sequenceDiagram
  participant Operator
  participant Pi_OS
  participant setup.sh
  participant Docker_Daemon
  participant Dockerfile
  participant Container

  Operator->>setup.sh: Run setup.sh
  setup.sh->>Pi_OS: apt update/upgrade, install Docker
  setup.sh->>Pi_OS: Add user to docker & audio groups
  setup.sh->>Docker_Daemon: Write daemon.json (disable IPv6)
  Operator->>Docker_Daemon: docker compose up
  Docker_Daemon->>Dockerfile: Build image
  Dockerfile->>Container: Install ALSA/JACK/ffmpeg and /venv deps
  Docker_Daemon->>Container: Mount /dev/snd, /etc/localtime, /var/run/chrony
  Container->>Container: Run stream_sync.sh entrypoint
Loading
sequenceDiagram
  participant Container
  participant stream_sync
  participant ALSA_Device
  participant jackd
  participant ffmpeg
  participant JACK_System
  participant Python_Uploaders

  Container->>stream_sync: Entrypoint
  stream_sync->>stream_sync: Preflight checks + wait_for_sync
  stream_sync->>ALSA_Device: aplay -l (discover device)
  stream_sync->>jackd: Start jackd with ALSA device
  stream_sync->>jackd: Wait for JACK ready
  stream_sync->>ffmpeg: Start ffmpeg (HLS ± FLAC)
  ffmpeg->>Filesystem: Emit HLS .ts segments
  stream_sync->>JACK_System: Connect capture ports -> ffjack inputs
  stream_sync->>Python_Uploaders: Start upload_s3.py & catchup_s3.py
Loading
sequenceDiagram
  participant ffmpeg
  participant HLS_Dir
  participant upload_s3
  participant inotify
  participant ffmpeg_RMS
  participant boto3
  participant S3

  ffmpeg->>HLS_Dir: Write .ts segment
  inotify->>upload_s3: IN_CLOSE_WRITE / IN_MOVED_TO
  upload_s3->>HLS_Dir: Verify file exists and size>0
  alt .ts segment
    upload_s3->>ffmpeg_RMS: Decode + compute RMS
    ffmpeg_RMS->>upload_s3: RMS value
    upload_s3->>upload_s3: Log RMS (warn if low)
  end
  upload_s3->>boto3: upload_file(local_path, s3_key)
  boto3->>S3: Store object
  S3->>upload_s3: Success
  upload_s3->>HLS_Dir: Delete local file
  alt First .ts segment
    upload_s3->>S3: Upload latest.txt
  end
Loading
sequenceDiagram
  participant CatchupMain
  participant Filesystem
  participant DiskGuard
  participant Connectivity
  participant S3

  loop every SCAN_INTERVAL
    CatchupMain->>Filesystem: find_stranded_segments (age>STALE_AGE)
    Filesystem->>CatchupMain: list of .ts files
    CatchupMain->>DiskGuard: enforce_disk_guard(total_size)
    alt size > MAX_STRANDED_BYTES
      DiskGuard->>Filesystem: delete oldest files
    end
    CatchupMain->>Connectivity: is_connected()
    alt connected
      CatchupMain->>Filesystem: write catchup.m3u8
      CatchupMain->>S3: upload manifest then segments (throttled)
      S3->>Filesystem: on success -> delete uploaded locals
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • paulcretu
  • scottveirs

Poem

🐰 I sniff the streams where currents hum,
JACK and ffmpeg beat the drum,
Segments stitch to cloud-bound threads,
When nets nap, I mend their beds,
A hopping node keeps data from glum.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: a new Docker-based streaming node implementation for Raspberry Pi 4 with Pisound HAT, which aligns with the core purpose of adding node_2026/.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch node_2026

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
node_2026/setup.sh (1)

23-23: ⚡ Quick win

Avoid piping a remote script directly to sh for Docker installation.

This weakens supply-chain integrity controls. Prefer a pinned/reviewable install path (official apt repo steps with keyring and explicit package versions).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/setup.sh` at line 23, Replace the unsafe one-liner "curl -fsSL
https://get.docker.com | sudo sh" with an explicit, reviewable install sequence:
either (A) add the official Docker apt repository and GPG key using apt-key
replacement (store key in /usr/share/keyrings/docker-archive-keyring.gpg), add
the repository with "signed-by", run apt update, and install pinned package
versions like docker-ce=<version> docker-ce-cli=<version> containerd.io, or (B)
if you must use the upstream install script, download it first (curl -fsSL -o
get-docker.sh), verify its checksum or signature, review it, then run it locally
with sudo sh get-docker.sh; update setup.sh to implement one of these secure
flows instead of piping directly to sh.
node_2026/docker-compose.yml (1)

2-8: ⚡ Quick win

Add an init process for cleaner shutdown/restart behavior.

Using init: true helps forward signals and reap zombies for the bash entrypoint, improving stop/restart reliability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/docker-compose.yml` around lines 2 - 8, The docker-compose service
"streaming" lacks an init process; add the "init: true" key under the streaming
service definition (at the same indentation level as image/build/command) so
Docker will use tini to forward signals and reap zombies for the
./stream_sync.sh entrypoint, improving shutdown/restart behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@node_2026/catchup_s3.py`:
- Around line 28-35: The module-level BUCKET logic can leave BUCKET empty when
BUCKET_TYPE is unset; update the top-level code around BUCKET, BUCKET_TYPE and
BUCKET_STREAMING in catchup_s3.py to fail fast: if BUCKET_TYPE is missing or
empty raise a clear RuntimeError (or SystemExit) explaining BUCKET_TYPE must be
set; when BUCKET_TYPE == "custom" also validate BUCKET_STREAMING exists and
raise a clear error if not; otherwise continue to set BUCKET to
"audio-orcasound-net" for "prod" and "dev-streaming-orcasound-net" for other
values.
- Around line 83-94: The sorted() key can raise FileNotFoundError when
os.path.getmtime(p) is called for a file removed concurrently; update
enforce_disk_guard to use a safe mtime getter (e.g. safe_getmtime(path) that
wraps os.path.getmtime in try/except and returns a sentinel like float('inf') or
filters out missing files) and/or pre-filter stranded to only existing paths
before sorting; then sort using key=safe_getmtime, proceed with the existing
loop that pops victims and calls total_size(aged) against MAX_STRANDED_BYTES,
and ensure aged only contains existing files when returned.

In `@node_2026/docker-compose.yml`:
- Around line 20-35: Remove the broad "privileged: true" setting and instead
grant only the needed hardware and kernel capabilities: keep the existing
devices mapping (devices: - "/dev/snd:/dev/snd"), preserve shm_size and ulimits
(rtprio and memlock), and add targeted fields such as cap_add:
["SYS_NICE","IPC_LOCK"] and group_add if a specific group is required; ensure
logging options remain unchanged. Update the docker-compose service block where
"privileged" is set to replace it with the explicit cap_add and group_add
entries and verify the device list and ulimits are intact so the container
retains required real-time and memory-lock privileges without full host
privilege escalation.

In `@node_2026/Dockerfile`:
- Around line 36-46: The container currently runs as root (see RUN usermod -aG
audio root and CMD ["./stream_sync.sh"]); create a dedicated non-root runtime
user (e.g., "appuser"), add that user to the audio group, chown /app and any
needed sockets/dev files to that user, remove the usermod root modification, and
switch to that user with USER appuser before the final CMD so the process
executes without root privileges while still retaining access to audio via group
membership; ensure stream_sync.sh remains executable and owned by the non-root
user.

In `@node_2026/README.txt`:
- Around line 53-56: The README references the wrong project directory name:
replace usages of the old directory string "node_val_docker" with the new
workflow directory "node_2026" in all occurrences (e.g., the commands that
change into ~/orcanode/node_val_docker and any subsequent references), update
the example cd/git clone steps and any paths mentioned around the spots flagged
(previously at lines referencing cd ~/orcanode/node_val_docker) so they
consistently use ~/orcanode/node_2026, and verify other references (e.g., in
setup, build, and run instructions) are updated to the unique identifiers
"node_val_docker" -> "node_2026" to avoid broken navigation.
- Around line 142-145: Update the watch command so it targets actual .ts segment
files instead of directories: replace the current '/tmp/<NODE_NAME>/hls/*/' glob
with a file glob that matches segment files (e.g. '/tmp/<NODE_NAME>/hls/*.ts' or
if segments live in per-stream subfolders use '/tmp/<NODE_NAME>/hls/*/*.ts') in
the README command so operators can see real-time .ts segment churn.

In `@node_2026/setup.sh`:
- Around line 53-58: The script currently unconditionally overwrites
/etc/docker/daemon.json via the heredoc written with "sudo tee
/etc/docker/daemon.json" (the DAEMONJSON heredoc); change this to preserve
existing settings by first checking for an existing file and merging the desired
keys instead of clobbering: if /etc/docker/daemon.json exists, load it and
merge/patch the "ipv6" and "dns" entries using jq (or create a temp JSON merge),
then write the merged result atomically back to /etc/docker/daemon.json; if it
doesn’t exist, create it as before. Ensure you handle permissions via sudo and
avoid partial writes by writing to a temp file and moving it into place.
- Around line 37-51: Remove the entire curl-based IPv4 extraction and /etc/hosts
mutation block in setup.sh (the code that sets DOCKER_IP and DOCKER_AUTH_IP, the
grep -qF checks, the sudo tee -a /etc/hosts lines, and the corresponding echo
warnings), since daemon.json already enforces IPv4/dns behavior; leave the
daemon.json configuration unchanged and ensure no other part of setup.sh depends
on DOCKER_IP or DOCKER_AUTH_IP before committing.

In `@node_2026/stream_sync.sh`:
- Around line 1-159: Save the jackd PID immediately after launching it (capture
PID into JACK_PID right after the jackd start line) and stop relying on implicit
background jobs; move FFMPEG_PID assignment into each NODE_TYPE branch where
ffmpeg is started so it's always set, then add a trap for SIGTERM, SIGINT, and
EXIT that explicitly kills and waits for JACK_PID, FFMPEG_PID and any background
python uploader PIDs (e.g., catchup_s3, upload_s3, upload_flac_s3) using kill
-TERM <PID> and wait <PID> (avoid kill 0); ensure the trap runs before launching
background uploaders and that all spawned PIDs are recorded so the trap can
terminate them cleanly.

In `@node_2026/upload_s3.py`:
- Around line 116-118: Replace the bare "except:" in upload_s3.py with a safe
exception handler: catch "Exception as e" instead of all exceptions so
KeyboardInterrupt/SystemExit are not swallowed, and log the full traceback (for
example via log.exception or traceback.format_exc()) when reporting "error
uploading to S3". Target the existing except block that currently does "except:"
/ "e = sys.exc_info()[0]" / "log.critical(...)" and change it to use "except
Exception as e" and emit the error details.
- Around line 129-135: The IN_MOVED_TO branch currently uploads every moved file
while the IN_CLOSE_WRITE branch skips temp files; update the IN_MOVED_TO handler
to apply the same guard by checking that 'tmp' not in filename before calling
s3_copy_file(path, filename). Locate the block that inspects type_names[0] ==
'IN_MOVED_TO' and add the same conditional used for the IN_CLOSE_WRITE branch
(the 'tmp' check and log.debug call) so only non-temporary files get passed to
s3_copy_file.
- Around line 28-39: The BUCKET variable can remain empty when BUCKET_TYPE is
unset causing obscure S3 errors; update upload_s3.py to validate
BUCKET_TYPE/BUCKET after the existing conditional (or when declaring BUCKET) and
fail fast with a clear error message (raise an exception or exit) if BUCKET_TYPE
is missing or BUCKET is still empty; reference the BUCKET and BUCKET_TYPE
environment checks and the BUCKET variable so the change is applied right after
that logic so any boto3 upload code using BUCKET sees a clear, early failure.

---

Nitpick comments:
In `@node_2026/docker-compose.yml`:
- Around line 2-8: The docker-compose service "streaming" lacks an init process;
add the "init: true" key under the streaming service definition (at the same
indentation level as image/build/command) so Docker will use tini to forward
signals and reap zombies for the ./stream_sync.sh entrypoint, improving
shutdown/restart behavior.

In `@node_2026/setup.sh`:
- Line 23: Replace the unsafe one-liner "curl -fsSL https://get.docker.com |
sudo sh" with an explicit, reviewable install sequence: either (A) add the
official Docker apt repository and GPG key using apt-key replacement (store key
in /usr/share/keyrings/docker-archive-keyring.gpg), add the repository with
"signed-by", run apt update, and install pinned package versions like
docker-ce=<version> docker-ce-cli=<version> containerd.io, or (B) if you must
use the upstream install script, download it first (curl -fsSL -o
get-docker.sh), verify its checksum or signature, review it, then run it locally
with sudo sh get-docker.sh; update setup.sh to implement one of these secure
flows instead of piping directly to sh.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa297f84-1fb3-44e8-9eed-ab5b593113ee

📥 Commits

Reviewing files that changed from the base of the PR and between 2cd6cc1 and 860bebd.

📒 Files selected for processing (10)
  • node_2026/.dockerignore
  • node_2026/.env.template
  • node_2026/.gitignore
  • node_2026/Dockerfile
  • node_2026/README.txt
  • node_2026/catchup_s3.py
  • node_2026/docker-compose.yml
  • node_2026/setup.sh
  • node_2026/stream_sync.sh
  • node_2026/upload_s3.py

Comment thread node_2026/catchup_s3.py
Comment on lines +28 to +35
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if os.environ["BUCKET_TYPE"] == "prod":
BUCKET = "audio-orcasound-net"
elif os.environ["BUCKET_TYPE"] == "custom":
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same issue: empty BUCKET when BUCKET_TYPE is unset.

Same problem as in upload_s3.py—if BUCKET_TYPE is not set, BUCKET remains empty and S3 operations will fail with an unclear error. Add a fail-fast check.

Proposed fix
 BUCKET = ""
 if "BUCKET_TYPE" in os.environ:
     if os.environ["BUCKET_TYPE"] == "prod":
         BUCKET = "audio-orcasound-net"
     elif os.environ["BUCKET_TYPE"] == "custom":
         BUCKET = os.environ["BUCKET_STREAMING"]
     else:
         BUCKET = "dev-streaming-orcasound-net"
+else:
+    log.critical("BUCKET_TYPE not set; exiting")
+    sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if os.environ["BUCKET_TYPE"] == "prod":
BUCKET = "audio-orcasound-net"
elif os.environ["BUCKET_TYPE"] == "custom":
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if os.environ["BUCKET_TYPE"] == "prod":
BUCKET = "audio-orcasound-net"
elif os.environ["BUCKET_TYPE"] == "custom":
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"
else:
log.critical("BUCKET_TYPE not set; exiting")
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/catchup_s3.py` around lines 28 - 35, The module-level BUCKET logic
can leave BUCKET empty when BUCKET_TYPE is unset; update the top-level code
around BUCKET, BUCKET_TYPE and BUCKET_STREAMING in catchup_s3.py to fail fast:
if BUCKET_TYPE is missing or empty raise a clear RuntimeError (or SystemExit)
explaining BUCKET_TYPE must be set; when BUCKET_TYPE == "custom" also validate
BUCKET_STREAMING exists and raise a clear error if not; otherwise continue to
set BUCKET to "audio-orcasound-net" for "prod" and "dev-streaming-orcasound-net"
for other values.

Comment thread node_2026/catchup_s3.py
Comment on lines +83 to +94
def enforce_disk_guard(stranded):
"""Delete oldest stranded segments if total exceeds MAX_STRANDED_BYTES."""
# Sort oldest first (by mtime)
aged = sorted(stranded, key=lambda p: os.path.getmtime(p))
while total_size(aged) > MAX_STRANDED_BYTES and aged:
victim = aged.pop(0)
try:
os.remove(victim)
log.warning(f"Disk guard: deleted {os.path.basename(victim)}")
except FileNotFoundError:
pass
return aged # updated list after deletions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sorted() key can raise FileNotFoundError if a file is deleted mid-operation.

If a file in stranded is deleted between finding it and sorting, os.path.getmtime(p) in the key function will raise FileNotFoundError, crashing the disk guard.

Proposed fix using a safe getter
 def enforce_disk_guard(stranded):
     """Delete oldest stranded segments if total exceeds MAX_STRANDED_BYTES."""
-    # Sort oldest first (by mtime)
-    aged = sorted(stranded, key=lambda p: os.path.getmtime(p))
+    # Sort oldest first (by mtime); deleted files sort to end
+    def safe_mtime(p):
+        try:
+            return os.path.getmtime(p)
+        except FileNotFoundError:
+            return float('inf')
+    aged = sorted(stranded, key=safe_mtime)
     while total_size(aged) > MAX_STRANDED_BYTES and aged:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/catchup_s3.py` around lines 83 - 94, The sorted() key can raise
FileNotFoundError when os.path.getmtime(p) is called for a file removed
concurrently; update enforce_disk_guard to use a safe mtime getter (e.g.
safe_getmtime(path) that wraps os.path.getmtime in try/except and returns a
sentinel like float('inf') or filters out missing files) and/or pre-filter
stranded to only existing paths before sorting; then sort using
key=safe_getmtime, proceed with the existing loop that pops victims and calls
total_size(aged) against MAX_STRANDED_BYTES, and ensure aged only contains
existing files when returned.

Comment on lines +20 to +35
devices:
- "/dev/snd:/dev/snd"

# 2. Grant high-level hardware access
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
privileged: true
shm_size: '256m'
ulimits:
rtprio: 95
memlock:
soft: -1
hard: -1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid full privileged mode; grant only required capabilities/devices.

privileged: true grants near-host-level control. For this workload, prefer explicit devices, group_add, and targeted cap_add (for example, SYS_NICE, possibly IPC_LOCK) plus ulimits. This materially reduces security risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/docker-compose.yml` around lines 20 - 35, Remove the broad
"privileged: true" setting and instead grant only the needed hardware and kernel
capabilities: keep the existing devices mapping (devices: -
"/dev/snd:/dev/snd"), preserve shm_size and ulimits (rtprio and memlock), and
add targeted fields such as cap_add: ["SYS_NICE","IPC_LOCK"] and group_add if a
specific group is required; ensure logging options remain unchanged. Update the
docker-compose service block where "privileged" is set to replace it with the
explicit cap_add and group_add entries and verify the device list and ulimits
are intact so the container retains required real-time and memory-lock
privileges without full host privilege escalation.

Comment thread node_2026/Dockerfile
Comment on lines +36 to +46
RUN usermod -aG audio root

###### Set working directory and copy files ############################

WORKDIR /app
COPY . .

RUN chmod +x stream_sync.sh
###### Runtime #########################################################

CMD ["./stream_sync.sh"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drop root runtime user for the container process.

The image runs as root, and this stacks with privileged: true in compose, making host compromise impact much worse. Add a non-root runtime user and only keep the minimum permissions required for audio access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/Dockerfile` around lines 36 - 46, The container currently runs as
root (see RUN usermod -aG audio root and CMD ["./stream_sync.sh"]); create a
dedicated non-root runtime user (e.g., "appuser"), add that user to the audio
group, chown /app and any needed sockets/dev files to that user, remove the
usermod root modification, and switch to that user with USER appuser before the
final CMD so the process executes without root privileges while still retaining
access to audio via group membership; ensure stream_sync.sh remains executable
and owned by the non-root user.

Comment thread node_2026/README.txt
Comment on lines +53 to +56
sudo apt-get install -y git
git clone https://github.com/orcasound/orcanode.git ~/orcanode
cd ~/orcanode/node_val_docker

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix inconsistent project path references (node_val_docker vs node_2026).

Line 55, Line 101, Line 116, Line 216, and Line 218 point users to ~/orcanode/node_val_docker, but this PR introduces the workflow under ~/orcanode/node_2026. These commands will send users to the wrong directory.

Suggested doc fix
-  cd ~/orcanode/node_val_docker
+  cd ~/orcanode/node_2026
...
-  cd ~/orcanode/node_val_docker
+  cd ~/orcanode/node_2026
...
-  cd ~/orcanode/node_val_docker
+  cd ~/orcanode/node_2026
...
-    file ~/orcanode/node_val_docker/.env
+    file ~/orcanode/node_2026/.env
...
-    sed -i 's/\r//' ~/orcanode/node_val_docker/.env
+    sed -i 's/\r//' ~/orcanode/node_2026/.env

Also applies to: 101-103, 116-117, 216-219

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/README.txt` around lines 53 - 56, The README references the wrong
project directory name: replace usages of the old directory string
"node_val_docker" with the new workflow directory "node_2026" in all occurrences
(e.g., the commands that change into ~/orcanode/node_val_docker and any
subsequent references), update the example cd/git clone steps and any paths
mentioned around the spots flagged (previously at lines referencing cd
~/orcanode/node_val_docker) so they consistently use ~/orcanode/node_2026, and
verify other references (e.g., in setup, build, and run instructions) are
updated to the unique identifiers "node_val_docker" -> "node_2026" to avoid
broken navigation.

Comment thread node_2026/setup.sh
Comment on lines +53 to +58
sudo tee /etc/docker/daemon.json > /dev/null << 'DAEMONJSON'
{
"ipv6": false,
"dns": ["8.8.8.8", "8.8.4.4"]
}
DAEMONJSON

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -name "setup.sh" -path "*/node_2026/*" 2>/dev/null | head -20

Repository: orcasound/orcanode

Length of output: 118


🏁 Script executed:

cd /repo && git ls-files | grep -i "node_2026/setup.sh"

Repository: orcasound/orcanode

Length of output: 118


🏁 Script executed:

find . -name "setup.sh" -path "*/node_2026/*" 2>/dev/null

Repository: orcasound/orcanode

Length of output: 83


🏁 Script executed:

git ls-files | grep "node_2026/setup.sh"

Repository: orcasound/orcanode

Length of output: 81


🏁 Script executed:

wc -l node_2026/setup.sh

Repository: orcasound/orcanode

Length of output: 84


🏁 Script executed:

cat -n node_2026/setup.sh | sed -n '45,70p'

Repository: orcasound/orcanode

Length of output: 1203


🏁 Script executed:

cat -n node_2026/setup.sh | head -60

Repository: orcasound/orcanode

Length of output: 2551


🏁 Script executed:

grep -n "daemon.json" node_2026/setup.sh

Repository: orcasound/orcanode

Length of output: 126


🏁 Script executed:

grep -n "docker" node_2026/setup.sh -i | head -20

Repository: orcasound/orcanode

Length of output: 1173


Avoid overwriting /etc/docker/daemon.json unconditionally.

This script uses sudo tee to write the daemon configuration without checking for or preserving existing settings. On fresh Docker installs this may not matter, but the script has no safeguards to prevent silent loss of pre-existing daemon settings if run on a system that already has Docker configured. Use jq to merge settings into the existing file, or check if the file exists before writing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/setup.sh` around lines 53 - 58, The script currently
unconditionally overwrites /etc/docker/daemon.json via the heredoc written with
"sudo tee /etc/docker/daemon.json" (the DAEMONJSON heredoc); change this to
preserve existing settings by first checking for an existing file and merging
the desired keys instead of clobbering: if /etc/docker/daemon.json exists, load
it and merge/patch the "ipv6" and "dns" entries using jq (or create a temp JSON
merge), then write the merged result atomically back to /etc/docker/daemon.json;
if it doesn’t exist, create it as before. Ensure you handle permissions via sudo
and avoid partial writes by writing to a temp file and moving it into place.

Comment thread node_2026/stream_sync.sh
Comment on lines +1 to +159
#!/bin/bash
# Script for live DASH/HLS streaming lossy audio as AAC and/or archiving lossless audio as FLAC

# Ensure system binaries are in PATH
export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/bin:$PATH

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# --- 0. PREFLIGHT CHECKS ---
# The .env sourcing is commented out because Docker Compose handles variable injection.
# if [ -f "$SCRIPT_DIR/.env" ]; then
# set -a
# source "$SCRIPT_DIR/.env"
# set +a
# echo "Loaded .env from $SCRIPT_DIR"
# fi

if ! command -v jackd &> /dev/null; then
echo "ERROR: jackd is not installed."
exit 1
fi
if ! command -v jack_wait &> /dev/null; then
echo "ERROR: jack_wait is not installed."
exit 1
fi
if ! command -v ffmpeg &> /dev/null; then
echo "ERROR: ffmpeg is not installed."
exit 1
fi

# --- 1. TIME SYNCHRONIZATION ---
wait_for_sync() {
echo "Checking for sane system time..."
local max_wait=60
local elapsed=0

# Check if the year is 2025 or later
while [ $(date +%Y) -lt 2025 ]; do
if [ $elapsed -ge $max_wait ]; then
echo "ERROR: Time sync timed out, aborting."
exit 1
fi
echo "Waiting for time sync (current year: $(date +%Y))..."
sleep 2
elapsed=$((elapsed + 2))
done
echo "Time looks sane: $(date)"
}

wait_for_sync

# --- 2. ACTIVATE VIRTUAL ENVIRONMENT ---
# Updated to use the absolute path within the container.
source /venv/bin/activate

# --- 3. DYNAMIC AUDIO DEVICE DISCOVERY ---
echo "Searching for ALSA device: $AUDIO_HW_ID..."

# Loop to find the hardware index (e.g., card 3) and set the correct hw address.
for i in $(seq 1 30); do
if aplay -l 2>/dev/null | grep -qi "$AUDIO_HW_ID"; then
# Extracts the number from a line like "card 3: pisound [pisound]".
CARD_NUM=$(aplay -l | grep -i "$AUDIO_HW_ID" | head -n 1 | awk -F'[: ]+' '{print $2}')

if [ -n "$CARD_NUM" ]; then
SOUND_CARD="hw:$CARD_NUM,0"
echo "Success! $AUDIO_HW_ID found at index $CARD_NUM. Using address: $SOUND_CARD"
break
fi
fi
echo "Audio device $AUDIO_HW_ID not ready yet, attempt $i..."
sleep 1
done

if [ -z "$SOUND_CARD" ]; then
echo "ERROR: ALSA device $AUDIO_HW_ID not found after 30s, aborting."
exit 1
fi

# --- 4. VARIABLES & DIRECTORIES ---
timestamp=$(date +%s)
echo "Node started at $timestamp. Node name: $NODE_NAME. Sound card address: $SOUND_CARD"

mkdir -p /tmp/$NODE_NAME/flac
mkdir -p /tmp/$NODE_NAME/hls/$timestamp
echo $timestamp > /tmp/$NODE_NAME/latest.txt

STREAM_RATE=48000
SAMPLE_RATE=${SAMPLE_RATE:-48000}

# --- 5. SETUP JACK ---
# Start jackd using the discovered $SOUND_CARD index.
JACK_NO_AUDIO_RESERVATION=1 jackd -t 2000 -P 75 -m -s -d alsa -d $SOUND_CARD -r $SAMPLE_RATE -p 1024 -n 10 &

echo "Waiting for JACK server to be ready..."
for i in $(seq 1 15); do
jack_wait -w -t 1 && break
if [ $i -eq 15 ]; then
echo "ERROR: JACK failed to start after 15s."
exit 1
fi
sleep 1
done
echo "JACK is ready."

# --- 6. FFMPEG STREAMING ---
FFMPEG_PID=""

if [ "$NODE_TYPE" = "research" ]; then
nice -n -10 ffmpeg -f jack -i ffjack \
-f segment -segment_time "00:00:$FLAC_DURATION.00" -strftime 1 "/tmp/$NODE_NAME/flac/%Y-%m-%d_%H-%M-%S_$NODE_NAME-$SAMPLE_RATE-$CHANNELS.flac" \
-f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format \
mpegts -ar $STREAM_RATE -ac 2 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
elif [ "$NODE_TYPE" = "hls-only" ]; then
nice -n -10 ffmpeg -f jack -i ffjack -f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format mpegts -ar $STREAM_RATE -ac $CHANNELS -threads 3 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
else
echo "Unsupported NODE_TYPE. Please use research or hls-only."
exit 1
fi

# --- 7. CONNECT JACK PORTS ---
echo "Waiting for ffjack ports..."
for i in $(seq 1 15); do
jack_lsp | grep -q "ffjack:input_1" && break
sleep 1
done

if ! jack_lsp | grep -q "ffjack:input_1"; then
echo "ERROR: ffjack ports never appeared."
cat /tmp/$NODE_NAME/ffmpeg.log
exit 1
fi

jack_connect -s default system:capture_1 ffjack:input_1
jack_connect -s default system:capture_2 ffjack:input_2

if [ "$NODE_LOOPBACK" = "true" ]; then
jack_connect system:capture_1 system:playback_1
jack_connect system:capture_2 system:playback_2
fi

# Launch Python uploaders
if [ "${NO_UPLOAD:-false}" = "true" ]; then
echo "NO_UPLOAD=true, skipping S3 upload. Segments will accumulate in /tmp/$NODE_NAME/hls/"
wait $FFMPEG_PID
elif [ "$NODE_TYPE" = "research" ]; then
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py &
python3 upload_flac_s3.py
else
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py
fi

echo "All processes started successfully." No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "stream_sync.sh" -type f 2>/dev/null | head -20

Repository: orcasound/orcanode

Length of output: 89


🏁 Script executed:

cat -n ./node_2026/stream_sync.sh

Repository: orcasound/orcanode

Length of output: 6361


Add signal trap and JACK process tracking to enable clean shutdown in container lifecycle.

The script launches jackd and ffmpeg as background jobs but lacks signal handlers and process tracking. This causes orphaned processes during container stop/restart, leading to restart failures and resource leaks. JACK is started at line 93 without saving its PID, preventing cleanup.

Key fixes:

  1. Save jackd PID immediately after launch
  2. Add trap on SIGTERM/SIGINT/EXIT to kill tracked processes
  3. Ensure all background jobs are properly terminated on exit
Suggested approach

Add at the top after shebang:

 #!/bin/bash
 # Script for live DASH/HLS streaming lossy audio as AAC and/or archiving lossless audio as FLAC
+
+# Process cleanup on exit/signal
+FFMPEG_PID=""
+JACK_PID=""
+cleanup() {
+    local code=$?
+    [ -n "${FFMPEG_PID}" ] && kill -TERM "$FFMPEG_PID" 2>/dev/null || true
+    [ -n "${JACK_PID}" ] && kill -TERM "$JACK_PID" 2>/dev/null || true
+    exit $code
+}
+trap cleanup SIGINT SIGTERM EXIT

Then track JACK_PID at line 93:

 JACK_NO_AUDIO_RESERVATION=1 jackd -t 2000 -P 75 -m -s -d alsa -d $SOUND_CARD -r $SAMPLE_RATE -p 1024 -n 10 &
+JACK_PID=$!

And move FFMPEG_PID initialization from line 107 into the conditional blocks where it's set.

Note: Avoid kill 0 (kills entire process group); instead kill only tracked processes explicitly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#!/bin/bash
# Script for live DASH/HLS streaming lossy audio as AAC and/or archiving lossless audio as FLAC
# Ensure system binaries are in PATH
export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/bin:$PATH
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- 0. PREFLIGHT CHECKS ---
# The .env sourcing is commented out because Docker Compose handles variable injection.
# if [ -f "$SCRIPT_DIR/.env" ]; then
# set -a
# source "$SCRIPT_DIR/.env"
# set +a
# echo "Loaded .env from $SCRIPT_DIR"
# fi
if ! command -v jackd &> /dev/null; then
echo "ERROR: jackd is not installed."
exit 1
fi
if ! command -v jack_wait &> /dev/null; then
echo "ERROR: jack_wait is not installed."
exit 1
fi
if ! command -v ffmpeg &> /dev/null; then
echo "ERROR: ffmpeg is not installed."
exit 1
fi
# --- 1. TIME SYNCHRONIZATION ---
wait_for_sync() {
echo "Checking for sane system time..."
local max_wait=60
local elapsed=0
# Check if the year is 2025 or later
while [ $(date +%Y) -lt 2025 ]; do
if [ $elapsed -ge $max_wait ]; then
echo "ERROR: Time sync timed out, aborting."
exit 1
fi
echo "Waiting for time sync (current year: $(date +%Y))..."
sleep 2
elapsed=$((elapsed + 2))
done
echo "Time looks sane: $(date)"
}
wait_for_sync
# --- 2. ACTIVATE VIRTUAL ENVIRONMENT ---
# Updated to use the absolute path within the container.
source /venv/bin/activate
# --- 3. DYNAMIC AUDIO DEVICE DISCOVERY ---
echo "Searching for ALSA device: $AUDIO_HW_ID..."
# Loop to find the hardware index (e.g., card 3) and set the correct hw address.
for i in $(seq 1 30); do
if aplay -l 2>/dev/null | grep -qi "$AUDIO_HW_ID"; then
# Extracts the number from a line like "card 3: pisound [pisound]".
CARD_NUM=$(aplay -l | grep -i "$AUDIO_HW_ID" | head -n 1 | awk -F'[: ]+' '{print $2}')
if [ -n "$CARD_NUM" ]; then
SOUND_CARD="hw:$CARD_NUM,0"
echo "Success! $AUDIO_HW_ID found at index $CARD_NUM. Using address: $SOUND_CARD"
break
fi
fi
echo "Audio device $AUDIO_HW_ID not ready yet, attempt $i..."
sleep 1
done
if [ -z "$SOUND_CARD" ]; then
echo "ERROR: ALSA device $AUDIO_HW_ID not found after 30s, aborting."
exit 1
fi
# --- 4. VARIABLES & DIRECTORIES ---
timestamp=$(date +%s)
echo "Node started at $timestamp. Node name: $NODE_NAME. Sound card address: $SOUND_CARD"
mkdir -p /tmp/$NODE_NAME/flac
mkdir -p /tmp/$NODE_NAME/hls/$timestamp
echo $timestamp > /tmp/$NODE_NAME/latest.txt
STREAM_RATE=48000
SAMPLE_RATE=${SAMPLE_RATE:-48000}
# --- 5. SETUP JACK ---
# Start jackd using the discovered $SOUND_CARD index.
JACK_NO_AUDIO_RESERVATION=1 jackd -t 2000 -P 75 -m -s -d alsa -d $SOUND_CARD -r $SAMPLE_RATE -p 1024 -n 10 &
echo "Waiting for JACK server to be ready..."
for i in $(seq 1 15); do
jack_wait -w -t 1 && break
if [ $i -eq 15 ]; then
echo "ERROR: JACK failed to start after 15s."
exit 1
fi
sleep 1
done
echo "JACK is ready."
# --- 6. FFMPEG STREAMING ---
FFMPEG_PID=""
if [ "$NODE_TYPE" = "research" ]; then
nice -n -10 ffmpeg -f jack -i ffjack \
-f segment -segment_time "00:00:$FLAC_DURATION.00" -strftime 1 "/tmp/$NODE_NAME/flac/%Y-%m-%d_%H-%M-%S_$NODE_NAME-$SAMPLE_RATE-$CHANNELS.flac" \
-f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format \
mpegts -ar $STREAM_RATE -ac 2 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
elif [ "$NODE_TYPE" = "hls-only" ]; then
nice -n -10 ffmpeg -f jack -i ffjack -f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format mpegts -ar $STREAM_RATE -ac $CHANNELS -threads 3 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
else
echo "Unsupported NODE_TYPE. Please use research or hls-only."
exit 1
fi
# --- 7. CONNECT JACK PORTS ---
echo "Waiting for ffjack ports..."
for i in $(seq 1 15); do
jack_lsp | grep -q "ffjack:input_1" && break
sleep 1
done
if ! jack_lsp | grep -q "ffjack:input_1"; then
echo "ERROR: ffjack ports never appeared."
cat /tmp/$NODE_NAME/ffmpeg.log
exit 1
fi
jack_connect -s default system:capture_1 ffjack:input_1
jack_connect -s default system:capture_2 ffjack:input_2
if [ "$NODE_LOOPBACK" = "true" ]; then
jack_connect system:capture_1 system:playback_1
jack_connect system:capture_2 system:playback_2
fi
# Launch Python uploaders
if [ "${NO_UPLOAD:-false}" = "true" ]; then
echo "NO_UPLOAD=true, skipping S3 upload. Segments will accumulate in /tmp/$NODE_NAME/hls/"
wait $FFMPEG_PID
elif [ "$NODE_TYPE" = "research" ]; then
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py &
python3 upload_flac_s3.py
else
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py
fi
echo "All processes started successfully."
#!/bin/bash
# Script for live DASH/HLS streaming lossy audio as AAC and/or archiving lossless audio as FLAC
# Process cleanup on exit/signal
FFMPEG_PID=""
JACK_PID=""
cleanup() {
local code=$?
[ -n "${FFMPEG_PID}" ] && kill -TERM "$FFMPEG_PID" 2>/dev/null || true
[ -n "${JACK_PID}" ] && kill -TERM "$JACK_PID" 2>/dev/null || true
exit $code
}
trap cleanup SIGINT SIGTERM EXIT
# Ensure system binaries are in PATH
export PATH=/usr/bin:/usr/local/bin:/usr/sbin:/bin:$PATH
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- 0. PREFLIGHT CHECKS ---
# The .env sourcing is commented out because Docker Compose handles variable injection.
# if [ -f "$SCRIPT_DIR/.env" ]; then
# set -a
# source "$SCRIPT_DIR/.env"
# set +a
# echo "Loaded .env from $SCRIPT_DIR"
# fi
if ! command -v jackd &> /dev/null; then
echo "ERROR: jackd is not installed."
exit 1
fi
if ! command -v jack_wait &> /dev/null; then
echo "ERROR: jack_wait is not installed."
exit 1
fi
if ! command -v ffmpeg &> /dev/null; then
echo "ERROR: ffmpeg is not installed."
exit 1
fi
# --- 1. TIME SYNCHRONIZATION ---
wait_for_sync() {
echo "Checking for sane system time..."
local max_wait=60
local elapsed=0
# Check if the year is 2025 or later
while [ $(date +%Y) -lt 2025 ]; do
if [ $elapsed -ge $max_wait ]; then
echo "ERROR: Time sync timed out, aborting."
exit 1
fi
echo "Waiting for time sync (current year: $(date +%Y))..."
sleep 2
elapsed=$((elapsed + 2))
done
echo "Time looks sane: $(date)"
}
wait_for_sync
# --- 2. ACTIVATE VIRTUAL ENVIRONMENT ---
# Updated to use the absolute path within the container.
source /venv/bin/activate
# --- 3. DYNAMIC AUDIO DEVICE DISCOVERY ---
echo "Searching for ALSA device: $AUDIO_HW_ID..."
# Loop to find the hardware index (e.g., card 3) and set the correct hw address.
for i in $(seq 1 30); do
if aplay -l 2>/dev/null | grep -qi "$AUDIO_HW_ID"; then
# Extracts the number from a line like "card 3: pisound [pisound]".
CARD_NUM=$(aplay -l | grep -i "$AUDIO_HW_ID" | head -n 1 | awk -F'[: ]+' '{print $2}')
if [ -n "$CARD_NUM" ]; then
SOUND_CARD="hw:$CARD_NUM,0"
echo "Success! $AUDIO_HW_ID found at index $CARD_NUM. Using address: $SOUND_CARD"
break
fi
fi
echo "Audio device $AUDIO_HW_ID not ready yet, attempt $i..."
sleep 1
done
if [ -z "$SOUND_CARD" ]; then
echo "ERROR: ALSA device $AUDIO_HW_ID not found after 30s, aborting."
exit 1
fi
# --- 4. VARIABLES & DIRECTORIES ---
timestamp=$(date +%s)
echo "Node started at $timestamp. Node name: $NODE_NAME. Sound card address: $SOUND_CARD"
mkdir -p /tmp/$NODE_NAME/flac
mkdir -p /tmp/$NODE_NAME/hls/$timestamp
echo $timestamp > /tmp/$NODE_NAME/latest.txt
STREAM_RATE=48000
SAMPLE_RATE=${SAMPLE_RATE:-48000}
# --- 5. SETUP JACK ---
# Start jackd using the discovered $SOUND_CARD index.
JACK_NO_AUDIO_RESERVATION=1 jackd -t 2000 -P 75 -m -s -d alsa -d $SOUND_CARD -r $SAMPLE_RATE -p 1024 -n 10 &
JACK_PID=$!
echo "Waiting for JACK server to be ready..."
for i in $(seq 1 15); do
jack_wait -w -t 1 && break
if [ $i -eq 15 ]; then
echo "ERROR: JACK failed to start after 15s."
exit 1
fi
sleep 1
done
echo "JACK is ready."
# --- 6. FFMPEG STREAMING ---
FFMPEG_PID=""
if [ "$NODE_TYPE" = "research" ]; then
nice -n -10 ffmpeg -f jack -i ffjack \
-f segment -segment_time "00:00:$FLAC_DURATION.00" -strftime 1 "/tmp/$NODE_NAME/flac/%Y-%m-%d_%H-%M-%S_$NODE_NAME-$SAMPLE_RATE-$CHANNELS.flac" \
-f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format \
mpegts -ar $STREAM_RATE -ac 2 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
elif [ "$NODE_TYPE" = "hls-only" ]; then
nice -n -10 ffmpeg -f jack -i ffjack -f segment -segment_list "/tmp/$NODE_NAME/hls/$timestamp/live.m3u8" -segment_list_flags +live -segment_list_size 5 -segment_time $SEGMENT_DURATION -segment_format mpegts -ar $STREAM_RATE -ac $CHANNELS -threads 3 -acodec aac "/tmp/$NODE_NAME/hls/$timestamp/live%03d.ts" \
>/tmp/$NODE_NAME/ffmpeg.log 2>&1 &
FFMPEG_PID=$!
else
echo "Unsupported NODE_TYPE. Please use research or hls-only."
exit 1
fi
# --- 7. CONNECT JACK PORTS ---
echo "Waiting for ffjack ports..."
for i in $(seq 1 15); do
jack_lsp | grep -q "ffjack:input_1" && break
sleep 1
done
if ! jack_lsp | grep -q "ffjack:input_1"; then
echo "ERROR: ffjack ports never appeared."
cat /tmp/$NODE_NAME/ffmpeg.log
exit 1
fi
jack_connect -s default system:capture_1 ffjack:input_1
jack_connect -s default system:capture_2 ffjack:input_2
if [ "$NODE_LOOPBACK" = "true" ]; then
jack_connect system:capture_1 system:playback_1
jack_connect system:capture_2 system:playback_2
fi
# Launch Python uploaders
if [ "${NO_UPLOAD:-false}" = "true" ]; then
echo "NO_UPLOAD=true, skipping S3 upload. Segments will accumulate in /tmp/$NODE_NAME/hls/"
wait $FFMPEG_PID
elif [ "$NODE_TYPE" = "research" ]; then
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py &
python3 upload_flac_s3.py
else
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py
fi
echo "All processes started successfully."
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 7-7: SCRIPT_DIR appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 38-38: Quote this to prevent word splitting.

(SC2046)


[info] 54-54: Not following: /venv/bin/activate was not specified as input (see shellcheck -x).

(SC1091)


[info] 84-84: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 85-85: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 85-85: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 86-86: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 86-86: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 93-93: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 93-93: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 98-98: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 112-112: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 114-114: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 117-117: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 117-117: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 118-118: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 134-134: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/stream_sync.sh` around lines 1 - 159, Save the jackd PID
immediately after launching it (capture PID into JACK_PID right after the jackd
start line) and stop relying on implicit background jobs; move FFMPEG_PID
assignment into each NODE_TYPE branch where ffmpeg is started so it's always
set, then add a trap for SIGTERM, SIGINT, and EXIT that explicitly kills and
waits for JACK_PID, FFMPEG_PID and any background python uploader PIDs (e.g.,
catchup_s3, upload_s3, upload_flac_s3) using kill -TERM <PID> and wait <PID>
(avoid kill 0); ensure the trap runs before launching background uploaders and
that all spawned PIDs are recorded so the trap can terminate them cleanly.

Comment thread node_2026/upload_s3.py
Comment on lines +28 to +39
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if(os.environ["BUCKET_TYPE"] == "prod"):
print("using production bucket")
BUCKET = 'audio-orcasound-net'
elif (os.environ["BUCKET_TYPE"] == "custom"):
print("using custom bucket")
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"

log.debug("hls bucket set to "+BUCKET)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Empty BUCKET when BUCKET_TYPE is unset causes unclear S3 failures.

If BUCKET_TYPE is not in the environment, BUCKET remains an empty string, and boto3 uploads will fail with a confusing error. Consider failing fast with a clear message.

Proposed fix
 BUCKET = ""
 if "BUCKET_TYPE" in os.environ:
     if(os.environ["BUCKET_TYPE"] == "prod"):
         print("using production bucket")
         BUCKET = 'audio-orcasound-net'
     elif (os.environ["BUCKET_TYPE"] == "custom"):
         print("using custom bucket")
         BUCKET = os.environ["BUCKET_STREAMING"]
     else:
         BUCKET = "dev-streaming-orcasound-net"
 
     log.debug("hls bucket set to "+BUCKET)
+else:
+    log.critical("BUCKET_TYPE not set; exiting")
+    sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if(os.environ["BUCKET_TYPE"] == "prod"):
print("using production bucket")
BUCKET = 'audio-orcasound-net'
elif (os.environ["BUCKET_TYPE"] == "custom"):
print("using custom bucket")
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"
log.debug("hls bucket set to "+BUCKET)
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if(os.environ["BUCKET_TYPE"] == "prod"):
print("using production bucket")
BUCKET = 'audio-orcasound-net'
elif (os.environ["BUCKET_TYPE"] == "custom"):
print("using custom bucket")
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"
log.debug("hls bucket set to "+BUCKET)
else:
log.critical("BUCKET_TYPE not set; exiting")
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/upload_s3.py` around lines 28 - 39, The BUCKET variable can remain
empty when BUCKET_TYPE is unset causing obscure S3 errors; update upload_s3.py
to validate BUCKET_TYPE/BUCKET after the existing conditional (or when declaring
BUCKET) and fail fast with a clear error message (raise an exception or exit) if
BUCKET_TYPE is missing or BUCKET is still empty; reference the BUCKET and
BUCKET_TYPE environment checks and the BUCKET variable so the change is applied
right after that logic so any boto3 upload code using BUCKET sees a clear, early
failure.

Comment thread node_2026/upload_s3.py
Comment on lines +116 to +118
except:
e = sys.exc_info()[0]
log.critical('error uploading to S3: '+str(e))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bare except: catches KeyboardInterrupt and SystemExit, masking shutdown signals.

The bare except clause captures all exceptions including system signals. This prevents clean container shutdown and hides unexpected errors. Catch a specific exception type instead.

Proposed fix
-    except:
-        e = sys.exc_info()[0]
-        log.critical('error uploading to S3: '+str(e))
+    except Exception as e:
+        log.critical(f'error uploading to S3: {e}')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except:
e = sys.exc_info()[0]
log.critical('error uploading to S3: '+str(e))
except Exception as e:
log.critical(f'error uploading to S3: {e}')
🧰 Tools
🪛 Ruff (0.15.13)

[error] 116-116: Do not use bare except

(E722)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/upload_s3.py` around lines 116 - 118, Replace the bare "except:" in
upload_s3.py with a safe exception handler: catch "Exception as e" instead of
all exceptions so KeyboardInterrupt/SystemExit are not swallowed, and log the
full traceback (for example via log.exception or traceback.format_exc()) when
reporting "error uploading to S3". Target the existing except block that
currently does "except:" / "e = sys.exc_info()[0]" / "log.critical(...)" and
change it to use "except Exception as e" and emit the error details.

Comment thread node_2026/upload_s3.py
Comment on lines +129 to +135
if type_names[0] == 'IN_CLOSE_WRITE':
if 'tmp' not in filename:
log.debug('Recieved a new file ' + filename)
s3_copy_file(path, filename)
if type_names[0] == 'IN_MOVED_TO':
log.debug('Recieved a new file ' + filename)
s3_copy_file(path, filename)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

IN_MOVED_TO handler lacks the 'tmp' not in filename guard.

The IN_CLOSE_WRITE branch (line 130) excludes temporary files, but IN_MOVED_TO (lines 133-135) processes any moved file. If ffmpeg moves a temp file, it will be uploaded. Apply the same filter for consistency.

Proposed fix
             if type_names[0] == 'IN_CLOSE_WRITE':
                 if 'tmp' not in filename:
-                    log.debug('Recieved a new file ' + filename)
+                    log.debug('Received a new file ' + filename)
                     s3_copy_file(path, filename)
             if type_names[0] == 'IN_MOVED_TO':
-                    log.debug('Recieved a new file ' + filename)
+                if 'tmp' not in filename:
+                    log.debug('Received a new file ' + filename)
                     s3_copy_file(path, filename)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if type_names[0] == 'IN_CLOSE_WRITE':
if 'tmp' not in filename:
log.debug('Recieved a new file ' + filename)
s3_copy_file(path, filename)
if type_names[0] == 'IN_MOVED_TO':
log.debug('Recieved a new file ' + filename)
s3_copy_file(path, filename)
if type_names[0] == 'IN_CLOSE_WRITE':
if 'tmp' not in filename:
log.debug('Received a new file ' + filename)
s3_copy_file(path, filename)
if type_names[0] == 'IN_MOVED_TO':
if 'tmp' not in filename:
log.debug('Received a new file ' + filename)
s3_copy_file(path, filename)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/upload_s3.py` around lines 129 - 135, The IN_MOVED_TO branch
currently uploads every moved file while the IN_CLOSE_WRITE branch skips temp
files; update the IN_MOVED_TO handler to apply the same guard by checking that
'tmp' not in filename before calling s3_copy_file(path, filename). Locate the
block that inspects type_names[0] == 'IN_MOVED_TO' and add the same conditional
used for the IN_CLOSE_WRITE branch (the 'tmp' check and log.debug call) so only
non-temporary files get passed to s3_copy_file.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new node_2026/ implementation of the Raspberry Pi 4 + Pisound hydrophone streaming node that runs the capture/segment/upload pipeline inside Docker, including an outage catch-up uploader and setup/ops documentation.

Changes:

  • Introduces a Debian-based Docker image + Compose service to run JACK + ffmpeg + Python uploaders in a container.
  • Adds real-time HLS segment uploading with per-segment RMS monitoring (upload_s3.py).
  • Adds “catch-up” logic to backfill stranded segments after connectivity returns (catchup_s3.py), plus host setup and runbook docs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
node_2026/upload_s3.py New inotify-based uploader for HLS artifacts with RMS logging and latest.txt publishing.
node_2026/catchup_s3.py New catch-up uploader that scans for stranded segments, generates a VOD manifest, and uploads at low priority.
node_2026/stream_sync.sh Container entrypoint to time-check, discover ALSA device, start JACK/ffmpeg, and launch uploaders.
node_2026/docker-compose.yml Compose service definition (privileged audio access, shm_size, logging limits, env injection).
node_2026/Dockerfile Debian Trixie image installing JACK/ffmpeg and Python deps for the node pipeline.
node_2026/setup.sh One-time host provisioning script (Docker install, IPv6 workaround, group permissions).
node_2026/README.txt Operational documentation for provisioning, running, and troubleshooting the containerized node.
node_2026/.env.template Configuration template for node identity, audio parameters, and AWS credentials.
node_2026/.gitignore Prevents committing local env/log/venv artifacts for this node implementation.
node_2026/.dockerignore Prevents baking env/log artifacts into the Docker build context.
Comments suppressed due to low confidence (3)

node_2026/upload_s3.py:139

  • The latest.txt publish is triggered purely by seeing a .ts event, not by confirming that the first segment upload succeeded. If S3 is unreachable at startup, this can publish latest.txt pointing clients at a timestamp directory with no uploaded segments. Consider having s3_copy_file return success/failure and only uploading latest.txt (and flipping latest_txt_uploaded) after a successful .ts upload.
            if not latest_txt_uploaded and filename.endswith('.ts'):
                log.debug('First segment uploaded — now publishing latest.txt')
                s3_copy_file(BASEPATH, 'latest.txt')
                latest_txt_uploaded = True

node_2026/upload_s3.py:105

  • boto3.resource('s3') is created for every segment upload. Since this runs per-file, consider creating the S3 client/resource once at module init and reusing it to reduce per-segment overhead and connection churn.
    try:
        resource = boto3.resource('s3')
        uploadpath = os.path.relpath(path, "/tmp")
        uploadkey = os.path.join(uploadpath, filename)
        log.debug('upload key: ' + uploadkey)
        try:
            resource.meta.client.upload_file(uploadfile, BUCKET, uploadkey)

node_2026/catchup_s3.py:87

  • enforce_disk_guard() sorts with os.path.getmtime(p) without handling FileNotFoundError. Since segments can be concurrently deleted/rotated, this can crash the catch-up loop. Consider filtering out missing paths before sorting or wrapping getmtime in a safe helper.
def enforce_disk_guard(stranded):
    """Delete oldest stranded segments if total exceeds MAX_STRANDED_BYTES."""
    # Sort oldest first (by mtime)
    aged = sorted(stranded, key=lambda p: os.path.getmtime(p))
    while total_size(aged) > MAX_STRANDED_BYTES and aged:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread node_2026/upload_s3.py
Comment on lines +28 to +39
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if(os.environ["BUCKET_TYPE"] == "prod"):
print("using production bucket")
BUCKET = 'audio-orcasound-net'
elif (os.environ["BUCKET_TYPE"] == "custom"):
print("using custom bucket")
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"

log.debug("hls bucket set to "+BUCKET)
Comment thread node_2026/upload_s3.py
Comment on lines +131 to +134
log.debug('Recieved a new file ' + filename)
s3_copy_file(path, filename)
if type_names[0] == 'IN_MOVED_TO':
log.debug('Recieved a new file ' + filename)
Comment thread node_2026/upload_s3.py
Comment on lines +4 to +7
from boto3.s3.transfer import S3Transfer
import inotify.adapters
import logging
import logging.handlers
Comment thread node_2026/stream_sync.sh
Comment on lines +151 to +156
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py &
python3 upload_flac_s3.py
else
nice -n 10 python3 catchup_s3.py &
python3 upload_s3.py
Comment thread node_2026/setup.sh
# Detect the actual user (works whether run as root or with sudo)
REAL_USER=${SUDO_USER:-$USER}
REAL_HOME=$(eval echo "~$REAL_USER")
PROJECT_DIR="$REAL_HOME/orcanode/node_val_docker"
Comment thread node_2026/README.txt

sudo apt-get install -y git
git clone https://github.com/orcasound/orcanode.git ~/orcanode
cd ~/orcanode/node_val_docker
Comment on lines +16 to +19
ports:
- "1234:1234"
- "8080:8080"

Comment thread node_2026/catchup_s3.py
Comment on lines +28 to +36
BUCKET = ""
if "BUCKET_TYPE" in os.environ:
if os.environ["BUCKET_TYPE"] == "prod":
BUCKET = "audio-orcasound-net"
elif os.environ["BUCKET_TYPE"] == "custom":
BUCKET = os.environ["BUCKET_STREAMING"]
else:
BUCKET = "dev-streaming-orcasound-net"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@node_2026/.env_template`:
- Line 17: The .env_template currently hardcodes a LogDNA ingestion key into the
SYSLOG_STRUCTURED_DATA value, which exposes a secret; replace the real-looking
key with a neutral placeholder (e.g., KEY_HERE or <LOGDNA_KEY>) in the
SYSLOG_STRUCTURED_DATA entry and update documentation to instruct developers to
set the real ingestion key in their untracked .env file or environment
variables; specifically edit the SYSLOG_STRUCTURED_DATA line to remove the
embedded secret and leave a clear placeholder so the real key is never checked
into source control.

In `@node_2026/README.txt`:
- Line 158: The README currently hardcodes the production bucket in verification
commands (e.g., "aws s3 ls s3://audio-orcasound-net/<NODE_NAME>/hls/
--human-readable"); update those commands to use the BUCKET_TYPE/BUCKET_NAME
mechanism instead (e.g., reference the resolved bucket variable or $BUCKET_NAME
built from BUCKET_TYPE=prod|dev|custom) so dev/custom deployments work; replace
every hardcoded "audio-orcasound-net" occurrence (notably the shown command and
the other occurrences mentioned) with the dynamic bucket variable and add a
short note/example showing how BUCKET_NAME is derived from BUCKET_TYPE.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a31f0e53-809f-4bfc-b68c-6ddcb21e268c

📥 Commits

Reviewing files that changed from the base of the PR and between 860bebd and cf32945.

📒 Files selected for processing (4)
  • node_2026/.env_template
  • node_2026/.gitignore
  • node_2026/README.txt
  • node_2026/stream_sync.sh
✅ Files skipped from review due to trivial changes (1)
  • node_2026/.gitignore

Comment thread node_2026/.env_template
AUDIO_HW_ID=pisound
CHANNELS=2
SYSLOG_URL=syslog://syslog-a.logdna.com:37043
SYSLOG_STRUCTURED_DATA='logdna@48950 key="313dbd82f35ccbe462e6e3483984f464" tag="docker"'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the hardcoded LogDNA ingestion key from the template.

This embeds a real-looking secret in a tracked file. Keep only a placeholder and inject the real value via an untracked .env.

Suggested fix
-SYSLOG_STRUCTURED_DATA='logdna@48950 key="313dbd82f35ccbe462e6e3483984f464" tag="docker"'
+SYSLOG_STRUCTURED_DATA='logdna@48950 key="REPLACE_WITH_LOGDNA_INGESTION_KEY" tag="docker"'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SYSLOG_STRUCTURED_DATA='logdna@48950 key="313dbd82f35ccbe462e6e3483984f464" tag="docker"'
SYSLOG_STRUCTURED_DATA='logdna@48950 key="REPLACE_WITH_LOGDNA_INGESTION_KEY" tag="docker"'
🧰 Tools
🪛 Betterleaks (1.2.0)

[high] 17-17: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/.env_template` at line 17, The .env_template currently hardcodes a
LogDNA ingestion key into the SYSLOG_STRUCTURED_DATA value, which exposes a
secret; replace the real-looking key with a neutral placeholder (e.g., KEY_HERE
or <LOGDNA_KEY>) in the SYSLOG_STRUCTURED_DATA entry and update documentation to
instruct developers to set the real ingestion key in their untracked .env file
or environment variables; specifically edit the SYSLOG_STRUCTURED_DATA line to
remove the embedded secret and leave a clear placeholder so the real key is
never checked into source control.

Comment thread node_2026/README.txt

If NO_UPLOAD=false, verify segments are reaching S3:

aws s3 ls s3://audio-orcasound-net/<NODE_NAME>/hls/ --human-readable

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid hardcoding the prod bucket in verification commands.

The README supports BUCKET_TYPE=prod|dev|custom, but these commands always point to audio-orcasound-net, which is incorrect for dev/custom deployments.

Suggested doc fix
-  aws s3 ls s3://audio-orcasound-net/<NODE_NAME>/hls/ --human-readable
+  aws s3 ls s3://<BUCKET_NAME>/<NODE_NAME>/hls/ --human-readable
...
-  s3://audio-orcasound-net/<NODE_NAME>/hls/<timestamp>/live000.ts
+  s3://<BUCKET_NAME>/<NODE_NAME>/hls/<timestamp>/live000.ts
...
-  aws s3 cp s3://audio-orcasound-net/<NODE_NAME>/hls/<timestamp>/live.m3u8 -
+  aws s3 cp s3://<BUCKET_NAME>/<NODE_NAME>/hls/<timestamp>/live.m3u8 -

Also applies to: 161-161, 166-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/README.txt` at line 158, The README currently hardcodes the
production bucket in verification commands (e.g., "aws s3 ls
s3://audio-orcasound-net/<NODE_NAME>/hls/ --human-readable"); update those
commands to use the BUCKET_TYPE/BUCKET_NAME mechanism instead (e.g., reference
the resolved bucket variable or $BUCKET_NAME built from
BUCKET_TYPE=prod|dev|custom) so dev/custom deployments work; replace every
hardcoded "audio-orcasound-net" occurrence (notably the shown command and the
other occurrences mentioned) with the dynamic bucket variable and add a short
note/example showing how BUCKET_NAME is derived from BUCKET_TYPE.

Updated AWS credentials placeholders in .env_template

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
node_2026/.env_template (1)

6-9: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add BUCKET_STREAMING placeholder for custom bucket configuration.

When BUCKET_TYPE=custom, upload_s3.py (line 33) expects BUCKET_STREAMING to be defined. This template lacks that variable, which would cause a KeyError at runtime if someone uncomments or adds BUCKET_TYPE=custom.

📝 Suggested addition
 BUCKET_TYPE=prod
+#BUCKET_STREAMING=your-custom-bucket-name
 `#NODE_TYPE`=research

As per context snippet 1 from upload_s3.py:14-40, the uploader code branches on BUCKET_TYPE and reads BUCKET_STREAMING when the type is "custom".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/.env_template` around lines 6 - 9, Add a BUCKET_STREAMING
placeholder to the environment template so code that expects it when
BUCKET_TYPE=custom won't KeyError; update the .env_template to include a
commented example like BUCKET_STREAMING=your-streaming-bucket (alongside the
existing BUCKET_TYPE and NODE_TYPE entries) so upload_s3.py's branch that reads
BUCKET_STREAMING has a clear default/example to follow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@node_2026/.env_template`:
- Around line 6-9: Add a BUCKET_STREAMING placeholder to the environment
template so code that expects it when BUCKET_TYPE=custom won't KeyError; update
the .env_template to include a commented example like
BUCKET_STREAMING=your-streaming-bucket (alongside the existing BUCKET_TYPE and
NODE_TYPE entries) so upload_s3.py's branch that reads BUCKET_STREAMING has a
clear default/example to follow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f417c29e-8982-4438-a94f-0eede03ccfbf

📥 Commits

Reviewing files that changed from the base of the PR and between cf32945 and 54b6fc5.

📒 Files selected for processing (1)
  • node_2026/.env_template

veirs and others added 2 commits May 27, 2026 21:11
Switch hls_list_size from 5 to 0 so live.m3u8 grows to include every
segment written since the last container start. Add a midnight crontab
in setup.sh to restart the container daily, giving each calendar day
its own S3 timestamp directory with a complete manifest. This makes all
segments discoverable by the orcasite server for spectrogram generation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When hls_list_size was 5, live.m3u8 only retained the last 5 segments
of each session, leaving earlier segments undiscoverable by the orcasite
server. repair_manifests.py scans all S3 timestamp directories, compares
.ts file count against manifest entries, and reconstructs a complete
live.m3u8 with approximate EXT-X-PROGRAM-DATE-TIME tags derived by
anchoring on the exact timestamps still present in the tail of the old
manifest. README_REPAIR.md documents the algorithm and usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@node_2026/README_REPAIR.md`:
- Line 7: Add explicit language identifiers to the fenced code blocks in
README_REPAIR.md to satisfy markdownlint MD040: change the fence before the
"s3://audio-orcasound-net/rpi_orcasound_lab/hls/1748383745/" snippet to ```text,
the fence before the "time(N) ≈ anchor_time + (N − anchor_seq) × avg_duration"
equation to ```text, the fence for the "python3 repair_manifests.py [--node
NODE]..." command to ```bash, and the fence for the "Scanning
s3://audio-orcasound-net/rpi_orcasound_lab/hls/  (dry_run=False)..." output to
```text (apply the same pattern to the other occurrences noted at the end of the
review).

In `@node_2026/repair_manifests.py`:
- Around line 293-317: Filter out non-timestamp prefixes (where
_dir_unix(prefix) == 0) immediately after calling list_timestamp_dirs and before
computing most_recent, reversing, and applying stop-at logic; update the code
around vars dirs, most_recent and the stop_at check so you discard entries with
_dir_unix(...) == 0 (or otherwise non-numeric) and then compute most_recent =
dirs[-1] if dirs else None, reverse dirs, and run the stop-at comparison using
the numeric _dir_unix(prefix) value to avoid non-timestamp entries becoming
most_recent or prematurely triggering the stop-at break; keep references to
list_timestamp_dirs, _dir_unix, most_recent, dirs, stop_at, and process_dir.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbfd3456-8124-4a5b-8d31-e7be632e61d6

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0c72 and b3cdcfd.

📒 Files selected for processing (2)
  • node_2026/README_REPAIR.md
  • node_2026/repair_manifests.py


Each recording session creates a timestamp directory on S3 such as:

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks to satisfy markdownlint.

These fences currently trigger MD040 and may fail markdown linting in CI.

💡 Suggested patch
-```
+```text
 s3://audio-orcasound-net/rpi_orcasound_lab/hls/1748383745/

@@

  • time(N) ≈ anchor_time + (N − anchor_seq) × avg_duration
    

@@
- +bash
python3 repair_manifests.py [--node NODE] [--bucket BUCKET]
[--days N] [--stop-at TIMESTAMP]
[--dry-run]

@@
-```
+```text
Scanning s3://audio-orcasound-net/rpi_orcasound_lab/hls/  (dry_run=False)
Found 47 directories to examine
...
Done. fixed=44  ok=2  skip=1  error=0
</details>


Also applies to: 48-48, 81-81, 113-113

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @node_2026/README_REPAIR.md at line 7, Add explicit language identifiers to
the fenced code blocks in README_REPAIR.md to satisfy markdownlint MD040: change
the fence before the
"s3://audio-orcasound-net/rpi_orcasound_lab/hls/1748383745/" snippet to text, the fence before the "time(N) ≈ anchor_time + (N − anchor_seq) × avg_duration" equation to text, the fence for the "python3 repair_manifests.py [--node
NODE]..." command to ```bash, and the fence for the "Scanning
s3://audio-orcasound-net/rpi_orcasound_lab/hls/ (dry_run=False)..." output to

review).

Comment on lines +293 to +317
dirs = list_timestamp_dirs(s3, args.bucket, args.node)
if not dirs:
print("No timestamp directories found.")
sys.exit(0)

# Optionally limit to recent N days
if args.days > 0:
cutoff = datetime.now(tz=timezone.utc) - timedelta(days=args.days)
cutoff_unix = int(cutoff.timestamp())
dirs = [d for d in dirs
if _dir_unix(d) >= cutoff_unix]

print(f"Found {len(dirs)} director{'y' if len(dirs)==1 else 'ies'} to examine\n")

most_recent = dirs[-1] if dirs else None
dirs = list(reversed(dirs))
counts = {'fixed': 0, 'ok': 0, 'skip': 0, 'error': 0}

for prefix in dirs:
ts_str = prefix.rstrip('/').split('/')[-1]
if args.stop_at and _dir_unix(prefix) < args.stop_at:
print(f" Reached stop-at threshold ({args.stop_at}), stopping.")
break
is_live = (prefix == most_recent)
status, msg = process_dir(s3, args.bucket, prefix, is_live, args.dry_run, s3_write)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter non-timestamp prefixes before ordering and stop-at logic.

_dir_unix() returns 0 for non-numeric prefixes; with current flow, those can become most_recent or trip the stop-at break early, skipping valid directories.

💡 Suggested patch
-    dirs = list_timestamp_dirs(s3, args.bucket, args.node)
-    if not dirs:
+    dirs = list_timestamp_dirs(s3, args.bucket, args.node)
+    dirs = [d for d in dirs if _dir_unix(d) > 0]
+    dirs.sort(key=_dir_unix)
+    if not dirs:
         print("No timestamp directories found.")
         sys.exit(0)
@@
-    most_recent = dirs[-1] if dirs else None
+    most_recent = dirs[-1]
     dirs = list(reversed(dirs))
@@
-        if args.stop_at and _dir_unix(prefix) < args.stop_at:
+        ts = _dir_unix(prefix)
+        if args.stop_at and ts < args.stop_at:
             print(f"  Reached stop-at threshold ({args.stop_at}), stopping.")
             break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@node_2026/repair_manifests.py` around lines 293 - 317, Filter out
non-timestamp prefixes (where _dir_unix(prefix) == 0) immediately after calling
list_timestamp_dirs and before computing most_recent, reversing, and applying
stop-at logic; update the code around vars dirs, most_recent and the stop_at
check so you discard entries with _dir_unix(...) == 0 (or otherwise non-numeric)
and then compute most_recent = dirs[-1] if dirs else None, reverse dirs, and run
the stop-at comparison using the numeric _dir_unix(prefix) value to avoid
non-timestamp entries becoming most_recent or prematurely triggering the stop-at
break; keep references to list_timestamp_dirs, _dir_unix, most_recent, dirs,
stop_at, and process_dir.

Comment thread node_2026/Dockerfile

FROM debian:trixie

LABEL maintainer="Orcasound <orcanode-devs@orcasound.net>"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who does orcanode-devs@orcasound.net currently go to?

Comment thread node_2026/README.txt
Comment on lines +74 to +75
NODE_NAME=rpi_orcasound_lab # unique name for this node
NODE_TYPE=hls-only # hls-only or research

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NODE_NAME=rpi_orcasound_lab # unique name for this node
NODE_TYPE=hls-only # hls-only or research
NODE_NAME=rpi_orcasound_lab # unique name for this node
NODE_TYPE=hls-only # hls-only or research

nit: align spacing

Comment thread node_2026/README.txt
After the first build, Docker manages the container automatically:
- restart: always — restarts on crash without any action needed
- Docker enabled at boot — container starts on every reboot
- crontab (installed by setup.sh) restarts at midnight each night so

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still true that it restarts at midnight each night?

Comment thread node_2026/stream_sync.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# --- 0. PREFLIGHT CHECKS ---
# The .env sourcing is commented out because Docker Compose handles variable injection.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't follow this, but do we need to keep these lines at all or should they be removed?

Comment thread node_2026/upload_s3.py
BASEPATH = os.path.join("/tmp", NODE)
PATH = os.path.join(BASEPATH, "hls")

# REGION = os.environ["REGION"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should line 18 be removed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants