Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
136 changes: 136 additions & 0 deletions .github/workflows/build-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Build and Test Orcanode Container

on:
push:
branches: [main]
paths:
- 'node/**'
- 'base/**'
- '.github/workflows/build-container.yml'
tags:
- 'v*.*.*' # Trigger on version tags.
pull_request:
paths:
- 'node/**'
- 'base/**'
- '.github/workflows/build-container.yml'
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Build container image
run: docker build -t orcanode:test -f node/Dockerfile .

- name: Verify Python is available
run: docker run --rm orcanode:test python3 --version

- name: Verify FFmpeg is installed
run: docker run --rm orcanode:test ffmpeg -version

- name: Verify Jack audio is installed
run: docker run --rm orcanode:test which jackd

- name: Verify Python dependencies
run: docker run --rm orcanode:test python3 -c "import boto3; import inotify; print('Python deps OK')"

- name: Verify application scripts are present
run: docker run --rm orcanode:test ls -la /app/stream.sh

- name: Test container startup
run: |
# Copy .env.example to test.env and customize for CI.
cp node/.env.example test.env

# Replace FILLTHISIN placeholders with test values.
sed -i 's/AWS_ACCESS_KEY_ID=FILLTHISIN/AWS_ACCESS_KEY_ID=test-key-id/g' test.env
sed -i 's/AWS_SECRET_ACCESS_KEY=FILLTHISIN/AWS_SECRET_ACCESS_KEY=test-secret/g' test.env
sed -i 's/key="FILLTHISIN"/key="test-logdna-key"/g' test.env
sed -i 's/NODE_NAME=rpi_location/NODE_NAME=rpi_github_ci/g' test.env

# Disable loopback testing since CI environments won't have audio hardware.
sed -i 's/NODE_LOOPBACK=true/NODE_LOOPBACK=false/g' test.env

# Start container with env file.
docker run -d --name orcanode-test \
--env-file test.env \
orcanode:test || true
Comment thread
dthaler marked this conversation as resolved.
Outdated

sleep 5
echo "=== Container logs ==="
docker logs orcanode-test

# Check container health: accept running or cleanly exited (exit code 0)
STATUS=$(docker inspect --format='{{.State.Status}}' orcanode-test)
EXIT_CODE=$(docker inspect --format='{{.State.ExitCode}}' orcanode-test)

echo "Container status: $STATUS, exit code: $EXIT_CODE"

if [ "$STATUS" = "running" ]; then
echo "Container is running (audio hardware errors expected in CI)"
elif [ "$STATUS" = "exited" ] && [ "$EXIT_CODE" = "0" ]; then
echo "Container exited cleanly (exit code 0)"
else
echo "Container failed with status: $STATUS, exit code: $EXIT_CODE"
exit 1
fi

docker stop orcanode-test || true
docker rm orcanode-test
rm test.env

build-and-push:
needs: test
if: github.ref == 'refs/heads/main' || github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
actions: write # Required for cache-to: type=gha

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1

- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with:
images: ghcr.io/${{ github.repository }}/orcanode
tags: |
# Semver tags on release
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# Branch-based tags
type=ref,event=branch
type=sha,prefix={{branch}}-
# Latest tag for main branch
type=raw,value=latest,enable={{is_default_branch}}
Comment on lines +155 to +170

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the repository name to confirm the resulting image path
echo "Repository: orcasound/orcanode"
echo "Resulting image path: ghcr.io/orcasound/orcanode/orcanode"
echo ""
echo "If this is unintended, the image path should be: ghcr.io/orcasound/orcanode"

Repository: orcasound/orcanode

Length of output: 227


🏁 Script executed:

# Check if there are other container images or references in the workflow
rg "images:" .github/workflows/ -A 2

Repository: orcasound/orcanode

Length of output: 290


🏁 Script executed:

# Look for any documentation about container image naming in the repo
fd -e md -e txt | xargs rg -l "container|image|ghcr" 2>/dev/null | head -20

Repository: orcasound/orcanode

Length of output: 87


🏁 Script executed:

# Check for other docker/metadata-action uses in the repository
rg "docker/metadata-action" .github/workflows/

Repository: orcasound/orcanode

Length of output: 186


🏁 Script executed:

# Look at the broader workflow context to see if there are multiple services/images
cat -n .github/workflows/build-container.yml | head -50

Repository: orcasound/orcanode

Length of output: 2092


🏁 Script executed:

# Check the README files for any documentation about container image naming
cat README.md | grep -i "container\|image\|ghcr" -A 2 -B 2

Repository: orcasound/orcanode

Length of output: 1719


🏁 Script executed:

# Also check the full build-container.yml workflow to understand if there's any reason for the /orcanode suffix
cat -n .github/workflows/build-container.yml | sed -n '130,182p'

Repository: orcasound/orcanode

Length of output: 2446


Simplify the image path by removing the redundant /orcanode suffix.

The image path ghcr.io/${{ github.repository }}/orcanode resolves to ghcr.io/orcasound/orcanode/orcanode, which is redundant since only a single container image is built in this workflow. Use:

images: ghcr.io/${{ github.repository }}

This produces the simpler and more conventional image path ghcr.io/orcasound/orcanode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-container.yml around lines 155 - 170, Update the
docker/metadata-action step (id: meta) to use a simplified image name under the
images key: change images: ghcr.io/${{ github.repository }}/orcanode to images:
ghcr.io/${{ github.repository }} so the resulting image path becomes
ghcr.io/orcasound/orcanode instead of the redundant
ghcr.io/orcasound/orcanode/orcanode; keep the rest of the metadata-action
configuration (tags block and type/raw latest enable) unchanged.


- name: Build and push multi-arch images
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
with:
context: .
file: ./node/Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
.env
.vs
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ An ARM or X86 device with a sound card (or other audio input devices) connected

### Installing

Create a base docker image for your architecture by running the script in /base/rpi or /base/amd64 as appropriate. You will need to create a .env file as appropriate for your projects. Here is an example of an .env file (tested/working as of June, 2021)...
The container is built from `node/Dockerfile` and is self-contained (no separate base image step required). You will need to create a `.env` file based on `node/.env.example` for your deployment. Here is an example of an `.env` file...

```
AWS_METADATA_SERVICE_TIMEOUT=5
Expand All @@ -43,11 +43,11 @@ LC_ALL=C.UTF-8
... except that the following fields are excised and will need to be added if you are integrating with the audio and logging systems of Orcasound:

```
AWSACCESSKEYID=YourAWSaccessKey
AWSSECRETACCESSKEY=YourAWSsecretAccessKey
SYSLOG_URL=syslog+tls://syslog-a.logdna.com:YourLogDNAPort
SYSLOG_STRUCTURED_DATA='logdna@YourLogDNAnumber key="YourLogDNAKey" tag="docker"
AWS_ACCESS_KEY_ID=YourAWSaccessKeyId
AWS_SECRET_ACCESS_KEY=YourAWSsecretAccessKey

SYSLOG_URL=syslog://syslog-a.logdna.com:YourLogDNAPort
SYSLOG_STRUCTURED_DATA='logdna@YourLogDNAnumber key="YourLogDNAKey" tag="docker"'
```

(You can request keys via the #hydrophone-nodes channel in the Orcasound Slack. As of October, 2021, we are continuing to use AWS S3 for storage and LogDNA for live-logging and troubleshooting.)
Expand All @@ -64,7 +64,7 @@ Here are explanations of some of the .env fields:

## Running local tests

At the root of the repository directory (where you also put your .env file) first copy the compose file you want to `docker-compose.yml`. For example, if you have a Raspberry Pi and you want to use the prebuilt image, then copy `docker-compose.rpi-pull.yml` to `docker-compose.yml`. Then run `docker-compose up -d`. Watch what happens using `htop`. If you want to verify files are being written to /tmp or /mnt directories, get the name of your streaming service using `docker-compose ps` (in this case `orcanode_streaming_1`) and then do `docker exec -it orcanode_streaming_1 /bin/bash` to get a bash shell within the running container.
Go to the `node/` directory (where you also put your `.env` file). Copy the compose file you want to `docker-compose.yml`. For example, if you have a Raspberry Pi and you want to use the prebuilt image, then copy `docker-compose.rpi-pull.yml` to `docker-compose.yml`. Then run `docker-compose up -d`. Watch what happens using `htop`. If you want to verify files are being written to /tmp or /mnt directories, get the name of your streaming service using `docker-compose ps` (in this case `orcanode_streaming_1`) and then do `docker exec -it orcanode_streaming_1 /bin/bash` to get a bash shell within the running container.

### Running an end-to-end test

Expand All @@ -91,9 +91,9 @@ If you would like to add a node to the Orcasound hydrophone network, contact adm

## Built With

* [FFmpeg](https://www.ffmpeg.org/) - Uses ALSA to acquire audio data, then generates lossy streams and/or lossless archive files
* [rsync](https://rsync.samba.org/) - Transfers files locally from /tmp to /mnt directories
* [s3fs](https://github.com/s3fs-fuse/s3fs-fuse) - Used to transfer audio data from local device to S3 bucket(s)
* [FFmpeg](https://www.ffmpeg.org/) - Uses JACK/ALSA to acquire audio data, then generates lossy streams and/or lossless archive files
* [JACK](https://jackaudio.org/) - Low-latency audio server used to route audio from the sound card to FFmpeg
* [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) - AWS SDK for Python, used to transfer audio data from the local device to S3 bucket(s)

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion base/upload_flac_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
print("using dev bucket")
BUCKET = "dev-streaming-orcasound-net"

log.debug("archive bucket set to ", BUCKET)
log.debug("archive bucket set to %s", BUCKET)

def s3_copy_file(path, filename):
log.debug('uploading file '+filename+' from '+path+' to bucket '+BUCKET)
Expand Down
2 changes: 1 addition & 1 deletion base/upload_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
print("using dev bucket")
BUCKET = "dev-streaming-orcasound-net"

log.debug("hls bucket set to ", BUCKET)
log.debug("hls bucket set to %s", BUCKET)

def s3_copy_file(path, filename):
log.debug('uploading file '+filename+' from '+path+' to bucket '+BUCKET)
Expand Down
89 changes: 89 additions & 0 deletions node/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Orcanode Configuration Template
# Copy this file to .env and customize for your deployment
# See README.md for detailed field descriptions

# === AWS Credentials ===
# Get these from AWS IAM Console or contact Orcasound admin
AWS_ACCESS_KEY_ID=FILLTHISIN
AWS_SECRET_ACCESS_KEY=FILLTHISIN

# === AWS Configuration ===
AWS_METADATA_SERVICE_TIMEOUT=5
AWS_METADATA_SERVICE_NUM_ATTEMPTS=0
REGION=us-west-2

# === S3 Bucket Selection ===
BUCKET_TYPE=dev
#BUCKET_TYPE=prod

Comment thread
dthaler marked this conversation as resolved.
# === Node Configuration ===
# Uncomment the NODE_TYPE you need:
NODE_TYPE=hls-only
#NODE_TYPE=research
#NODE_TYPE=offline
Comment thread
dthaler marked this conversation as resolved.
Outdated

# Unique identifier for this node (format: rpi_location)
NODE_NAME=rpi_location

# Audio loopback for testing (true/false/hls)
# - true: Route audio input directly to output via Jack
# - false: No loopback
# - hls: Play back the generated HLS stream with ffplay (for testing)
NODE_LOOPBACK=true
#NODE_LOOPBACK=false
#NODE_LOOPBACK=hls

# Audio Settings
SAMPLE_RATE=48000
AUDIO_HW_ID=pisound
CHANNELS=1

# === Logging Configuration ===
# Used by logspout sidecar - get credentials from Orcasound admin
SYSLOG_URL=syslog://syslog-a.logdna.com:37043
SYSLOG_STRUCTURED_DATA='logdna@48950 key="FILLTHISIN" tag="docker"'

# === Stream Configuration ===
FLAC_DURATION=30
SEGMENT_DURATION=10

# === System Configuration ===
LC_ALL=C.UTF-8

# === Field Descriptions ===
#
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY:
# AWS credentials for S3 bucket access
#
# BUCKET_TYPE:
# - dev: Development bucket (dev-streaming-orcasound-net)
# - prod: Production bucket (audio-orcasound-net)
# - custom: Use custom buckets (requires BUCKET_STREAMING and BUCKET_ARCHIVE)
#
# NODE_TYPE:
# - hls-only: Stream HLS segments only (most common)
# - research: Stream HLS + archive FLAC files
# - offline: Local testing mode
#
# NODE_NAME:
# Unique identifier in format: rpi_location (e.g., rpi_bush_point, rpi_port_townsend)
#
# AUDIO_HW_ID:
# Sound card identifier. Find with: arecord -l
# Use logical name (pisound, USB) not device numbers (0,0)
#
# SAMPLE_RATE:
# Audio sampling rate in Hz (48000 for standard, 192000 for research)
#
# CHANNELS:
# Number of audio channels (1=mono, 2=stereo)
#
# SEGMENT_DURATION:
# Seconds per HLS segment (typically 10)
#
# FLAC_DURATION:
# Seconds per archived FLAC file (research mode only, typically 30)
#
# SYSLOG_URL / SYSLOG_STRUCTURED_DATA:
# LogDNA/Mezmo credentials for centralized logging via logspout
# Request credentials in #hydrophone-nodes on Orcasound Slack
5 changes: 5 additions & 0 deletions node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Environment variables with secrets
.env

# But keep the example
!.env.example
89 changes: 53 additions & 36 deletions node/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
# Node Dockerfile for hydrophone streaming
# use base image for project

FROM orcastream/orcabase:latest
MAINTAINER Orcasound <orcanode-devs@orcasound.net>

###### hack to get ffmpeg to build
# RUN apt-get update && apt-get install -y --no-install-recommends libraspberrypi-dev raspberrypi-kernel-headers
# RUN git clone https://github.com/raspberrypi/userland.git
# RUN cd userland/host_applications/linux/apps/hello_pi && ./rebuild.sh


###### Install Jack #################################

ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends jack-capture
RUN apt-get update && apt-get install -y --no-install-recommends jackd1

# Install ALSA and GPAC
#RUN apt-get update && apt-get install -y --no-install-recommends \
# alsa-utils \
# gpac

############################### Copy files #####################################

COPY . .

################################## TODO ########################################
# Do the following:
# - Add pisound driver curl command
# - Add other audio drivers and configure via CLI if possible?
# - Remove "misc tools" and other installs no longer needed (upon Resin.io deployment)?

################################# Miscellaneous ################################

# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Multi-arch self-contained build for CI/CD deployment

FROM python:3.11-slim-bookworm

LABEL org.opencontainers.image.source="https://github.com/orcasound/orcanode"
LABEL org.opencontainers.image.description="Orcasound hydrophone streaming node"
LABEL org.opencontainers.image.authors="Orcasound <orcanode-devs@orcasound.net>"

# Install system dependencies
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
jack-capture \
jackd2 \
alsa-utils \
git \
curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Install Python dependencies
RUN pip3 install --no-cache-dir \
boto3 \
inotify

# Set working directory
WORKDIR /app

# Copy node application files first
COPY node/ ./

# Copy Python upload scripts from base directory (overwrites if they exist in node/)
COPY base/upload_s3.py ./
COPY base/upload_flac_s3.py ./
Comment thread
dthaler marked this conversation as resolved.

# Set environment defaults
ENV SAMPLE_RATE=48000 \
STREAM_RATE=48000 \
CHANNELS=1 \
NODE_TYPE=hls-only

# Make scripts executable
RUN chmod +x *.sh

# Ensure .env.example is available for reference
RUN if [ -f .env.example ]; then \
echo ".env.example is available at /app/.env.example"; \
else \
echo "Warning: .env.example not found"; \
fi

# Run the streaming script
CMD ["./stream.sh"]
Loading
Loading