mirror of
https://github.com/wimpysworld/stream-sprout
synced 2026-03-14 14:45:50 +01:00
Compare commits
72 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6975589341 | ||
|
|
5df0f028e2 | ||
|
|
8393e053b9 | ||
|
|
7a63368963 |
||
|
|
da76e1c219 |
||
|
|
2bda8192f1 |
||
|
|
a811fe527e | ||
|
|
c417dc1b10 | ||
|
|
b3be5c43d9 | ||
|
|
90eb31a5e5 | ||
|
|
f76da1c62b | ||
|
|
5d864aacb8 | ||
|
|
382dff7a48 |
||
|
|
bd1676efa6 |
||
|
|
a79d451d0c | ||
|
|
c470ca46e4 | ||
|
|
c156db1f64 | ||
|
|
7c57494674 | ||
|
|
57a1f800d2 | ||
|
|
9fbbde4d6c | ||
|
|
1d7e3e8247 | ||
|
|
84a1e43137 | ||
|
|
48c4943d72 | ||
|
|
1a19e85d94 | ||
|
|
3f91c0f573 | ||
|
|
e5a0db3a8f | ||
|
|
6ec390f406 | ||
|
|
0bb875c287 | ||
|
|
901586e4bf | ||
|
|
ed5d5d136b | ||
|
|
5aa579111e | ||
|
|
39c182ecf7 | ||
|
|
6984d04f7a | ||
|
|
1cb4c8ced3 | ||
|
|
a9ed96eaea | ||
|
|
43d6b9ad88 | ||
|
|
8e3b4dc089 | ||
|
|
f1b552c2bd |
||
|
|
9de404f4b2 |
||
|
|
84b36880cb | ||
|
|
198c807a07 |
||
|
|
5514c3da26 | ||
|
|
a11f8f57b0 | ||
|
|
544822aaa9 | ||
|
|
4495463a74 | ||
|
|
46d611bbda | ||
|
|
3ae05ef362 | ||
|
|
e25f977afd | ||
|
|
c78953780a | ||
|
|
132b240133 | ||
|
|
7b548aa7df | ||
|
|
6f89206695 | ||
|
|
86352205af | ||
|
|
9830543ac5 | ||
|
|
f20bde1521 | ||
|
|
cb86dfc5f4 | ||
|
|
ec278996b9 | ||
|
|
1cc2100527 | ||
|
|
5801fecb3d | ||
|
|
1f5231c6e9 | ||
|
|
cd6a8185c6 | ||
|
|
f3b1271813 | ||
|
|
830edfabab | ||
|
|
f0262fbd38 | ||
|
|
0eba5601d7 |
||
|
|
a79438f0d2 | ||
|
|
4928a51bd1 | ||
|
|
dda746f103 | ||
|
|
4e32f890bc | ||
|
|
f0916a091b | ||
|
|
7998737c0e | ||
|
|
8ca09f94b5 |
18 changed files with 618 additions and 97 deletions
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -21,21 +21,25 @@ Steps to reproduce the behavior:
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Stream Sprout output**
|
**Stream Sprout output and logs**
|
||||||
Run `stream-sprout` and include the output of the failure below:
|
Run `stream-sprout` and include the output, along with **redacted logs** (*remove your IP address and keys*), and wrap it in the collapsible markdown section below.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Stream Sprout output</summary>
|
<summary>Stream Sprout output</summary>
|
||||||
|
|
||||||
```text
|
```text
|
||||||
stream-sprout output here
|
stream-sprout output from the time of the error here
|
||||||
|
```
|
||||||
|
|
||||||
|
<summary>Stream Sprout logs</summary>
|
||||||
|
|
||||||
|
```text
|
||||||
|
stream-sprout logs from the time of the error here
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
**System information**
|
**System information**
|
||||||
- OS: [e.g. Ubuntu 20.04]
|
Run `stream-sprout --info` and include the output here.
|
||||||
- stream-sprout version: [e.g. 0.1.0]
|
|
||||||
- FFmpeg version: [e.g. 4.2.4]
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
|
||||||
BIN
.github/obs-settings.png
vendored
BIN
.github/obs-settings.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 26 KiB |
8
.github/workflows/flake-checker.yml
vendored
8
.github/workflows/flake-checker.yml
vendored
|
|
@ -13,9 +13,9 @@ jobs:
|
||||||
name: Flake Checker
|
name: Flake Checker
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: DeterminateSystems/nix-installer-action@v13
|
- uses: DeterminateSystems/nix-installer-action@v21
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@v7
|
- uses: DeterminateSystems/magic-nix-cache-action@v13
|
||||||
- uses: DeterminateSystems/flake-checker-action@v8
|
- uses: DeterminateSystems/flake-checker-action@v12
|
||||||
|
|
|
||||||
8
.github/workflows/flake-updater.yml
vendored
8
.github/workflows/flake-updater.yml
vendored
|
|
@ -10,11 +10,11 @@ jobs:
|
||||||
name: Flake Lock Updater
|
name: Flake Lock Updater
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: DeterminateSystems/nix-installer-action@v13
|
- uses: DeterminateSystems/nix-installer-action@v21
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@v7
|
- uses: DeterminateSystems/magic-nix-cache-action@v13
|
||||||
- uses: DeterminateSystems/update-flake-lock@v23
|
- uses: DeterminateSystems/update-flake-lock@v28
|
||||||
with:
|
with:
|
||||||
pr-title: "chore: update flake.lock"
|
pr-title: "chore: update flake.lock"
|
||||||
|
|
|
||||||
2
.github/workflows/lint-pr.yml
vendored
2
.github/workflows/lint-pr.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
name: Validate pull request title
|
name: Validate pull request title
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v6
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/lint-shellcheck.yml
vendored
2
.github/workflows/lint-shellcheck.yml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
name: Shellcheck
|
name: Shellcheck
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Run ShellCheck
|
- name: Run ShellCheck
|
||||||
uses: ludeeus/action-shellcheck@master
|
uses: ludeeus/action-shellcheck@master
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
48
.github/workflows/publish-release.yml
vendored
48
.github/workflows/publish-release.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
name: "Check versions ⚖️"
|
name: "Check versions ⚖️"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: "Compare App and Git versions 🟰"
|
- name: "Compare App and Git versions 🟰"
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
name: "Build Release 👨🔧"
|
name: "Build Release 👨🔧"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: "Build .deb 🍥"
|
- name: "Build .deb 🍥"
|
||||||
env:
|
env:
|
||||||
DEBFULLNAME: "Martin Wimpress"
|
DEBFULLNAME: "Martin Wimpress"
|
||||||
|
|
@ -69,7 +69,7 @@ jobs:
|
||||||
id-token: "write"
|
id-token: "write"
|
||||||
contents: "read"
|
contents: "read"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@v4"
|
- uses: "actions/checkout@v6"
|
||||||
with:
|
with:
|
||||||
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
|
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
|
||||||
- uses: "DeterminateSystems/nix-installer-action@main"
|
- uses: "DeterminateSystems/nix-installer-action@main"
|
||||||
|
|
@ -79,3 +79,45 @@ jobs:
|
||||||
visibility: "public"
|
visibility: "public"
|
||||||
name: "wimpysworld/stream-sprout"
|
name: "wimpysworld/stream-sprout"
|
||||||
tag: "${{ inputs.tag }}"
|
tag: "${{ inputs.tag }}"
|
||||||
|
|
||||||
|
publish-container:
|
||||||
|
needs: [version-check]
|
||||||
|
name: "Publish Container 🐋"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: "Checkout 🥡"
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Container Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get stream-sprout version 🔢
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
STREAM_SPROUT_VER=$(grep "^readonly VERSION" stream-sprout | cut -d'"' -f2)
|
||||||
|
echo "STREAM_SPROUT_VER=$STREAM_SPROUT_VER" >> $GITHUB_ENV
|
||||||
|
- name: "Build Container 🐋"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Containerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:latest-alpine
|
||||||
|
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
|
||||||
|
|
|
||||||
35
.github/workflows/scan-container.yaml
vendored
Normal file
35
.github/workflows/scan-container.yaml
vendored
Normal file
|
|
@ -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 }}
|
||||||
70
.github/workflows/test-build-stream-sprout.yml
vendored
70
.github/workflows/test-build-stream-sprout.yml
vendored
|
|
@ -5,18 +5,24 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
- .github/workflows/*.yml
|
||||||
- stream-sprout
|
- stream-sprout
|
||||||
- debian/**
|
- debian/**
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- package.nix
|
- package.nix
|
||||||
|
- Containerfile
|
||||||
|
- snap/snapcraft.yaml
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
- .github/workflows/*.yml
|
||||||
- stream-sprout
|
- stream-sprout
|
||||||
- debian/**
|
- debian/**
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- package.nix
|
- package.nix
|
||||||
|
- Containerfile
|
||||||
|
- snap/snapcraft.yaml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# TODO: arm64 runner
|
# TODO: arm64 runner
|
||||||
|
|
@ -27,7 +33,7 @@ jobs:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout 🥡"
|
- name: "Checkout 🥡"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: "Build & Test .deb 🍥"
|
- name: "Build & Test .deb 🍥"
|
||||||
env:
|
env:
|
||||||
DEBFULLNAME: "Martin Wimpress"
|
DEBFULLNAME: "Martin Wimpress"
|
||||||
|
|
@ -49,13 +55,69 @@ jobs:
|
||||||
contents: "read"
|
contents: "read"
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout 🥡"
|
- name: "Checkout 🥡"
|
||||||
uses: "actions/checkout@v4"
|
uses: "actions/checkout@v6"
|
||||||
- name: "Install Nix ❄️"
|
- name: "Install Nix ❄️"
|
||||||
uses: "DeterminateSystems/nix-installer-action@v13"
|
uses: "DeterminateSystems/nix-installer-action@v21"
|
||||||
- name: "Enable Magic Nix Cache 🪄"
|
- name: "Enable Magic Nix Cache 🪄"
|
||||||
uses: "DeterminateSystems/magic-nix-cache-action@v7"
|
uses: "DeterminateSystems/magic-nix-cache-action@v13"
|
||||||
- name: "Build & Test .nix ❄️"
|
- name: "Build & Test .nix ❄️"
|
||||||
run: |
|
run: |
|
||||||
nix build .#stream-sprout
|
nix build .#stream-sprout
|
||||||
tree ./result
|
tree ./result
|
||||||
|
|
||||||
|
test-container-build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: "Checkout 🥡"
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Container Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get stream-sprout version 🔢
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
STREAM_SPROUT_VER=$(grep "^readonly VERSION" stream-sprout | cut -d'"' -f2)
|
||||||
|
echo "STREAM_SPROUT_VER=$STREAM_SPROUT_VER" >> $GITHUB_ENV
|
||||||
|
- name: "Build Container 🐋"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Containerfile
|
||||||
|
push: false
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:latest-alpine
|
||||||
|
ghcr.io/${{ github.repository }}:${{ env.STREAM_SPROUT_VER }}-alpine
|
||||||
|
ghcr.io/${{ github.repository }}:${{ github.sha }}-alpine
|
||||||
|
platforms: linux/amd64, linux/arm64
|
||||||
|
- name: Logout from Container Registry
|
||||||
|
run: docker logout ghcr.io
|
||||||
|
|
||||||
|
test-snap-build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🥡
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Build snap 🐊
|
||||||
|
uses: snapcore/action-build@v1
|
||||||
|
id: snapcraft
|
||||||
|
- name: Show log 🪵
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
cat /home/runner/.local/state/snapcraft/log/snapcraft*.log
|
||||||
|
- name: Review snap 🕵️
|
||||||
|
uses: diddlesnaps/snapcraft-review-action@v1
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.snapcraft.outputs.snap }}
|
||||||
|
isClassic: false
|
||||||
|
- name: Upload artifacts ⤴️
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: stream-sprout-snap
|
||||||
|
path: ${{ steps.snapcraft.outputs.snap}}
|
||||||
|
|
|
||||||
105
AGENTS.md
Normal file
105
AGENTS.md
Normal file
|
|
@ -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)
|
||||||
17
Containerfile
Normal file
17
Containerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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" ]
|
||||||
1
Dockerfile
Symbolic link
1
Dockerfile
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
Containerfile
|
||||||
129
README.md
129
README.md
|
|
@ -23,9 +23,9 @@ Stream Sprout 🌱 is a simple, self-contained, and easy-to-use solution for str
|
||||||
<img src=".github/demo.gif" alt="Stream Sprout" width="505" height="415"/>
|
<img src=".github/demo.gif" alt="Stream Sprout" width="505" height="415"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
It uses [FFmpeg](https://ffmpeg.org/) to receive the video stream from OBS Studio (or anything that can publish a RTMP stream) and then restreams it to multiple destinations; providing similar functionality as services like Restream.io and Livepush.io but without the need to pay 💸 for a third-party service or run something like nginx with the [RTMP module](https://github.com/arut/nginx-rtmp-module).
|
It uses [FFmpeg](https://ffmpeg.org/) to receive the video stream from OBS Studio (or any encoder that can produce RTMP) and then restreams it to multiple destinations. This provides similar functionality as services like Restream.io and Livepush.io but without the need to pay 💸 for a third-party service or run something like nginx with the [RTMP module](https://github.com/arut/nginx-rtmp-module).
|
||||||
|
|
||||||
Stream Sprout is configured with a simple YAML file and designed to be run on the same computer as your [OBS Studio](https://obsproject.com/) instance (it can be run remotely too) and does not require root privileges.
|
Stream Sprout is configured with a simple YAML file and designed to be run on the same computer as your [OBS Studio](https://obsproject.com/) instance (it can be run remotely, [**with appropriate security measures**](#-ffmpeg-rtmp-server-accepts-any-rtmp-stream-on-the-listening-port-), and does not require root privileges.
|
||||||
|
|
||||||
There is no transcoding or processing of the video stream 🎞️
|
There is no transcoding or processing of the video stream 🎞️
|
||||||
The stream is received and then restreamed to the destinations you configure without modification.
|
The stream is received and then restreamed to the destinations you configure without modification.
|
||||||
|
|
@ -39,7 +39,7 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
- [Install](#installation) Stream Sprout 🧑💻
|
- [Install](#installation) Stream Sprout 🧑💻
|
||||||
- [Configure](#configuration) Stream Sprout 🧑💻
|
- [Configure](#configure-stream-sprout) Stream Sprout 🧑💻
|
||||||
- [Configure](#configure-obs-studio) OBS Studio 🎛️
|
- [Configure](#configure-obs-studio) OBS Studio 🎛️
|
||||||
- Start `stream-sprout` ⌨️
|
- Start `stream-sprout` ⌨️
|
||||||
- Click the *Start Streaming* button in OBS Studio 🖱️
|
- Click the *Start Streaming* button in OBS Studio 🖱️
|
||||||
|
|
@ -52,7 +52,7 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot
|
||||||
### Debian
|
### Debian
|
||||||
|
|
||||||
- Download the Stream Sprout .deb package from the [releases page](https://github.com/wimpysworld/stream-sprout/releases) 📦️
|
- Download the Stream Sprout .deb package from the [releases page](https://github.com/wimpysworld/stream-sprout/releases) 📦️
|
||||||
- Install it with `apt-get install ./stream-sprout_0.1.4-1_all.deb`.
|
- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`.
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
|
|
@ -78,10 +78,66 @@ See the flake on FlakeHub for more details:
|
||||||
|
|
||||||
- <https://flakehub.com/flake/wimpysworld/stream-sprout>
|
- <https://flakehub.com/flake/wimpysworld/stream-sprout>
|
||||||
|
|
||||||
|
### Snap
|
||||||
|
|
||||||
|
[](https://snapcraft.io/stream-sprout)
|
||||||
|
|
||||||
|
For Linux distributions that support snap packages, Stream Sprout is available from the Snap Store 🛍️
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo snap install stream-sprout
|
||||||
|
```
|
||||||
|
|
||||||
### Ubuntu
|
### Ubuntu
|
||||||
|
|
||||||
- Download the Stream Sprout .deb package from the [releases page](https://github.com/wimpysworld/stream-sprout/releases) 📦️
|
- Download the Stream Sprout .deb package from the [releases page](https://github.com/wimpysworld/stream-sprout/releases) 📦️
|
||||||
- Install it with `apt-get install ./stream-sprout_0.1.4-1_all.deb`.
|
- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`.
|
||||||
|
|
||||||
|
### Docker & Podman
|
||||||
|
|
||||||
|
#### Pull the container
|
||||||
|
|
||||||
|
The Stream Sprout container image is available from the GitHub Container Registry for amd64 and arm64.
|
||||||
|
To pull the latest container image:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull ghcr.io/wimpysworld/stream-sprout:latest-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you want a specific version:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull ghcr.io/wimpysworld/stream-sprout:0.1.5-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run the container
|
||||||
|
|
||||||
|
The `stream-sprout.yaml` configuration file will be on the host computer so you need mount a volume to access it from the container.
|
||||||
|
|
||||||
|
If you have already pulled the container image, you can run Stream Sprout with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -p 1935:1935 -it -v $PWD:/data stream-sprout --config /data/stream-sprout.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have not pulled or built the container image, you can run Stream Sprout with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -p 1935:1935 -it -v $PWD:/data ghcr.io/wimpysworld/stream-sprout:alpine-latest --config /data/stream-sprout.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `-p 1935:1935` part will expose the RTMP server port `1935` on the host computer.
|
||||||
|
- If you have configured Stream Sprout to use a different port, you should change the port number here too.
|
||||||
|
- The `-it` options will run the container in interactive mode.
|
||||||
|
- The `-v $PWD:/data` part will mount your current directory `$PWD` as `/data` within the container, allowing you to access your files using the `/data` path.
|
||||||
|
|
||||||
|
#### Build the container
|
||||||
|
|
||||||
|
Build the Stream Sprout container image:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build -t stream-sprout .
|
||||||
|
```
|
||||||
|
|
||||||
### From source
|
### From source
|
||||||
|
|
||||||
|
|
@ -105,24 +161,43 @@ If you don't specify a configuration file, Stream Sprout will look for a configu
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
|
Here's an example configuration for the Stream Sprout `server:` section.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
server:
|
server:
|
||||||
url: "rtmp://127.0.0.1:1935"
|
ip: 127.0.0.1
|
||||||
key: "create your key with uuidgen here"
|
port: 1935
|
||||||
|
app: sprout
|
||||||
|
key: create your key with uuidgen here
|
||||||
archive_stream: false
|
archive_stream: false
|
||||||
archive_path: "${HOME}/Streams"
|
archive_path: ~/Streams
|
||||||
```
|
```
|
||||||
|
|
||||||
The `server:` section is used to configure the RTMP server that Stream Sprout creates; it must be an RTMP URL.
|
The `server:` section is used to configure the RTMP server that Stream Sprout creates.
|
||||||
The default port for RTMP is `1935`, but you can use any port you like.
|
- The default `ip` address is `127.0.0.1`. Use `0.0.0.0` to allow connections to any network interface.
|
||||||
If you remotely host Stream Sprout, you should use an IP address in the `url:` that accessible by your computer that runs OBS Studio and also set `key:` to a secure value to prevent unauthorized access.
|
- If you remotely host Stream Sprout, use an IP address that is accessible by your computer that runs OBS Studio.
|
||||||
Running `uuidgen` will generate a suitable value.
|
- The default `port` for RTMP is `1935`, but you can use any port between `1024` and `65535`.
|
||||||
|
- The default `app` name is `sprout`, but you can use any name you like.
|
||||||
|
- Set `key:` to a secure value to prevent unauthorized access. Running `uuidgen` will generate a suitable value.
|
||||||
|
|
||||||
|
The IP address, port, app name and key are composed to create the RTMP URL that you will use in OBS Studio.
|
||||||
|
For example, `rtmp://ip:port/app/key`.
|
||||||
|
|
||||||
|
### 🚨 FFMPEG WILL ACCEPT ANY RTMP STREAM ON THE CORRECT PORT 🚨
|
||||||
|
|
||||||
|
**FFmpeg does not currently enforce `app` or `key` paths for its incoming RTMP server.**
|
||||||
|
**Regardless of the `app` or `key` you set in the Stream Sprout YAML FFmpeg will accept *any* incoming stream on the correct `port`**
|
||||||
|
|
||||||
|
⚠️ Do not expose the Stream Sprout RTMP server to the public internet without additional security measures ⚠️
|
||||||
|
- Consider using a VPN or SSH tunnel to secure the connection 🔐
|
||||||
|
- Or firewall the RTMP port to only allow connections from trusted IP addresses 🔥🧱
|
||||||
|
- See the [Limitations section](#limitations) section below for more information.
|
||||||
|
|
||||||
|
#### Archive streams
|
||||||
|
|
||||||
If `archive_stream:` is `true` Stream Sprout will archive the stream to disk in the directory specified by `archive_path:`.
|
If `archive_stream:` is `true` Stream Sprout will archive the stream to disk in the directory specified by `archive_path:`.
|
||||||
If `archive_path:` is not accessible, Stream Sprout will fallback to using the current working directory.
|
If `archive_path:` is not accessible, Stream Sprout will fallback to using the current working directory.
|
||||||
|
|
||||||
Here's an example configuration for the Stream Sprout `server:` section.
|
|
||||||
|
|
||||||
### Services
|
### Services
|
||||||
|
|
||||||
`services:` are arbitrarily named.
|
`services:` are arbitrarily named.
|
||||||
|
|
@ -181,24 +256,32 @@ services:
|
||||||
- Go to `Settings` > `Stream`
|
- Go to `Settings` > `Stream`
|
||||||
- Select `Custom` from the `Service` dropdown
|
- Select `Custom` from the `Service` dropdown
|
||||||
- Copy the server `url:` from your Stream Sprout configuration to the `Server` field:
|
- Copy the server `url:` from your Stream Sprout configuration to the `Server` field:
|
||||||
- `rtmp://127.0.0.1:1935` (*default*)
|
- `rtmp://127.0.0.1:1935/sprout` (*default*)
|
||||||
- Copy the `key:` (if you specified one) from your Stream Sprout configuration to the `Stream Key` field
|
- Copy the `key:` (if you specified one) from your Stream Sprout configuration to the `Stream Key` field
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Stream Sprout does not support secure RTMP (RTMPS) at this time.
|
- Protecting the Stream Sprout RTMP server with a key does not work
|
||||||
- *At least I don't think it does, but I haven't fully tested it.*
|
- FFmpeg does not currently support enforcing RTMP stream app paths or keys
|
||||||
|
- https://www.reddit.com/r/ffmpeg/comments/s4keuu/enforce_rtmp_stream_keys_and_strict_paths/
|
||||||
|
- https://patchwork.ffmpeg.org/project/ffmpeg/patch/20190925185708.70924-1-unique.will.martin@gmail.com/
|
||||||
|
```
|
||||||
|
[rtmp @ 0x2ca9be80] Unexpected stream STREAMBOMB, expecting c5b559b2-589d-4925-a28e-20d1954fd6c5
|
||||||
|
Last message repeated 1 times
|
||||||
|
```
|
||||||
- Each destination you add will increase your bandwidth requirements.
|
- Each destination you add will increase your bandwidth requirements.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
These are some of the references used to create this project:
|
These are some of the references used to create this project:
|
||||||
|
|
||||||
- https://trac.ffmpeg.org/wiki/EncodingForStreamingSites
|
- https://trac.ffmpeg.org/wiki/EncodingForStreamingSites
|
||||||
- https://ffmpeg.org/ffmpeg-protocols.html#rtmp
|
- https://ffmpeg.org/ffmpeg-protocols.html#rtmp
|
||||||
- https://ffmpeg.org/ffmpeg-formats.html#flv
|
- https://ffmpeg.org/ffmpeg-formats.html#flv
|
||||||
- https://ffmpeg.org/ffmpeg-formats.html#tee-1
|
- https://ffmpeg.org/ffmpeg-formats.html#tee-1
|
||||||
- https://obsproject.com/forum/resources/obs-studio-stream-to-multiple-platforms-or-channels-at-once.932/
|
- https://obsproject.com/forum/resources/obs-studio-stream-to-multiple-platforms-or-channels-at-once.932/
|
||||||
- https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg
|
- https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg
|
||||||
|
- https://dev.to/ajeetraina/run-ffmpeg-within-a-docker-container-a-step-by-step-guide-c0l
|
||||||
|
- https://github.com/jrottenberg/ffmpeg
|
||||||
|
|
|
||||||
20
flake.lock
generated
20
flake.lock
generated
|
|
@ -2,12 +2,12 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-schemas": {
|
"flake-schemas": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721078157,
|
"lastModified": 1761577921,
|
||||||
"narHash": "sha256-c2AZH9cOnSpPXV8Lwy19/I8EgW7G+E+Zh6YQBZZwzxI=",
|
"narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=",
|
||||||
"rev": "29e53dd33b1a38f235ef073e768c62821cb6146e",
|
"rev": "47849c7625e223d36766968cc6dc23ba0e135922",
|
||||||
"revCount": 66,
|
"revCount": 107,
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721548954,
|
"lastModified": 1769318308,
|
||||||
"narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=",
|
"narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=",
|
||||||
"rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a",
|
"rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c",
|
||||||
"revCount": 633334,
|
"revCount": 906484,
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ stdenv.mkDerivation rec {
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
install -Dm755 -t "$out/bin" stream-sprout
|
install -Dm755 -t "$out/bin" stream-sprout
|
||||||
|
wrapProgram $out/bin/stream-sprout \
|
||||||
|
--prefix PATH : "${lib.makeBinPath runtimePaths}"
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
|
||||||
53
snap/snapcraft.yaml
Normal file
53
snap/snapcraft.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
name: stream-sprout
|
||||||
|
base: core24
|
||||||
|
adopt-info: stream-sprout
|
||||||
|
summary: Restream video to multiple destinations
|
||||||
|
description: |
|
||||||
|
Restream a video source to multiple destinations such as Twitch, YouTube,
|
||||||
|
and Owncast
|
||||||
|
|
||||||
|
grade: stable
|
||||||
|
confinement: strict
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
amd64:
|
||||||
|
build-on: [ amd64 ]
|
||||||
|
build-for: [ amd64 ]
|
||||||
|
arm64:
|
||||||
|
build-on: [ arm64 ]
|
||||||
|
build-for: [arm64 ]
|
||||||
|
|
||||||
|
parts:
|
||||||
|
stream-sprout:
|
||||||
|
after: [ deps ]
|
||||||
|
plugin: dump
|
||||||
|
source: .
|
||||||
|
build-packages:
|
||||||
|
- git
|
||||||
|
override-pull: |
|
||||||
|
craftctl default
|
||||||
|
craftctl set version=$(grep "^readonly VERSION" stream-sprout | cut -d'"' -f2)-$(git rev-parse --short HEAD)
|
||||||
|
prime:
|
||||||
|
- stream-sprout
|
||||||
|
- stream-sprout.yaml.example
|
||||||
|
- LICENSE
|
||||||
|
- SECURITY.md
|
||||||
|
|
||||||
|
deps:
|
||||||
|
plugin: nil
|
||||||
|
stage-packages:
|
||||||
|
- ffmpeg
|
||||||
|
- sed
|
||||||
|
- mawk
|
||||||
|
- grep
|
||||||
|
|
||||||
|
apps:
|
||||||
|
stream-sprout:
|
||||||
|
command: stream-sprout
|
||||||
|
environment:
|
||||||
|
LD_LIBRARY_PATH: $SNAP/usr/lib/$CRAFT_ARCH_BUILD_FOR/pulseaudio:$SNAP/usr/lib/$CRAFT_ARCH_BUILD_FOR/blas:$SNAP/usr/lib/$CRAFT_ARCH_BUILD_FOR/lapack
|
||||||
|
plugs:
|
||||||
|
- home
|
||||||
|
- removable-media
|
||||||
|
- network-bind
|
||||||
|
- network
|
||||||
179
stream-sprout
179
stream-sprout
|
|
@ -5,7 +5,7 @@
|
||||||
stty -echoctl
|
stty -echoctl
|
||||||
|
|
||||||
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
|
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
|
||||||
readonly VERSION="0.1.4"
|
readonly VERSION="0.1.6"
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
echo -e " \e[31m\U26D4\e[0m Control-C"
|
echo -e " \e[31m\U26D4\e[0m Control-C"
|
||||||
|
|
@ -28,10 +28,61 @@ function show_help() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --config <path> Specify a custom config file path."
|
echo " --config <path> Specify a custom config file path."
|
||||||
|
echo " --info Show system information; useful when filing bug reports."
|
||||||
echo " --version Show version information."
|
echo " --version Show version information."
|
||||||
echo " --help Display this help message."
|
echo " --help Display this help message."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function show_info() {
|
||||||
|
local CONTAINER_ENV
|
||||||
|
local CONTAINER_RUNTIME
|
||||||
|
local CONTAINER_RUNTIMES=("docker" "lxc" "podman")
|
||||||
|
local OS_KERNEL
|
||||||
|
local PRETTY_NAME
|
||||||
|
OS_KERNEL=$(uname -s)
|
||||||
|
|
||||||
|
if [ "${OS_KERNEL}" == "Darwin" ]; then
|
||||||
|
# Get macOS product name and version using swvers
|
||||||
|
if [ -x "$(command -v sw_vers)" ]; then
|
||||||
|
PRETTY_NAME="$(sw_vers -productName) $(sw_vers -productVersion)"
|
||||||
|
else
|
||||||
|
PRETTY_NAME="macOS"
|
||||||
|
fi
|
||||||
|
elif [ -e /etc/os-release ]; then
|
||||||
|
PRETTY_NAME=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
|
||||||
|
else
|
||||||
|
PRETTY_NAME="Unknown OS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "Operating System : ${PRETTY_NAME}"
|
||||||
|
# Check for container environment
|
||||||
|
if [ "${OS_KERNEL}" == "Linux" ]; then
|
||||||
|
if [ -n "${SNAP}" ]; then
|
||||||
|
CONTAINER_ENV="Yes"
|
||||||
|
CONTAINER_RUNTIME="snapd"
|
||||||
|
else
|
||||||
|
for runtime in "${CONTAINER_RUNTIMES[@]}"; do
|
||||||
|
if grep -qa ":/${runtime}/" /proc/1/cgroup; then
|
||||||
|
CONTAINER_ENV="Yes"
|
||||||
|
CONTAINER_RUNTIME="${runtime}"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
CONTAINER_ENV="No"
|
||||||
|
CONTAINER_RUNTIME="Unknown"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo -e "Containerized : ${CONTAINER_ENV}"
|
||||||
|
if [ "${CONTAINER_ENV,,}" == "yes" ]; then
|
||||||
|
echo -e "Container Runtime: ${CONTAINER_RUNTIME}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo -e "Stream Sprout : ${VERSION}"
|
||||||
|
echo -e "awk : $(awk --version | head -n 1)"
|
||||||
|
echo -e "bash : $(bash --version | head -n 1)"
|
||||||
|
echo -e "ffmpeg : $(ffmpeg -version | head -n 1)"
|
||||||
|
}
|
||||||
|
|
||||||
function show_version() {
|
function show_version() {
|
||||||
echo -e "\e[92mStream Sprout\e[0m ${VERSION} using FFmpeg ${FFMPEG_VER}"
|
echo -e "\e[92mStream Sprout\e[0m ${VERSION} using FFmpeg ${FFMPEG_VER}"
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +97,8 @@ function parse_yaml() {
|
||||||
w='[a-zA-Z0-9_]*'
|
w='[a-zA-Z0-9_]*'
|
||||||
fs=$'\034'
|
fs=$'\034'
|
||||||
sed -ne "s|^\(${s}\):|\1|" \
|
sed -ne "s|^\(${s}\):|\1|" \
|
||||||
|
-e 's|`||g;s|\$||g;' \
|
||||||
|
-e "s|~|${HOME}|g;" \
|
||||||
-e "s|^\(${s}\)\(${w}\)${s}:${s}[\"']\(.*\)[\"']$s\$|\1${fs}\2${fs}\3|p" \
|
-e "s|^\(${s}\)\(${w}\)${s}:${s}[\"']\(.*\)[\"']$s\$|\1${fs}\2${fs}\3|p" \
|
||||||
-e "s|^\(${s}\)\(${w}\)${s}:${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "${1}" |
|
-e "s|^\(${s}\)\(${w}\)${s}:${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "${1}" |
|
||||||
awk -F"${fs}" '{
|
awk -F"${fs}" '{
|
||||||
|
|
@ -101,30 +154,94 @@ function add_service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_stream_tee() {
|
function get_stream_tee() {
|
||||||
local SERVICE=""
|
local SERVICE_ENABLED=""
|
||||||
|
local SERVICE_KEY=""
|
||||||
|
local SERVICE_NAME=""
|
||||||
|
local SERVICE_RTMP=""
|
||||||
local URI=""
|
local URI=""
|
||||||
local URI_ENV=""
|
|
||||||
local KEY_ENV=""
|
STREAM_TEE=""
|
||||||
parse_yaml "${STREAM_SPROUT_YAML}" sprout_ | grep '^sprout_services_.*_enabled=' | while read -r SERVICES; do
|
# Iterate over all the sprout_services variables
|
||||||
SERVICE=$(echo "${SERVICES}" | cut -d'_' -f3)
|
for var in "${!sprout_services@}"; do
|
||||||
ENABLED=$(echo "${SERVICES}" | cut -d'=' -f2 | tr -d \'\" )
|
# Check the variable matches the pattern: sprout_services_*_enabled
|
||||||
if [[ "${ENABLED,,}" == "true" || "${ENABLED}" == "1" ]]; then
|
if [[ "${var}" =~ ^sprout_services_.*_enabled$ ]]; then
|
||||||
echo -e " \e[35m\U1F4E1\e[0m ${SERVICE}"
|
# Derive the service name
|
||||||
# Construct the variable name
|
# - First remove `sprout_services_` prefix from the beginning of the value stored in the variable $var.
|
||||||
URI_ENV="sprout_services_${SERVICE}_rtmp_server"
|
# - Next remove the suffix `_enabled` from the end of the SERVICE_NAME variable's value.
|
||||||
KEY_ENV="sprout_services_${SERVICE}_key"
|
SERVICE_NAME="${var#sprout_services_}"
|
||||||
# Use indirect expansion to get the value
|
SERVICE_NAME="${SERVICE_NAME%_enabled}"
|
||||||
URI="${!URI_ENV}${!KEY_ENV}"
|
# Get the value of the variable $var
|
||||||
if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
|
SERVICE_ENABLED="${!var}"
|
||||||
echo -e " \e[31m\U1F6AB\e[0m ${SERVICE} is not a valid RTMP service URL"
|
if [[ "${SERVICE_ENABLED,,}" == "true" || "${SERVICE_ENABLED}" == "1" ]]; then
|
||||||
return
|
echo -e " \e[35m\U1F4E1\e[0m ${SERVICE_NAME}"
|
||||||
|
# TODO: This assumes that the RTMP URL and key are set in the YAML file.
|
||||||
|
# Construct the variable name
|
||||||
|
SERVICE_RTMP="sprout_services_${SERVICE_NAME}_rtmp_server"
|
||||||
|
SERVICE_KEY="sprout_services_${SERVICE_NAME}_key"
|
||||||
|
# Use indirect expansion to get the value
|
||||||
|
# By concatenating these two indirectly referenced values, URI
|
||||||
|
# is set to the full URI needed for streaming. For instance, if
|
||||||
|
# SERVICE_RTMP points to a variable holding rtmp://example.com/live
|
||||||
|
# and SERVICE_KEY points to a variable holding abcd1234, then URI
|
||||||
|
# would be set to rtmp://example.com/live/abcd1234.
|
||||||
|
URI="${!SERVICE_RTMP}/${!SERVICE_KEY}"
|
||||||
|
if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
|
||||||
|
echo -e " \e[31m\U1F6AB\e[0m ${SERVICE_NAME} is not a valid RTMP service URL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
add_service "${URI}"
|
||||||
fi
|
fi
|
||||||
add_service "${URI}"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
add_archive
|
add_archive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_server_url() {
|
||||||
|
local asterisks=""
|
||||||
|
local key_length=0
|
||||||
|
# Check if the sprout_server_url is set and display a deprecation notice if it is
|
||||||
|
if [ -n "${sprout_server_url}" ]; then
|
||||||
|
echo -e " \e[31m\U1F6AB\e[0m server:"
|
||||||
|
echo -e " ╰─url: in the YAML is deprecated. Please configure ip: and port: instead."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Validate the sprout_server_ip is valid
|
||||||
|
if [[ ! "${sprout_server_ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo -e " \e[33m\U26A0\e[0m server:"
|
||||||
|
echo -e " ╰─ip: in the YAML is not valid. Falling back to '127.0.0.1'."
|
||||||
|
sprout_server_ip="127.0.0.1"
|
||||||
|
fi
|
||||||
|
# Validate the sprout_server_port is valid
|
||||||
|
if [[ ! "${sprout_server_port}" =~ ^[0-9]+$ ]] || [[ "${sprout_server_port}" -lt 1024 ]] || [[ "${sprout_server_port}" -gt 65535 ]]; then
|
||||||
|
echo -e " \e[33m\U26A0\e[0m server:"
|
||||||
|
echo -e " ╰─port: in the YAML is not valid. Must be between 1024 and 65535. Falling back to '1935'."
|
||||||
|
sprout_server_port="1935"
|
||||||
|
fi
|
||||||
|
# Check that sprout_server_app is not empty
|
||||||
|
if [ -z "${sprout_server_app}" ]; then
|
||||||
|
echo -e " \e[33m\U26A0\e[0m server:"
|
||||||
|
echo -e " ╰─app: is not configured in the YAML. Falling back to 'sprout'."
|
||||||
|
sprout_server_app="sprout"
|
||||||
|
fi
|
||||||
|
# Check that sprout_server_key is not empty
|
||||||
|
if [ -z "${sprout_server_key}" ]; then
|
||||||
|
echo -e " \e[33m\U26A0\e[0m server:"
|
||||||
|
echo -e " ╰─key: is not configured in the YAML. \e[1;97mYour Stream Sprout server is unprotected.\e[0m"
|
||||||
|
fi
|
||||||
|
sprout_server_url="rtmp://${sprout_server_ip}:${sprout_server_port}/${sprout_server_app}"
|
||||||
|
if [ -n "${sprout_server_key}" ]; then
|
||||||
|
# Calculate the length of sprout_server_key
|
||||||
|
key_length=${#sprout_server_key}
|
||||||
|
# Create a string of asterisks equal to the length of sprout_server_key
|
||||||
|
asterisks=$(printf "%*s" "${key_length}" "" | tr ' ' '*')
|
||||||
|
echo -e " \e[36m\U1F310\e[0m ${sprout_server_url}/${asterisks}"
|
||||||
|
# Append the sprout_server_key to the sprout_server_url
|
||||||
|
sprout_server_url+="/${sprout_server_key}"
|
||||||
|
else
|
||||||
|
echo -e " \e[36m\U1F310\e[0m ${sprout_server_url}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function stream_details() {
|
function stream_details() {
|
||||||
local AUDIO=""
|
local AUDIO=""
|
||||||
local VIDEO=""
|
local VIDEO=""
|
||||||
|
|
@ -145,7 +262,7 @@ function stream_details() {
|
||||||
AUDIO_FREQ=$(echo "${AUDIO}" | awk -F', ' '{print $2}' | awk '{print $1 " " $2}')
|
AUDIO_FREQ=$(echo "${AUDIO}" | awk -F', ' '{print $2}' | awk '{print $1 " " $2}')
|
||||||
AUDIO_CHANNELS=$(echo "${AUDIO}" | awk -F', ' '{print $3}' | awk '{print $1}')
|
AUDIO_CHANNELS=$(echo "${AUDIO}" | awk -F', ' '{print $3}' | awk '{print $1}')
|
||||||
AUDIO_BITRATE=$(echo "${AUDIO}" | awk -F', ' '{print $5}' | awk '{print $1 " " $2}')
|
AUDIO_BITRATE=$(echo "${AUDIO}" | awk -F', ' '{print $5}' | awk '{print $1 " " $2}')
|
||||||
VIDEO_CODEC=$(echo "${VIDEO}" | awk -F': ' '{print $3}' | awk '{print $1}')
|
VIDEO_CODEC=$(echo "${VIDEO}" | awk -F', ' '{print $1}' | awk '{print $4 " " $5}')
|
||||||
VIDEO_FPS=$(echo "${VIDEO}" | awk -F', ' '{print $7}' | awk '{print $1 " " $2}')
|
VIDEO_FPS=$(echo "${VIDEO}" | awk -F', ' '{print $7}' | awk '{print $1 " " $2}')
|
||||||
VIDEO_RES=$(echo "${VIDEO}" | awk -F', ' '{print $5}' | awk '{print $1}')
|
VIDEO_RES=$(echo "${VIDEO}" | awk -F', ' '{print $5}' | awk '{print $1}')
|
||||||
VIDEO_BITRATE=$(echo "${VIDEO}" | awk -F', ' '{print $6}' | awk '{print $1 " " $2}')
|
VIDEO_BITRATE=$(echo "${VIDEO}" | awk -F', ' '{print $6}' | awk '{print $1 " " $2}')
|
||||||
|
|
@ -163,6 +280,11 @@ function banner() {
|
||||||
echo -e $'\E[38;2;249;35;101m \E[39m\E[38;2;244;27;114m \E[39m\E[38;2;238;20;126m \E[39m\E[38;2;232;14;138m \E[39m\E[38;2;224;9;151m \E[39m\E[38;2;215;5;163m \E[39m\E[38;2;206;3;175m \E[39m\E[38;2;195;2;187m \E[39m\E[38;2;184;2;198m \E[39m\E[38;2;173;3;208m \E[39m\E[38;2;161;6;217m \E[39m\E[38;2;148;10;226m \E[39m\E[38;2;136;15;233m \E[39m\E[38;2;124;21;240m \E[39m\E[38;2;111;28;245m \E[39m\E[38;2;99;36;249m \E[39m\E[38;2;87;46;252m \E[39m\E[38;2;75;56;254m \E[39m\E[38;2;64;66;254m \E[39m\E[38;2;53;78;254m \E[39m\E[38;2;43;90;252m \E[39m\E[38;2;34;102;248m \E[39m\E[38;2;26;115;244m \E[39m\E[38;2;19;127;238m \E[39m\E[38;2;13;139;231m \E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m \E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m \E[39m\E[38;2;2;198;184m \E[39m\E[38;2;3;208;172m \E[39m\E[38;2;6;218;160m \E[39m\E[38;2;10;226;147m \E[39m\E[38;2;15;234;135m \E[39m\E[38;2;21;240;123m \E[39m\E[38;2;29;245;110m \E[39m\E[38;2;37;250;98m \E[39m\E[38;2;46;252;86m|\E[39m\E[38;2;56;254;74m_\E[39m\E[38;2;67;254;63m|\E[39m\E[38;2;78;254;52m \E[39m\E[38;2;90;251;43m \E[39m\E[38;2;103;248;34m \E[39m\E[38;2;115;244;26m \E[39m\E[38;2;128;238;19m \E[39m\E[38;2;140;231;13m \E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m \E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m \E[39m\E[38;2;199;183;2m \E[39m\E[38;2;209;171;3m \E[39m\E[38;2;218;159;6m \E[39m\E[38;2;227;147;10m \E[39m\E[38;2;234;134;15m \E[39m\E[38;2;240;122;22m \E[39m\E[38;2;246;110;29m \E[39m\E[38;2;250;97;37m \E[39m\E[38;2;253;85;47m\E[39m'
|
echo -e $'\E[38;2;249;35;101m \E[39m\E[38;2;244;27;114m \E[39m\E[38;2;238;20;126m \E[39m\E[38;2;232;14;138m \E[39m\E[38;2;224;9;151m \E[39m\E[38;2;215;5;163m \E[39m\E[38;2;206;3;175m \E[39m\E[38;2;195;2;187m \E[39m\E[38;2;184;2;198m \E[39m\E[38;2;173;3;208m \E[39m\E[38;2;161;6;217m \E[39m\E[38;2;148;10;226m \E[39m\E[38;2;136;15;233m \E[39m\E[38;2;124;21;240m \E[39m\E[38;2;111;28;245m \E[39m\E[38;2;99;36;249m \E[39m\E[38;2;87;46;252m \E[39m\E[38;2;75;56;254m \E[39m\E[38;2;64;66;254m \E[39m\E[38;2;53;78;254m \E[39m\E[38;2;43;90;252m \E[39m\E[38;2;34;102;248m \E[39m\E[38;2;26;115;244m \E[39m\E[38;2;19;127;238m \E[39m\E[38;2;13;139;231m \E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m \E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m \E[39m\E[38;2;2;198;184m \E[39m\E[38;2;3;208;172m \E[39m\E[38;2;6;218;160m \E[39m\E[38;2;10;226;147m \E[39m\E[38;2;15;234;135m \E[39m\E[38;2;21;240;123m \E[39m\E[38;2;29;245;110m \E[39m\E[38;2;37;250;98m \E[39m\E[38;2;46;252;86m|\E[39m\E[38;2;56;254;74m_\E[39m\E[38;2;67;254;63m|\E[39m\E[38;2;78;254;52m \E[39m\E[38;2;90;251;43m \E[39m\E[38;2;103;248;34m \E[39m\E[38;2;115;244;26m \E[39m\E[38;2;128;238;19m \E[39m\E[38;2;140;231;13m \E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m \E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m \E[39m\E[38;2;199;183;2m \E[39m\E[38;2;209;171;3m \E[39m\E[38;2;218;159;6m \E[39m\E[38;2;227;147;10m \E[39m\E[38;2;234;134;15m \E[39m\E[38;2;240;122;22m \E[39m\E[38;2;246;110;29m \E[39m\E[38;2;250;97;37m \E[39m\E[38;2;253;85;47m\E[39m'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((BASH_VERSINFO[0] < 5)); then
|
||||||
|
echo -e " \e[31m\U1F6AB\e[0m bash 5.0 or newer is required to run this script. You have ${BASH_VERSION}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Check that ffmpeg are available on the PATH
|
# Check that ffmpeg are available on the PATH
|
||||||
if ! command -v ffmpeg &> /dev/null; then
|
if ! command -v ffmpeg &> /dev/null; then
|
||||||
echo -e " \e[31m\U1F6AB\e[0m ffmpeg is not installed. Exiting."
|
echo -e " \e[31m\U1F6AB\e[0m ffmpeg is not installed. Exiting."
|
||||||
|
|
@ -181,6 +303,9 @@ while [[ "$#" -gt 0 ]]; do
|
||||||
echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_CONFIG} was not found. Exiting."
|
echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_CONFIG} was not found. Exiting."
|
||||||
exit 1
|
exit 1
|
||||||
fi;;
|
fi;;
|
||||||
|
--info)
|
||||||
|
show_info
|
||||||
|
exit 0;;
|
||||||
--version)
|
--version)
|
||||||
show_version
|
show_version
|
||||||
exit 0;;
|
exit 0;;
|
||||||
|
|
@ -221,25 +346,14 @@ while true; do
|
||||||
eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
|
eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
|
||||||
show_version
|
show_version
|
||||||
echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
|
echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
|
||||||
if [[ ! "${sprout_server_url}" =~ ^rtmp://.* ]]; then
|
get_server_url
|
||||||
echo -e " \e[31m\U1F6AB\e[0m ${sprout_server_url} is not a valid RTMP server URL."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -en " \e[36m\U1F310\e[0m ${sprout_server_url}"
|
|
||||||
if [ -n "${sprout_server_key}" ]; then
|
|
||||||
sprout_server_url+="/${sprout_server_key}"
|
|
||||||
echo " (key required)"
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
STREAM_TEE=""
|
|
||||||
get_stream_tee
|
get_stream_tee
|
||||||
FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log)
|
FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log)
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
-hide_banner \
|
-hide_banner \
|
||||||
-flags +global_header \
|
-flags +global_header \
|
||||||
-fflags nobuffer \
|
-fflags nobuffer \
|
||||||
-listen 1 -i "${sprout_server_url}?rtmp_buffer=0&rtmp_live=live" \
|
-listen 1 -i "${sprout_server_url}" \
|
||||||
-flvflags no_duration_filesize \
|
-flvflags no_duration_filesize \
|
||||||
-c:v copy -c:a copy -map 0 \
|
-c:v copy -c:a copy -map 0 \
|
||||||
-movflags +faststart \
|
-movflags +faststart \
|
||||||
|
|
@ -288,4 +402,5 @@ while true; do
|
||||||
done
|
done
|
||||||
rename_archive
|
rename_archive
|
||||||
echo
|
echo
|
||||||
|
unset sprout_server_url
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
server:
|
server:
|
||||||
url: "rtmp://127.0.0.1:1935"
|
ip: 127.0.0.1
|
||||||
key: "create your key with uuidgen here"
|
port: 1935
|
||||||
|
app: sprout
|
||||||
|
key: create your key with uuidgen here
|
||||||
archive_stream: false
|
archive_stream: false
|
||||||
archive_path: "${HOME}/Streams"
|
archive_path: ~/Streams
|
||||||
|
|
||||||
services:
|
services:
|
||||||
trovo:
|
trovo:
|
||||||
enabled: false
|
enabled: false
|
||||||
rtmp_server: "rtmp://livepush.trovo.live/live/"
|
rtmp_server: rtmp://livepush.trovo.live/live/
|
||||||
key: "your_trovo_stream_key"
|
key: your_trovo_stream_key
|
||||||
twitch:
|
twitch:
|
||||||
enabled: true
|
enabled: true
|
||||||
rtmp_server: "rtmp://live.twitch.tv/app/"
|
rtmp_server: rtmp://live.twitch.tv/app/
|
||||||
key: "your_twitch_stream_key"
|
key: your_twitch_stream_key
|
||||||
youtube:
|
youtube:
|
||||||
enabled: true
|
enabled: true
|
||||||
rtmp_server: "rtmp://a.rtmp.youtube.com/live2/"
|
rtmp_server: rtmp://a.rtmp.youtube.com/live2/
|
||||||
key: "your_youtube_stream_key"
|
key: your_youtube_stream_key
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue