mirror of
https://github.com/wimpysworld/stream-sprout
synced 2026-03-14 14:45:50 +01:00
Compare commits
88 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 |
21 changed files with 793 additions and 131 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/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
|
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
|
||||||
143
README.md
143
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/) 📡
|
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 🎞️
|
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.
|
||||||
|
|
@ -35,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 🖱️
|
||||||
|
|
@ -48,14 +52,14 @@ 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.3-1_all.deb`.
|
- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`.
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
Install the Stream Sprout requirements using `brew`:
|
Install the Stream Sprout requirements using `brew`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew install bash ffmpeg procps
|
brew install bash ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
Now clone the project:
|
Now clone the project:
|
||||||
|
|
@ -74,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.3-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
|
||||||
|
|
||||||
|
|
@ -92,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 📝
|
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`
|
- Current working directory `./stream-sprout.yaml`
|
||||||
- XDG configuration directory `$XDG_CONFIG_HOME/stream-sprout.yaml` (*Linux*) or `~/.config/stream-sprout.yaml` (*macOS*)
|
- XDG configuration directory `$XDG_CONFIG_HOME/stream-sprout.yaml` (*Linux*) or `~/.config/stream-sprout.yaml` (*macOS*)
|
||||||
- `/etc/stream-sprout.yaml`
|
- `/etc/stream-sprout.yaml`
|
||||||
|
|
||||||
### Server
|
### 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.
|
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.
|
||||||
|
- 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
|
||||||
|
|
||||||
`services:` are arbitrarily named.
|
`services:` are arbitrarily named.
|
||||||
|
|
@ -175,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
|
||||||
|
|
|
||||||
1
debian/control
vendored
1
debian/control
vendored
|
|
@ -17,7 +17,6 @@ Depends:
|
||||||
ffmpeg,
|
ffmpeg,
|
||||||
grep,
|
grep,
|
||||||
mawk,
|
mawk,
|
||||||
procps,
|
|
||||||
sed,
|
sed,
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
}:
|
}:
|
||||||
mkShell {
|
mkShell {
|
||||||
packages = with pkgs; ([
|
packages = with pkgs; ([
|
||||||
|
coreutils-full
|
||||||
ffmpeg-headless
|
ffmpeg-headless
|
||||||
|
gawk
|
||||||
gnugrep
|
gnugrep
|
||||||
gnused
|
gnused
|
||||||
mawk
|
|
||||||
procps
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
||||||
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",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
, installShellFiles
|
, installShellFiles
|
||||||
, makeWrapper
|
, makeWrapper
|
||||||
, stdenv
|
, stdenv
|
||||||
|
, coreutils-full
|
||||||
, ffmpeg-headless
|
, ffmpeg-headless
|
||||||
, gawk
|
, gawk
|
||||||
, gnugrep
|
, gnugrep
|
||||||
|
|
@ -10,11 +11,11 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
runtimePaths = [
|
runtimePaths = [
|
||||||
|
coreutils-full
|
||||||
ffmpeg-headless
|
ffmpeg-headless
|
||||||
gawk
|
gawk
|
||||||
gnugrep
|
gnugrep
|
||||||
gnused
|
gnused
|
||||||
procps
|
|
||||||
];
|
];
|
||||||
versionMatches =
|
versionMatches =
|
||||||
builtins.match ''
|
builtins.match ''
|
||||||
|
|
@ -33,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
|
||||||
368
stream-sprout
368
stream-sprout
|
|
@ -1,16 +1,92 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
|
|
||||||
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
|
# Disable echo of control characters like ^C
|
||||||
readonly VERSION="0.1.3"
|
stty -echoctl
|
||||||
|
|
||||||
function ctrl_c() {
|
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
|
||||||
echo " - Trapped: CTRL-C"
|
readonly VERSION="0.1.6"
|
||||||
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
|
rename_archive
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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
|
# https://stackoverflow.com/questions/5014632/how-can-i-parse-a-yaml-file-from-a-linux-shell-script
|
||||||
function parse_yaml() {
|
function parse_yaml() {
|
||||||
local prefix="${2}"
|
local prefix="${2}"
|
||||||
|
|
@ -21,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}" '{
|
||||||
|
|
@ -41,7 +119,8 @@ function rename_archive() {
|
||||||
# If there is a stream file, then rename it to the current date and time
|
# If there is a stream file, then rename it to the current date and time
|
||||||
if [ -e "${sprout_server_archive_path}/${sprout_server_archive_temp}" ]; then
|
if [ -e "${sprout_server_archive_path}/${sprout_server_archive_temp}" ]; then
|
||||||
STAMP=$(date +%Y%m%d_%H%M%S)
|
STAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
echo " - Rename: ${sprout_server_archive_path}/${sprout_server_archive_temp} to ${sprout_server_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"
|
mv "${sprout_server_archive_path}/${sprout_server_archive_temp}" "${sprout_server_archive_path}/stream-sprout-${STAMP}.mkv"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -53,9 +132,9 @@ function add_archive() {
|
||||||
if [ -z "${sprout_server_archive_path}" ]; then
|
if [ -z "${sprout_server_archive_path}" ]; then
|
||||||
sprout_server_archive_path="$(dirname "${PWD}")"
|
sprout_server_archive_path="$(dirname "${PWD}")"
|
||||||
else
|
else
|
||||||
mkdir -p "${sprout_server_archive_path}" 2> /dev/null
|
mkdir -p "${sprout_server_archive_path}" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
echo " - Archive: ${sprout_server_archive_path}/${sprout_server_archive_temp}"
|
echo -e " \e[34m\U1F4BE\e[0m ${sprout_server_archive_path}/${sprout_server_archive_temp}"
|
||||||
if [ -n "${STREAM_TEE}" ]; then
|
if [ -n "${STREAM_TEE}" ]; then
|
||||||
STREAM_TEE+="|"
|
STREAM_TEE+="|"
|
||||||
fi
|
fi
|
||||||
|
|
@ -75,82 +154,253 @@ 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 " - Service: ${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 " - Invalid URL: ${SERVICE} is not a valid RTMP 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
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check that ffmpeg and pkill are available on the PATH
|
function get_server_url() {
|
||||||
for CMD in ffmpeg pkill; do
|
local asterisks=""
|
||||||
if ! command -v "${CMD}" &> /dev/null; then
|
local key_length=0
|
||||||
echo "ERROR! ${CMD} is not installed. Exiting."
|
# 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
# Check in the current working directory
|
function stream_details() {
|
||||||
if [ -f "./${STREAM_SPROUT_YAML}" ]; then
|
local AUDIO=""
|
||||||
STREAM_SPROUT_CONFIG="./${STREAM_SPROUT_YAML}"
|
local VIDEO=""
|
||||||
# Check in the user's home directory, considering XDG on Linux and compatibility with macOS
|
local AUDIO_CODEC=""
|
||||||
elif [ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}" ]; then
|
local AUDIO_BITRATE=""
|
||||||
STREAM_SPROUT_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}"
|
local AUDIO_FREQ=""
|
||||||
# Check in /etc
|
local AUDIO_CHANNELS=""
|
||||||
elif [ -f "/etc/${STREAM_SPROUT_YAML}" ]; then
|
local VIDEO_CODEC=""
|
||||||
STREAM_SPROUT_CONFIG="/etc/${STREAM_SPROUT_YAML}"
|
local VIDEO_FPS=""
|
||||||
else
|
local VIDEO_RES=""
|
||||||
echo "ERROR: ${STREAM_SPROUT_YAML} was not found."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval "$(parse_yaml "${STREAM_SPROUT_YAML}" sprout_)"
|
# 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
|
||||||
|
|
||||||
# trap ctrl-c and call ctrl_c() to clean up
|
FFMPEG_VER="$(ffmpeg -version | head -n 1 | cut -d' ' -f3)"
|
||||||
trap ctrl_c INT
|
|
||||||
|
|
||||||
while true; do
|
# Parse command line arguments
|
||||||
echo "Stream Sprout v${VERSION} using ${STREAM_SPROUT_CONFIG}"
|
while [[ "$#" -gt 0 ]]; do
|
||||||
if [[ ! "${sprout_server_url}" =~ ^rtmp://.* ]]; then
|
case "${1}" in
|
||||||
echo " - Invalid URL: ${sprout_server_url} is not a valid RTMP URL."
|
--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 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo -n " - Server: ${sprout_server_url}"
|
fi
|
||||||
if [ -n "${sprout_server_key}" ]; then
|
|
||||||
sprout_server_url+="/${sprout_server_key}"
|
banner
|
||||||
echo " (key required)"
|
|
||||||
else
|
# trap relevant signals and call cleanup()
|
||||||
echo ""
|
trap cleanup INT QUIT TERM
|
||||||
fi
|
|
||||||
STREAM_TEE=""
|
while true; do
|
||||||
|
eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
|
||||||
|
show_version
|
||||||
|
echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
|
||||||
|
get_server_url
|
||||||
get_stream_tee
|
get_stream_tee
|
||||||
|
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 \
|
||||||
-f tee -use_fifo 1 "${STREAM_TEE}" 2>/dev/null
|
-f tee -use_fifo 1 "${STREAM_TEE}" >"${FFMPEG_LOG}" 2>&1 &
|
||||||
echo " - Server: Stopping..."
|
|
||||||
|
# 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
|
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