mirror of
https://github.com/wimpysworld/stream-sprout
synced 2026-03-14 14:45:50 +01:00
Compare commits
105 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 |
||
|
|
6d14c84e72 | ||
|
|
dbc72f288c | ||
|
|
5061ae32be | ||
|
|
080092e136 |
||
|
|
b2904c7481 | ||
|
|
c4148c12a1 | ||
|
|
cc928c2565 | ||
|
|
18c13219c0 | ||
|
|
6e5098bf9f | ||
|
|
71ed0d7c26 | ||
|
|
63c7aa39d7 | ||
|
|
66b39fda90 | ||
|
|
f81ff23d52 | ||
|
|
d580129636 | ||
|
|
f8b3c9c794 | ||
|
|
e42d6f9d39 |
||
|
|
c523d9bae1 | ||
|
|
8844ba12be | ||
|
|
824a7e724b | ||
|
|
5f8f70bced | ||
|
|
04db65ce11 | ||
|
|
aae7ce3afd | ||
|
|
db233cde72 | ||
|
|
a3d4e11ee5 | ||
|
|
ed4b0ad746 | ||
|
|
a77c910db2 | ||
|
|
67925af986 | ||
|
|
d1650f6d19 |
||
|
|
7c2373358b |
||
|
|
a1beb178f1 |
||
|
|
11d61aa534 | ||
|
|
dc44f33c81 | ||
|
|
2774e28e91 |
24 changed files with 890 additions and 206 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**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Stream Sprout output**
|
||||
Run `stream-sprout` and include the output of the failure below:
|
||||
**Stream Sprout output and logs**
|
||||
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>
|
||||
<summary>Stream Sprout output</summary>
|
||||
|
||||
```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>
|
||||
|
||||
**System information**
|
||||
- OS: [e.g. Ubuntu 20.04]
|
||||
- stream-sprout version: [e.g. 0.1.0]
|
||||
- FFmpeg version: [e.g. 4.2.4]
|
||||
Run `stream-sprout --info` and include the output here.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
|
|
|||
BIN
.github/demo.gif
vendored
Normal file
BIN
.github/demo.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
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
|
||||
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
|
||||
|
|
|
|||
8
.github/workflows/flake-updater.yml
vendored
8
.github/workflows/flake-updater.yml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
2
.github/workflows/lint-pr.yml
vendored
2
.github/workflows/lint-pr.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/lint-shellcheck.yml
vendored
2
.github/workflows/lint-shellcheck.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
51
.github/workflows/publish-release.yml
vendored
51
.github/workflows/publish-release.yml
vendored
|
|
@ -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,8 +37,11 @@ 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"
|
||||
DEBEMAIL: "code@wimpress.io"
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install debhelper devscripts
|
||||
|
|
@ -66,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"
|
||||
|
|
@ -76,3 +79,45 @@ jobs:
|
|||
visibility: "public"
|
||||
name: "wimpysworld/stream-sprout"
|
||||
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 }}
|
||||
72
.github/workflows/test-build-stream-sprout.yml
vendored
72
.github/workflows/test-build-stream-sprout.yml
vendored
|
|
@ -5,18 +5,24 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/*.yml
|
||||
- stream-sprout
|
||||
- debian/**
|
||||
- flake.nix
|
||||
- package.nix
|
||||
- Containerfile
|
||||
- snap/snapcraft.yaml
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/*.yml
|
||||
- stream-sprout
|
||||
- debian/**
|
||||
- flake.nix
|
||||
- package.nix
|
||||
- Containerfile
|
||||
- snap/snapcraft.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
# TODO: arm64 runner
|
||||
|
|
@ -27,9 +33,11 @@ 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"
|
||||
DEBEMAIL: "code@wimpress.io"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
|
|
@ -47,13 +55,69 @@ 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
|
||||
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)
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
We welcome contributions to Stream Sprout.
|
||||
|
||||
- Help other Quickemu users by answering questions in the [Quickemu Discussions](https://github.com/quickemu-project/quickemu/discussions) 🛟
|
||||
- Improve the documentation in [this README](https://github.com/wimpys-world/stream-sprout/edit/master/README.md) 📖
|
||||
- File bug reports and feature requests in the [Issues](https://github.com/wimpys-world/stream-sprout/issues) 📁
|
||||
- Submit [Pull requests](https://github.com/wimpys-world/stream-sprout/pulls) to fix bugs 🐞 or add new features ✨
|
||||
- Improve the documentation in [this README](https://github.com/wimpysworld/stream-sprout/edit/master/README.md) 📖
|
||||
- File bug reports and feature requests in the [Issues](https://github.com/wimpysworld/stream-sprout/issues) 📁
|
||||
- Submit [Pull requests](https://github.com/wimpysworld/stream-sprout/pulls) to fix bugs 🐞 or add new features ✨
|
||||
- Commit messages must [conform to the Conventional Commits specification](https://www.conventionalcommits.org/).
|
||||
- Tell other people about Stream Sprout 📣
|
||||
- [Sponsor the project](https://github.com/sponsors/flexiondotorg) 💖
|
||||
|
|
|
|||
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
|
||||
155
README.md
155
README.md
|
|
@ -19,9 +19,13 @@
|
|||
|
||||
Stream Sprout 🌱 is a simple, self-contained, and easy-to-use solution for streaming to multiple destinations such as Twitch, YouTube, [Owncast](https://owncast.online/) and [Peertube](https://joinpeertube.org/) 📡
|
||||
|
||||
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).
|
||||
<div align="center">
|
||||
<img src=".github/demo.gif" alt="Stream Sprout" width="505" height="415"/>
|
||||
</div>
|
||||
|
||||
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.
|
||||
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, [**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 🎞️
|
||||
The stream is received and then restreamed to the destinations you configure without modification.
|
||||
|
|
@ -30,12 +34,12 @@ Optionally you can also archive the stream to disk 💾
|
|||
While the restreaming process is lightweight, **your bandwidth requirements will increase with each destination you add.** 📈
|
||||
Ensure you have sufficient bandwidth to support the number of destinations you intend to stream to ⤴️
|
||||
|
||||
Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any other platform that supports `bash`, `ffmpeg` and `yq` 👍️
|
||||
Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any other platform that supports `bash` and `ffmpeg` 👍️
|
||||
|
||||
## 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 🖱️
|
||||
|
|
@ -47,17 +51,15 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot
|
|||
|
||||
### Debian
|
||||
|
||||
Stream Sprout depends on `yq` and that is available in bullseye-backports, bookworm, trixie and newer.
|
||||
|
||||
- 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.0-1_all.deb`.
|
||||
- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`.
|
||||
|
||||
### macOS
|
||||
|
||||
Install the Stream Sprout requirements using `brew`:
|
||||
|
||||
```shell
|
||||
brew install bash ffmpeg procps yq
|
||||
brew install bash ffmpeg
|
||||
```
|
||||
|
||||
Now clone the project:
|
||||
|
|
@ -76,18 +78,70 @@ See the flake on FlakeHub for more details:
|
|||
|
||||
- <https://flakehub.com/flake/wimpysworld/stream-sprout>
|
||||
|
||||
### Ubuntu 24.04 and newer
|
||||
### Snap
|
||||
|
||||
Stream Sprout depends on `yq` and that has been available in Ubuntu since 23.10.
|
||||
[](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
|
||||
|
||||
- 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.0-1_all.deb`.
|
||||
- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`.
|
||||
|
||||
For Ubuntu versions earlier than 24.04 you can [install Stream Sprout from source](#from-source).
|
||||
### 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
|
||||
|
||||
You need to have [FFmpeg](https://ffmpeg.org/) and [yq](https://github.com/kislyuk/yq) installed on your system.
|
||||
You need to have [FFmpeg](https://ffmpeg.org/) on your system.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/wimpysworld/stream-sprout.git
|
||||
|
|
@ -98,31 +152,52 @@ cd stream-sprout
|
|||
|
||||
Copy the [example Stream Sprout configuration](https://github.com/wimpysworld/stream-sprout/blob/main/stream-sprout.yaml.example) and edit it to suit your needs 📝
|
||||
|
||||
Stream Sprout will look for a configuration file in the following locations, in this order:
|
||||
You can specify the configuration file to use with the `--config <path>` option.
|
||||
If you don't specify a configuration file, Stream Sprout will look for a configuration file in the following locations, in this order:
|
||||
|
||||
- Current working directory `./stream-sprout.yaml`
|
||||
- XDG configuration directory `$XDG_CONFIG_HOME/stream-sprout.yaml` (*Linux*) or `~/.config/stream-sprout.yaml` (*macOS*)
|
||||
- `/etc/stream-sprout.yaml`
|
||||
|
||||
### Server
|
||||
|
||||
The `server:` section is used to configure the RTMP server that Stream Sprout creates; it must be an RTMP URL.
|
||||
The default port for RTMP is `1935`, but you can use any port you like.
|
||||
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.
|
||||
Running `uuidgen` will generate a suitable value.
|
||||
|
||||
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.
|
||||
|
||||
Here's an example configuration for the Stream Sprout `server:` section.
|
||||
|
||||
```yaml
|
||||
server:
|
||||
url: "rtmp://127.0.0.1:1935"
|
||||
key: "<insert your own key here>"
|
||||
ip: 127.0.0.1
|
||||
port: 1935
|
||||
app: sprout
|
||||
key: create your key with uuidgen here
|
||||
archive_stream: false
|
||||
archive_path: "${HOME}/Streams"
|
||||
archive_path: ~/Streams
|
||||
```
|
||||
|
||||
The `server:` section is used to configure the RTMP server that Stream Sprout creates.
|
||||
- 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, use an IP address that is accessible by your computer that runs OBS Studio.
|
||||
- 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_path:` is not accessible, Stream Sprout will fallback to using the current working directory.
|
||||
|
||||
### Services
|
||||
|
||||
`services:` are arbitrarily named.
|
||||
|
|
@ -146,7 +221,7 @@ services:
|
|||
twitch:
|
||||
enabled: true
|
||||
rtmp_server: "rtmp://live.twitch.tv/app/"
|
||||
key: "<your_stream_key>"
|
||||
key: "your_twitch_stream_key"
|
||||
```
|
||||
|
||||
#### Ingest servers
|
||||
|
|
@ -172,7 +247,7 @@ services:
|
|||
youtube:
|
||||
enabled: true
|
||||
rtmp_server: "rtmp://a.rtmp.youtube.com/live2/"
|
||||
key: "<your_stream_key>"
|
||||
key: "your_youtube_stream_key"
|
||||
```
|
||||
|
||||
## Configure OBS Studio
|
||||
|
|
@ -181,24 +256,32 @@ services:
|
|||
- Go to `Settings` > `Stream`
|
||||
- Select `Custom` from the `Service` dropdown
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
## Limitations
|
||||
|
||||
- Stream Sprout does not support secure RTMP (RTMPS) at this time.
|
||||
- *At least I don't think it does, but I haven't fully tested it.*
|
||||
- Protecting the Stream Sprout RTMP server with a key does not work
|
||||
- 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.
|
||||
|
||||
## References
|
||||
|
||||
These are some of the references used to create this project:
|
||||
|
||||
- https://trac.ffmpeg.org/wiki/EncodingForStreamingSites
|
||||
- https://ffmpeg.org/ffmpeg-protocols.html#rtmp
|
||||
- https://ffmpeg.org/ffmpeg-formats.html#flv
|
||||
- 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://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg
|
||||
- https://trac.ffmpeg.org/wiki/EncodingForStreamingSites
|
||||
- https://ffmpeg.org/ffmpeg-protocols.html#rtmp
|
||||
- https://ffmpeg.org/ffmpeg-formats.html#flv
|
||||
- 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://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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Here are the versions of Stream Sprout currently being supported with security u
|
|||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a vulnerability then [file an issue](https://github.com/wimpys-world/stream-sprout/issues/new) and click *Report a vulnerability*.
|
||||
If you discover a vulnerability then [file an issue](https://github.com/wimpysworld/stream-sprout/issues/new) and click *Report a vulnerability*.
|
||||
|
||||
- Stream Sprout is a spare-time hobby project.
|
||||
- We do not have SLAs for responding to security issues.
|
||||
|
|
|
|||
30
debian/control
vendored
30
debian/control
vendored
|
|
@ -5,9 +5,9 @@ Maintainer: Martin Wimpress <code@wimpress.io>
|
|||
Build-Depends:
|
||||
debhelper-compat (= 12),
|
||||
Standards-Version: 4.5.1
|
||||
Homepage: https://github.com/wimpys-world/stream-sprout
|
||||
Vcs-Browser: https://github.com/wimpys-world/stream-sprout
|
||||
Vcs-Git: https://github.com/wimpys-world/stream-sprout.git
|
||||
Homepage: https://github.com/wimpysworld/stream-sprout
|
||||
Vcs-Browser: https://github.com/wimpysworld/stream-sprout
|
||||
Vcs-Git: https://github.com/wimpysworld/stream-sprout.git
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: stream-sprout
|
||||
|
|
@ -15,11 +15,25 @@ Architecture: all
|
|||
Depends:
|
||||
coreutils,
|
||||
ffmpeg,
|
||||
procps,
|
||||
yq,
|
||||
grep,
|
||||
mawk,
|
||||
sed,
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Description: Restream a video source to multiple destinations such as Twitch, YouTube, and Owncast.
|
||||
Stream Sprout uses FFmpeg to re-stream a video source to multiple destinations
|
||||
such as Twitch, YouTube, and Owncast.
|
||||
Description: Restream a video source to multiple destinations such as Twitch, YouTube, Owncast and Peertube.
|
||||
Stream Sprout is a simple, self-contained, and easy-to-use solution for
|
||||
streaming to multiple destinations such as Twitch, YouTube, Owncast and Peertube
|
||||
.
|
||||
It uses FFmpeg 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.
|
||||
.
|
||||
Stream Sprout is configured with a simple YAML file and designed to be run on
|
||||
the same computer as your OBS Studio instance (it can be run remotely too) and
|
||||
does not require root privileges.
|
||||
.
|
||||
There is no transcoding or processing of the video stream. The stream is
|
||||
received and then restreamed to the destinations you configure without
|
||||
modification. Optionally you can also archive the stream to disk.
|
||||
|
|
|
|||
57
debian/copyright
vendored
57
debian/copyright
vendored
|
|
@ -1,45 +1,40 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: stream-sprout
|
||||
Upstream-Contact: Martin Wimpress <code@wimpress.io>
|
||||
Source: https://github.com/wimpys-world/stream-sprout
|
||||
Source: https://github.com/wimpysworld/stream-sprout
|
||||
|
||||
Files: *
|
||||
Copyright: 2024 Martin Wimpress <code@wimpress.io>
|
||||
License: APACHE-2.0
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
License: Apache-2.0
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
.
|
||||
On Debian systems, the complete text of the Apache License, Version 2
|
||||
can be found in "/usr/share/common-licenses/Apache-2.0".
|
||||
|
||||
# If you want to use GPL v2 or later for the /debian/* files use
|
||||
# the following clauses, or change it to suit. Delete these two lines
|
||||
Files: debian/*
|
||||
Copyright: 2024 Martin Wimpress <code@wimpress.io>
|
||||
License: GPL-2+
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
License: Apache-2.0
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
||||
On Debian systems, the complete text of the Apache License, Version 2
|
||||
can be found in "/usr/share/common-licenses/Apache-2.0".
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
}:
|
||||
mkShell {
|
||||
packages = with pkgs; ([
|
||||
coreutils-full
|
||||
ffmpeg-headless
|
||||
procps
|
||||
yq
|
||||
gawk
|
||||
gnugrep
|
||||
gnused
|
||||
]);
|
||||
|
||||
shellHook = ''
|
||||
|
|
|
|||
20
flake.lock
generated
20
flake.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
15
package.nix
15
package.nix
|
|
@ -2,15 +2,20 @@
|
|||
, installShellFiles
|
||||
, makeWrapper
|
||||
, stdenv
|
||||
, coreutils-full
|
||||
, ffmpeg-headless
|
||||
, gawk
|
||||
, gnugrep
|
||||
, gnused
|
||||
, procps
|
||||
, yq
|
||||
}:
|
||||
let
|
||||
runtimePaths = [
|
||||
coreutils-full
|
||||
ffmpeg-headless
|
||||
procps
|
||||
yq
|
||||
gawk
|
||||
gnugrep
|
||||
gnused
|
||||
];
|
||||
versionMatches =
|
||||
builtins.match ''
|
||||
|
|
@ -29,12 +34,14 @@ stdenv.mkDerivation rec {
|
|||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -Dm755 -t "$out/bin" stream-sprout
|
||||
wrapProgram $out/bin/stream-sprout \
|
||||
--prefix PATH : "${lib.makeBinPath runtimePaths}"
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Re-stream a video source to multiple destinations such as Twitch, YouTube, and Owncast.";
|
||||
homepage = "https://github.com/wimpys-world/stream-sprout";
|
||||
homepage = "https://github.com/wimpysworld/stream-sprout";
|
||||
mainProgram = "stream-sprout";
|
||||
license = lib.licenses.asl20;
|
||||
maintainers = with lib.maintainers; [ flexiondotorg ];
|
||||
|
|
|
|||
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
|
||||
425
stream-sprout
425
stream-sprout
|
|
@ -1,51 +1,144 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2154
|
||||
|
||||
# Disable echo of control characters like ^C
|
||||
stty -echoctl
|
||||
|
||||
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
|
||||
readonly VERSION="0.1.1"
|
||||
readonly VERSION="0.1.6"
|
||||
|
||||
function ctrl_c() {
|
||||
echo " - Trapped: CTRL-C"
|
||||
pkill ffmpeg
|
||||
function cleanup() {
|
||||
echo -e " \e[31m\U26D4\e[0m Control-C"
|
||||
sleep 0.25
|
||||
if kill -0 "${FFMPEG_PID}" 2>/dev/null; then
|
||||
echo -e " \e[31m\U1F480\e[0m FFmpeg process (${FFMPEG_PID}) has been terminated"
|
||||
kill "${FFMPEG_PID}"
|
||||
else
|
||||
echo -e " \e[31m\U23F9\e[0m FFmpeg process (${FFMPEG_PID}) has ended"
|
||||
fi
|
||||
rename_archive
|
||||
exit
|
||||
}
|
||||
|
||||
function get_archive_path() {
|
||||
local ARCHIVE_PATH=""
|
||||
ARCHIVE_PATH=$(yq e ".server.archive_path" "${STREAM_SPROUT_CONFIG}")
|
||||
# Expand any environment variables in the path
|
||||
ARCHIVE_PATH=$(eval echo "${ARCHIVE_PATH}")
|
||||
if [ -z "${ARCHIVE_PATH}" ]; then
|
||||
echo "./"
|
||||
# Function to display help
|
||||
function show_help() {
|
||||
echo "Restream a video source to multiple destinations such as Twitch, YouTube, Owncast and Peertube."
|
||||
echo ""
|
||||
echo "Usage: $(basename "${0}") [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
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 " --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
|
||||
mkdir -p "${ARCHIVE_PATH}" 2> /dev/null
|
||||
echo "${ARCHIVE_PATH}"
|
||||
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() {
|
||||
echo -e "\e[92mStream Sprout\e[0m ${VERSION} using FFmpeg ${FFMPEG_VER}"
|
||||
}
|
||||
|
||||
# https://stackoverflow.com/questions/5014632/how-can-i-parse-a-yaml-file-from-a-linux-shell-script
|
||||
function parse_yaml() {
|
||||
local prefix="${2}"
|
||||
local s=""
|
||||
local w=""
|
||||
local fs=""
|
||||
s='[[:space:]]*'
|
||||
w='[a-zA-Z0-9_]*'
|
||||
fs=$'\034'
|
||||
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" "${1}" |
|
||||
awk -F"${fs}" '{
|
||||
indent = length($1)/2;
|
||||
vname[indent] = $2;
|
||||
for (i in vname) {if (i > indent) {delete vname[i]}}
|
||||
if (length($3) > 0) {
|
||||
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
|
||||
gsub(/^[ \t]+/, "", $3); gsub(/[ \t]+$/, "", $3);
|
||||
gsub(/\s*#.*$/, "", $3);
|
||||
printf("%s%s%s=\"%s\"\n", "'"${prefix}"'",vn, $2, $3);
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
function rename_archive() {
|
||||
local STAMP=""
|
||||
# If there is a stream file, then rename it to the current date and time
|
||||
if [ -e "${ARCHIVE_PATH}/${ARCHIVE_TEMP}" ]; then
|
||||
if [ -e "${sprout_server_archive_path}/${sprout_server_archive_temp}" ]; then
|
||||
STAMP=$(date +%Y%m%d_%H%M%S)
|
||||
echo " - Rename: ${ARCHIVE_PATH}/${ARCHIVE_TEMP} to ${ARCHIVE_PATH}/stream-sprout-${STAMP}.mkv"
|
||||
mv "${ARCHIVE_PATH}/${ARCHIVE_TEMP}" "${ARCHIVE_PATH}/stream-sprout-${STAMP}.mkv"
|
||||
echo -e " \U1F500 ${sprout_server_archive_path}/${sprout_server_archive_temp}"
|
||||
echo -e " \U21AA ${sprout_server_archive_path}/stream-sprout-${STAMP}.mkv"
|
||||
mv "${sprout_server_archive_path}/${sprout_server_archive_temp}" "${sprout_server_archive_path}/stream-sprout-${STAMP}.mkv"
|
||||
fi
|
||||
}
|
||||
|
||||
function add_archive() {
|
||||
local ARCHIVE_ENABLED=""
|
||||
ARCHIVE_PATH="$(get_archive_path)"
|
||||
ARCHIVE_TEMP="stream-temp-$(date +%s%N).mkv"
|
||||
|
||||
sprout_server_archive_temp="stream-temp-$(date +%s%N).mkv"
|
||||
# Check if recording is enabled in the YAML configuration
|
||||
ARCHIVE_ENABLED=$(yq e ".server.archive_stream" "${STREAM_SPROUT_CONFIG}")
|
||||
if [[ "${ARCHIVE_ENABLED,,}" == "true" || "${ARCHIVE_ENABLED}" == "1" ]]; then
|
||||
echo " - Archive: ${ARCHIVE_PATH}/${ARCHIVE_TEMP}"
|
||||
if [[ "${sprout_server_archive_stream,,}" == "true" || "${sprout_server_archive_stream}" == "1" ]]; then
|
||||
if [ -z "${sprout_server_archive_path}" ]; then
|
||||
sprout_server_archive_path="$(dirname "${PWD}")"
|
||||
else
|
||||
mkdir -p "${sprout_server_archive_path}" 2>/dev/null
|
||||
fi
|
||||
echo -e " \e[34m\U1F4BE\e[0m ${sprout_server_archive_path}/${sprout_server_archive_temp}"
|
||||
if [ -n "${STREAM_TEE}" ]; then
|
||||
STREAM_TEE+="|"
|
||||
fi
|
||||
STREAM_TEE+="[f=matroska]${ARCHIVE_PATH}/${ARCHIVE_TEMP}"
|
||||
STREAM_TEE+="[f=matroska]${sprout_server_archive_path}/${sprout_server_archive_temp}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -62,88 +155,252 @@ function add_service() {
|
|||
|
||||
function get_stream_tee() {
|
||||
local SERVICE_ENABLED=""
|
||||
local SERVICES=""
|
||||
local SERVICE_KEY=""
|
||||
local SERVICE_NAME=""
|
||||
local SERVICE_RTMP=""
|
||||
local URI=""
|
||||
|
||||
# Extract services from the YAML
|
||||
SERVICES=$(yq e '.services | keys | .[]' "${STREAM_SPROUT_CONFIG}")
|
||||
|
||||
# Iterate over each service
|
||||
for SERVICE in ${SERVICES}; do
|
||||
# Check if the service is enabled in the YAML configuration
|
||||
SERVICE_ENABLED=$(yq e ".services.${SERVICE}.enabled" "${STREAM_SPROUT_CONFIG}")
|
||||
if [[ "${SERVICE_ENABLED,,}" == "true" || "${SERVICE_ENABLED}" == "1" ]]; then
|
||||
echo " - Service: ${SERVICE}"
|
||||
URI=$(yq e ".services.${SERVICE}.rtmp_server" "${STREAM_SPROUT_CONFIG}")
|
||||
if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
|
||||
echo " - Invalid URL: ${SERVICE} is not a valid RTMP URL."
|
||||
return
|
||||
STREAM_TEE=""
|
||||
# Iterate over all the sprout_services variables
|
||||
for var in "${!sprout_services@}"; do
|
||||
# Check the variable matches the pattern: sprout_services_*_enabled
|
||||
if [[ "${var}" =~ ^sprout_services_.*_enabled$ ]]; then
|
||||
# Derive the service name
|
||||
# - First remove `sprout_services_` prefix from the beginning of the value stored in the variable $var.
|
||||
# - Next remove the suffix `_enabled` from the end of the SERVICE_NAME variable's value.
|
||||
SERVICE_NAME="${var#sprout_services_}"
|
||||
SERVICE_NAME="${SERVICE_NAME%_enabled}"
|
||||
# Get the value of the variable $var
|
||||
SERVICE_ENABLED="${!var}"
|
||||
if [[ "${SERVICE_ENABLED,,}" == "true" || "${SERVICE_ENABLED}" == "1" ]]; then
|
||||
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
|
||||
URI+=$(yq e ".services.${SERVICE}.key" "${STREAM_SPROUT_CONFIG}")
|
||||
add_service "${URI}"
|
||||
fi
|
||||
done
|
||||
add_archive
|
||||
}
|
||||
|
||||
# Check that ffmpeg and yq are available on the PATH
|
||||
for CMD in ffmpeg pkill yq; do
|
||||
if ! command -v "${CMD}" &> /dev/null; then
|
||||
echo "ERROR! ${CMD} is not installed. Exiting."
|
||||
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() {
|
||||
local AUDIO=""
|
||||
local VIDEO=""
|
||||
local AUDIO_CODEC=""
|
||||
local AUDIO_BITRATE=""
|
||||
local AUDIO_FREQ=""
|
||||
local AUDIO_CHANNELS=""
|
||||
local VIDEO_CODEC=""
|
||||
local VIDEO_FPS=""
|
||||
local VIDEO_RES=""
|
||||
local VIDEO_BITRATE=""
|
||||
|
||||
AUDIO="$(grep "Audio:" "${FFMPEG_LOG}" | head -n 1)"
|
||||
VIDEO="$(grep "Video:" "${FFMPEG_LOG}" | head -n 1)"
|
||||
|
||||
# Correcting the parsing to accurately extract the required information
|
||||
AUDIO_CODEC=$(echo "${AUDIO}" | awk -F', ' '{print $1}' | awk '{print $4 " " $5}')
|
||||
AUDIO_FREQ=$(echo "${AUDIO}" | awk -F', ' '{print $2}' | awk '{print $1 " " $2}')
|
||||
AUDIO_CHANNELS=$(echo "${AUDIO}" | awk -F', ' '{print $3}' | awk '{print $1}')
|
||||
AUDIO_BITRATE=$(echo "${AUDIO}" | awk -F', ' '{print $5}' | awk '{print $1 " " $2}')
|
||||
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_RES=$(echo "${VIDEO}" | awk -F', ' '{print $5}' | awk '{print $1}')
|
||||
VIDEO_BITRATE=$(echo "${VIDEO}" | awk -F', ' '{print $6}' | awk '{print $1 " " $2}')
|
||||
|
||||
echo -e " \e[32m\U1F441\e[0m FFmpeg detected a new stream"
|
||||
echo -e " ├─ Audio: ${AUDIO_FREQ} ${AUDIO_CODEC} in ${AUDIO_CHANNELS^} ~${AUDIO_BITRATE}"
|
||||
echo -e " ╰─ Video: ${VIDEO_RES} ${VIDEO_CODEC} at ${VIDEO_FPS} ~${VIDEO_BITRATE}"
|
||||
}
|
||||
|
||||
function banner() {
|
||||
echo -e $'\E[38;2;254;75;55m \E[39m\E[38;2;254;64;66m_\E[39m\E[38;2;254;54;77m_\E[39m\E[38;2;252;44;89m_\E[39m\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'
|
||||
echo -e $'\E[38;2;254;64;66m|\E[39m\E[38;2;254;54;77m \E[39m\E[38;2;252;44;89m \E[39m\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'
|
||||
echo -e $'\E[38;2;254;54;77m|\E[39m\E[38;2;252;44;89m_\E[39m\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'
|
||||
echo -e $'\E[38;2;252;44;89m|\E[39m\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'
|
||||
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
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo -e " \e[31m\U1F6AB\e[0m ffmpeg is not installed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FFMPEG_VER="$(ffmpeg -version | head -n 1 | cut -d' ' -f3)"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "${1}" in
|
||||
--config)
|
||||
STREAM_SPROUT_CONFIG="${2}"
|
||||
shift
|
||||
if [ ! -f "${STREAM_SPROUT_CONFIG}" ]; then
|
||||
echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_CONFIG} was not found. Exiting."
|
||||
exit 1
|
||||
fi;;
|
||||
--info)
|
||||
show_info
|
||||
exit 0;;
|
||||
--version)
|
||||
show_version
|
||||
exit 0;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0;;
|
||||
*)
|
||||
echo "Unknown option: ${1}"
|
||||
show_help
|
||||
exit 1;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Check in the current working directory
|
||||
if [ -f "./${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="./${STREAM_SPROUT_YAML}"
|
||||
# Check in the user's home directory, considering XDG on Linux and compatibility with macOS
|
||||
elif [ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}"
|
||||
# Check in /etc
|
||||
elif [ -f "/etc/${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="/etc/${STREAM_SPROUT_YAML}"
|
||||
else
|
||||
echo "ERROR: ${STREAM_SPROUT_YAML} was not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the file is valid YAML
|
||||
if ! yq eval '.' "${STREAM_SPROUT_CONFIG}" &>/dev/null; then
|
||||
echo "ERROR: ${STREAM_SPROUT_CONFIG} is not valid YAML."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# trap ctrl-c and call ctrl_c() to clean up
|
||||
trap ctrl_c INT
|
||||
|
||||
while true; do
|
||||
echo "Stream Sprout v${VERSION} using ${STREAM_SPROUT_CONFIG}"
|
||||
SERVER_URL=$(yq e ".server.url" "${STREAM_SPROUT_CONFIG}")
|
||||
SERVER_KEY=$(yq e ".server.key" "${STREAM_SPROUT_CONFIG}")
|
||||
if [[ ! "${SERVER_URL}" =~ ^rtmp://.* ]]; then
|
||||
echo " - Invalid URL: ${SERVER_URL} is not a valid RTMP URL."
|
||||
# Check if a custom config path was not provided
|
||||
if [ -z "${STREAM_SPROUT_CONFIG}" ]; then
|
||||
# Check in the current working directory
|
||||
if [ -f "./${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="./${STREAM_SPROUT_YAML}"
|
||||
# Check in the user's home directory, considering XDG on Linux and compatibility with macOS
|
||||
elif [ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}"
|
||||
# Check in /etc
|
||||
elif [ -f "/etc/${STREAM_SPROUT_YAML}" ]; then
|
||||
STREAM_SPROUT_CONFIG="/etc/${STREAM_SPROUT_YAML}"
|
||||
else
|
||||
echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_YAML} was not found. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo -n " - Server: ${SERVER_URL}"
|
||||
if [ "${SERVER_KEY}" != "null" ]; then
|
||||
SERVER_URL+="/${SERVER_KEY}"
|
||||
echo " (key required)"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
STREAM_TEE=""
|
||||
fi
|
||||
|
||||
banner
|
||||
|
||||
# trap relevant signals and call cleanup()
|
||||
trap cleanup INT QUIT TERM
|
||||
|
||||
while true; do
|
||||
eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
|
||||
show_version
|
||||
echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
|
||||
get_server_url
|
||||
get_stream_tee
|
||||
FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log)
|
||||
ffmpeg \
|
||||
-hide_banner \
|
||||
-flags +global_header \
|
||||
-fflags nobuffer \
|
||||
-listen 1 -i "${SERVER_URL}?rtmp_buffer=0&rtmp_live=live" \
|
||||
-listen 1 -i "${sprout_server_url}" \
|
||||
-flvflags no_duration_filesize \
|
||||
-c:v copy -c:a copy -map 0 \
|
||||
-movflags +faststart \
|
||||
-f tee -use_fifo 1 "${STREAM_TEE}" 2>/dev/null
|
||||
echo " - Server: Stopping..."
|
||||
-f tee -use_fifo 1 "${STREAM_TEE}" >"${FFMPEG_LOG}" 2>&1 &
|
||||
|
||||
# Capture the PID of the ffmpeg process
|
||||
FFMPEG_PID=$!
|
||||
|
||||
echo -e " \U2B07 FFmpeg process (${FFMPEG_PID}) logging to ${FFMPEG_LOG}"
|
||||
|
||||
COUNTER=0
|
||||
# 0 for standing-by
|
||||
# 1 for streaming
|
||||
STREAMING_STATUS=0
|
||||
|
||||
# Monitor the FFmpeg process
|
||||
while sleep 1; do
|
||||
STAMP="[$(date +%H:%M:%S)]"
|
||||
if ! kill -0 "${FFMPEG_PID}" 2>/dev/null; then
|
||||
echo -e " \e[31m\U23F9\e[0m FFmpeg has stopped"
|
||||
break
|
||||
else
|
||||
if grep "Input #0, flv, from 'rtmp://" "${FFMPEG_LOG}" > /dev/null; then
|
||||
NEW_STATUS=1
|
||||
else
|
||||
NEW_STATUS=0
|
||||
fi
|
||||
|
||||
# Check if status changed or if it's time to log the status again
|
||||
if [ ${NEW_STATUS} -ne ${STREAMING_STATUS} ] || (( COUNTER % 30 == 0 )); then
|
||||
# If the status has changed, then show the details
|
||||
if [ ${NEW_STATUS} -ne ${STREAMING_STATUS} ]; then
|
||||
stream_details
|
||||
fi
|
||||
|
||||
if [ ${NEW_STATUS} -eq 1 ]; then
|
||||
echo -e " \e[32m\U25B6\e[0m FFmpeg is streaming ${STAMP}"
|
||||
else
|
||||
echo -e " \e[33m\U23F8\e[0m FFmpeg is standing-by ${STAMP}"
|
||||
fi
|
||||
# Update the current status
|
||||
STREAMING_STATUS=${NEW_STATUS}
|
||||
fi
|
||||
((COUNTER++))
|
||||
fi
|
||||
done
|
||||
rename_archive
|
||||
echo
|
||||
unset sprout_server_url
|
||||
done
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
server:
|
||||
url: "rtmp://127.0.0.1:1935"
|
||||
key: "<insert your own key here>"
|
||||
ip: 127.0.0.1
|
||||
port: 1935
|
||||
app: sprout
|
||||
key: create your key with uuidgen here
|
||||
archive_stream: false
|
||||
archive_path: "${HOME}/Streams"
|
||||
archive_path: ~/Streams
|
||||
|
||||
services:
|
||||
trovo:
|
||||
enabled: false
|
||||
rtmp_server: "rtmp://livepush.trovo.live/live/"
|
||||
key: "<your_stream_key>"
|
||||
rtmp_server: rtmp://livepush.trovo.live/live/
|
||||
key: your_trovo_stream_key
|
||||
twitch:
|
||||
enabled: true
|
||||
rtmp_server: "rtmp://live.twitch.tv/app/"
|
||||
key: "<your_stream_key>"
|
||||
rtmp_server: rtmp://live.twitch.tv/app/
|
||||
key: your_twitch_stream_key
|
||||
youtube:
|
||||
enabled: true
|
||||
rtmp_server: "rtmp://a.rtmp.youtube.com/live2/"
|
||||
key: "<your_stream_key>"
|
||||
rtmp_server: rtmp://a.rtmp.youtube.com/live2/
|
||||
key: your_youtube_stream_key
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue