diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index ee7dfb2..6f0b13f 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -13,9 +13,9 @@ jobs: name: Flake Checker runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v13 - - uses: DeterminateSystems/magic-nix-cache-action@v7 - - uses: DeterminateSystems/flake-checker-action@v8 + - uses: DeterminateSystems/nix-installer-action@v21 + - uses: DeterminateSystems/magic-nix-cache-action@v13 + - uses: DeterminateSystems/flake-checker-action@v12 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 0a2c2bc..a0c019d 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -10,11 +10,11 @@ jobs: name: Flake Lock Updater runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v13 - - uses: DeterminateSystems/magic-nix-cache-action@v7 - - uses: DeterminateSystems/update-flake-lock@v23 + - uses: DeterminateSystems/nix-installer-action@v21 + - uses: DeterminateSystems/magic-nix-cache-action@v13 + - uses: DeterminateSystems/update-flake-lock@v28 with: pr-title: "chore: update flake.lock" diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index c70997b..b6dbcb5 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -15,7 +15,7 @@ jobs: name: Validate pull request title runs-on: ubuntu-22.04 steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/lint-shellcheck.yml b/.github/workflows/lint-shellcheck.yml index bd125ea..81b8108 100644 --- a/.github/workflows/lint-shellcheck.yml +++ b/.github/workflows/lint-shellcheck.yml @@ -10,7 +10,7 @@ jobs: name: Shellcheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index dd1890b..63153dd 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: name: "Check versions โš–๏ธ" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Compare App and Git versions ๐ŸŸฐ" @@ -37,7 +37,7 @@ jobs: name: "Build Release ๐Ÿ‘จโ€๐Ÿ”ง" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: "Build .deb ๐Ÿฅ" env: DEBFULLNAME: "Martin Wimpress" @@ -69,7 +69,7 @@ jobs: id-token: "write" contents: "read" steps: - - uses: "actions/checkout@v4" + - uses: "actions/checkout@v6" with: ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" - uses: "DeterminateSystems/nix-installer-action@main" @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout ๐Ÿฅก" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx @@ -113,5 +113,11 @@ jobs: ghcr.io/${{ github.repository }}:${{ env.STREAM_SPROUT_VER }}-alpine ghcr.io/${{ github.repository }}:${{ github.sha }}-alpine platforms: linux/amd64, linux/arm64 + - name: "Generate SBOM" + uses: anchore/sbom-action@v0 + with: + image: ghcr.io/${{ github.repository }}:latest-alpine + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} - name: Logout from Container Registry run: docker logout ghcr.io diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml new file mode 100644 index 0000000..b60b1a0 --- /dev/null +++ b/.github/workflows/scan-container.yaml @@ -0,0 +1,35 @@ +name: "Vulnerability ๐Ÿž scan ๐Ÿ” container" + +on: + schedule: + - cron: "0 10 * * 2" + workflow_dispatch: + +jobs: + vulnerability-scan: + name: "Build and scan" + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: build local container + uses: docker/build-push-action@v6 + with: + context: . + file: ./Containerfile + tags: localbuild/testimage:latest + push: false + load: true + + - name: Scan image + uses: anchore/scan-action@v7 + with: + image: "localbuild/testimage:latest" + output-format: table + + - name: Inspect action report + run: cat ${{ steps.scan.outputs.table }} \ No newline at end of file diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 265f7fd..e9a43b0 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout ๐Ÿฅก" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Build & Test .deb ๐Ÿฅ" env: DEBFULLNAME: "Martin Wimpress" @@ -55,11 +55,11 @@ jobs: contents: "read" steps: - name: "Checkout ๐Ÿฅก" - uses: "actions/checkout@v4" + uses: "actions/checkout@v6" - name: "Install Nix โ„๏ธ" - uses: "DeterminateSystems/nix-installer-action@v13" + uses: "DeterminateSystems/nix-installer-action@v21" - name: "Enable Magic Nix Cache ๐Ÿช„" - uses: "DeterminateSystems/magic-nix-cache-action@v7" + uses: "DeterminateSystems/magic-nix-cache-action@v13" - name: "Build & Test .nix โ„๏ธ" run: | nix build .#stream-sprout @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout ๐Ÿฅก" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout ๐Ÿฅก - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build snap ๐ŸŠ uses: snapcore/action-build@v1 id: snapcraft @@ -117,7 +117,7 @@ jobs: snap: ${{ steps.snapcraft.outputs.snap }} isClassic: false - name: Upload artifacts โคด๏ธ - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v6 with: name: stream-sprout-snap path: ${{ steps.snapcraft.outputs.snap}} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2a806d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# AGENTS.md + +## Overview + +Stream Sprout is a bash-based RTMP restreaming tool that forwards a single video source (from OBS Studio or similar) to multiple destinations like Twitch, YouTube, Owncast, and Peertube simultaneously. It uses FFmpeg's tee muxer to copy streams without transcoding. + +## Tech Stack + +- **Language:** Bash 5.0+ (single script: `stream-sprout`) +- **Runtime dependency:** FFmpeg (RTMP server and restreaming) +- **Configuration:** YAML parsed via awk/sed +- **Packaging:** Nix flake, Debian .deb, Snap, Docker/Podman + +## Build and Run Commands + +```bash +# Run directly (requires ffmpeg, bash 5.0+, awk, grep, sed) +./stream-sprout --config stream-sprout.yaml + +# Show version and FFmpeg info +./stream-sprout --version + +# Show system info (useful for bug reports) +./stream-sprout --info + +# Nix build +nix build + +# Enter development shell with all dependencies +nix develop + +# Docker build and run +docker build -t stream-sprout . +docker run -p 1935:1935 -it -v $PWD:/data stream-sprout --config /data/stream-sprout.yaml +``` + +## Linting + +ShellCheck is enforced via CI on all pull requests. + +```bash +# Run locally before committing +shellcheck stream-sprout +``` + +The script includes `# shellcheck disable=SC2154` for variables set dynamically via `eval` from YAML parsing. + +## Code Style + +- Bash scripts use `#!/usr/bin/env bash` +- Functions use `function name() {}` syntax +- Use `local` for function-scoped variables +- Use `readonly` for constants +- Validation with informative error messages using Unicode icons and ANSI colours +- Version is tracked in the script: `readonly VERSION="x.y.z"` + +## Project Structure + +``` +stream-sprout # Main bash script (single file) +stream-sprout.yaml # Local config (gitignored) +stream-sprout.yaml.example # Example configuration +package.nix # Nix package definition +devshell.nix # Nix development shell +flake.nix # Nix flake +Dockerfile # Alpine-based container +``` + +## Configuration + +YAML config with two main sections: + +- `server:` - RTMP server settings (ip, port, app, key, archive options) +- `services:` - Destination services (each with enabled, rtmp_server, key) + +Config search order: `./stream-sprout.yaml`, `$XDG_CONFIG_HOME/stream-sprout.yaml`, `/etc/stream-sprout.yaml` + +## PR and Commit Guidelines + +- **Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/)** +- PR titles are validated against Conventional Commits format +- Single-commit PRs must have matching PR title and commit message +- ShellCheck must pass with no warnings + +Common prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:` + +## Version Updates + +When changing version: + +1. Update `VERSION` in `stream-sprout` script +2. The Nix package extracts version automatically from the script + +## Constraints + +- Requires bash 5.0 or newer +- FFmpeg must be available on PATH +- RTMP only (no RTMPS support currently) +- FFmpeg does not enforce stream keys (documented security limitation) + +## Security Considerations + +- Stream keys are stored in plain text in YAML config +- FFmpeg accepts any RTMP stream on the configured port regardless of app/key path +- Do not expose the RTMP port to untrusted networks without additional protection (VPN, firewall, SSH tunnel) diff --git a/Containerfile b/Containerfile index 13d8c86..5e0b5dc 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,17 @@ -FROM ghcr.io/jrottenberg/ffmpeg:7-alpine -RUN apk add --no-cache --update bash coreutils gawk grep sed +FROM alpine:latest + +RUN apk add --no-cache --update \ + bash \ + jellyfin-ffmpeg \ + gawk \ + grep \ + sed + +RUN ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/local/bin/ffmpeg && \ + ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/local/bin/ffprobe + COPY --chown=nobody:nobody --chmod=755 stream-sprout /usr/bin/stream-sprout + EXPOSE 1935 +USER nobody ENTRYPOINT [ "stream-sprout" ] diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 0000000..5240dc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +Containerfile \ No newline at end of file diff --git a/README.md b/README.md index 021ff63..c2c81e7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Stream Sprout is developed on Linux ๐Ÿง and should work on macOS ๐Ÿ or any ot ## Get Started - [Install](#installation) Stream Sprout ๐Ÿง‘โ€๐Ÿ’ป -- [Configure](#configuration) Stream Sprout ๐Ÿง‘โ€๐Ÿ’ป +- [Configure](#configure-stream-sprout) Stream Sprout ๐Ÿง‘โ€๐Ÿ’ป - [Configure](#configure-obs-studio) OBS Studio ๐ŸŽ›๏ธ - Start `stream-sprout` โŒจ๏ธ - Click the *Start Streaming* button in OBS Studio ๐Ÿ–ฑ๏ธ @@ -271,10 +271,6 @@ services: [rtmp @ 0x2ca9be80] Unexpected stream STREAMBOMB, expecting c5b559b2-589d-4925-a28e-20d1954fd6c5 Last message repeated 1 times ``` -- Stream Sprout does not support restreaming using secure RTMP (RTMPS). - - *At least I don't think it does, but I haven't fully tested it.* - - Kick only appears to support rtmps:// URLs and Stream Sprout restreams do not appear on Kick. - - https://superuser.com/questions/1438939/live-streaming-over-rtmps-using-ffmpeg - Each destination you add will increase your bandwidth requirements. ## References diff --git a/flake.lock b/flake.lock index 3480d0d..6186ae3 100644 --- a/flake.lock +++ b/flake.lock @@ -2,12 +2,12 @@ "nodes": { "flake-schemas": { "locked": { - "lastModified": 1721078157, - "narHash": "sha256-c2AZH9cOnSpPXV8Lwy19/I8EgW7G+E+Zh6YQBZZwzxI=", - "rev": "29e53dd33b1a38f235ef073e768c62821cb6146e", - "revCount": 66, + "lastModified": 1761577921, + "narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=", + "rev": "47849c7625e223d36766968cc6dc23ba0e135922", + "revCount": 107, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.3/0190b841-54d3-7b7a-8550-24942bc38caf/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.2.0/019a4a84-544d-7c59-b26d-e334e320c932/source.tar.gz" }, "original": { "type": "tarball", @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1721548954, - "narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=", - "rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a", - "revCount": 633334, + "lastModified": 1769318308, + "narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=", + "rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c", + "revCount": 906484, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633334%2Brev-63d37ccd2d178d54e7fb691d7ec76000740ea24a/0190d847-0241-7628-8ab0-d49f442300f4/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.906484%2Brev-1cd347bf3355fce6c64ab37d3967b4a2cb4b878c/019bfb68-fb8e-7f55-bb2a-5bee98516c95/source.tar.gz" }, "original": { "type": "tarball", diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3ad0b86..5183687 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -16,9 +16,6 @@ platforms: arm64: build-on: [ arm64 ] build-for: [arm64 ] - armhf: - build-on: [ armhf ] - build-for: [ armhf ] parts: stream-sprout: diff --git a/stream-sprout b/stream-sprout index eb911d8..9fa9a20 100755 --- a/stream-sprout +++ b/stream-sprout @@ -5,7 +5,7 @@ stty -echoctl readonly STREAM_SPROUT_YAML="stream-sprout.yaml" -readonly VERSION="0.1.5" +readonly VERSION="0.1.6" function cleanup() { echo -e " \e[31m\U26D4\e[0m Control-C"