Compare commits

..

No commits in common. "main" and "0.1.3" have entirely different histories.

21 changed files with 131 additions and 793 deletions

View file

@ -21,25 +21,21 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**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.
**Stream Sprout output**
Run `stream-sprout` and include the output of the failure below:
<details>
<summary>Stream Sprout output</summary>
```text
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
stream-sprout output here
```
</details>
**System information**
Run `stream-sprout --info` and include the output here.
- OS: [e.g. Ubuntu 20.04]
- stream-sprout version: [e.g. 0.1.0]
- FFmpeg version: [e.g. 4.2.4]
**Screenshots**
If applicable, add screenshots to help explain your problem.

BIN
.github/demo.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

@ -13,9 +13,9 @@ jobs:
name: Flake Checker
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: DeterminateSystems/nix-installer-action@v21
- uses: DeterminateSystems/magic-nix-cache-action@v13
- uses: DeterminateSystems/flake-checker-action@v12
- uses: DeterminateSystems/nix-installer-action@v13
- uses: DeterminateSystems/magic-nix-cache-action@v7
- uses: DeterminateSystems/flake-checker-action@v8

View file

@ -10,11 +10,11 @@ jobs:
name: Flake Lock Updater
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: DeterminateSystems/nix-installer-action@v21
- uses: DeterminateSystems/magic-nix-cache-action@v13
- uses: DeterminateSystems/update-flake-lock@v28
- uses: DeterminateSystems/nix-installer-action@v13
- uses: DeterminateSystems/magic-nix-cache-action@v7
- uses: DeterminateSystems/update-flake-lock@v23
with:
pr-title: "chore: update flake.lock"

View file

@ -15,7 +15,7 @@ jobs:
name: Validate pull request title
runs-on: ubuntu-22.04
steps:
- uses: amannn/action-semantic-pull-request@v6
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View file

@ -10,7 +10,7 @@ jobs:
name: Shellcheck
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:

View file

@ -16,7 +16,7 @@ jobs:
name: "Check versions ⚖️"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Compare App and Git versions 🟰"
@ -37,7 +37,7 @@ jobs:
name: "Build Release 👨‍🔧"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: "Build .deb 🍥"
env:
DEBFULLNAME: "Martin Wimpress"
@ -69,7 +69,7 @@ jobs:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v6"
- uses: "actions/checkout@v4"
with:
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
- uses: "DeterminateSystems/nix-installer-action@main"
@ -79,45 +79,3 @@ 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

View file

@ -1,35 +0,0 @@
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 }}

View file

@ -5,24 +5,18 @@ 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
@ -33,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout 🥡"
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: "Build & Test .deb 🍥"
env:
DEBFULLNAME: "Martin Wimpress"
@ -55,69 +49,13 @@ jobs:
contents: "read"
steps:
- name: "Checkout 🥡"
uses: "actions/checkout@v6"
uses: "actions/checkout@v4"
- name: "Install Nix ❄️"
uses: "DeterminateSystems/nix-installer-action@v21"
uses: "DeterminateSystems/nix-installer-action@v13"
- name: "Enable Magic Nix Cache 🪄"
uses: "DeterminateSystems/magic-nix-cache-action@v13"
uses: "DeterminateSystems/magic-nix-cache-action@v7"
- 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
View file

@ -1,105 +0,0 @@
# 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)

View file

@ -1,17 +0,0 @@
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" ]

View file

@ -1 +0,0 @@
Containerfile

143
README.md
View file

@ -19,13 +19,9 @@
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/) 📡
<div align="center">
<img src=".github/demo.gif" alt="Stream Sprout" width="505" height="415"/>
</div>
It uses [FFmpeg](https://ffmpeg.org/) to receive the video stream from OBS Studio (or anything that can publish a RTMP stream) and then restreams it to multiple destinations; providing similar functionality as services like Restream.io and Livepush.io but without the need to pay 💸 for a third-party service or run something like nginx with the [RTMP module](https://github.com/arut/nginx-rtmp-module).
It uses [FFmpeg](https://ffmpeg.org/) to receive the video stream from OBS Studio (or any encoder that can produce RTMP) and then restreams it to multiple destinations. This provides similar functionality as services like Restream.io and Livepush.io but without the need to pay 💸 for a third-party service or run something like nginx with the [RTMP module](https://github.com/arut/nginx-rtmp-module).
Stream Sprout is configured with a simple YAML file and designed to be run on the same computer as your [OBS Studio](https://obsproject.com/) instance (it can be run remotely, [**with appropriate security measures**](#-ffmpeg-rtmp-server-accepts-any-rtmp-stream-on-the-listening-port-), and does not require root privileges.
Stream Sprout is configured with a simple YAML file and designed to be run on the same computer as your [OBS Studio](https://obsproject.com/) instance (it can be run remotely 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.
@ -39,7 +35,7 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot
## Get Started
- [Install](#installation) Stream Sprout 🧑‍💻
- [Configure](#configure-stream-sprout) Stream Sprout 🧑‍💻
- [Configure](#configuration) Stream Sprout 🧑‍💻
- [Configure](#configure-obs-studio) OBS Studio 🎛️
- Start `stream-sprout` ⌨️
- Click the *Start Streaming* button in OBS Studio 🖱️
@ -52,14 +48,14 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot
### Debian
- 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.5-1_all.deb`.
- Install it with `apt-get install ./stream-sprout_0.1.3-1_all.deb`.
### macOS
Install the Stream Sprout requirements using `brew`:
```shell
brew install bash ffmpeg
brew install bash ffmpeg procps
```
Now clone the project:
@ -78,66 +74,10 @@ See the flake on FlakeHub for more details:
- <https://flakehub.com/flake/wimpysworld/stream-sprout>
### Snap
[![stream-sprout](https://snapcraft.io/stream-sprout/badge.svg)](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.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 .
```
- Install it with `apt-get install ./stream-sprout_0.1.3-1_all.deb`.
### From source
@ -152,52 +92,31 @@ 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 📝
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:
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:
ip: 127.0.0.1
port: 1935
app: sprout
key: create your key with uuidgen here
url: "rtmp://127.0.0.1:1935"
key: "create your key with uuidgen here"
archive_stream: false
archive_path: ~/Streams
archive_path: "${HOME}/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.
@ -256,32 +175,24 @@ 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/sprout` (*default*)
- `rtmp://127.0.0.1:1935` (*default*)
- Copy the `key:` (if you specified one) from your Stream Sprout configuration to the `Stream Key` field
![OBS Studio Stream Settings](.github/obs-settings.png)
## Limitations
- 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
```
- 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.*
- 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://dev.to/ajeetraina/run-ffmpeg-within-a-docker-container-a-step-by-step-guide-c0l
- https://github.com/jrottenberg/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

1
debian/control vendored
View file

@ -17,6 +17,7 @@ Depends:
ffmpeg,
grep,
mawk,
procps,
sed,
${misc:Depends},
${shlibs:Depends},

View file

@ -5,11 +5,11 @@
}:
mkShell {
packages = with pkgs; ([
coreutils-full
ffmpeg-headless
gawk
gnugrep
gnused
mawk
procps
]);
shellHook = ''

20
flake.lock generated
View file

@ -2,12 +2,12 @@
"nodes": {
"flake-schemas": {
"locked": {
"lastModified": 1761577921,
"narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=",
"rev": "47849c7625e223d36766968cc6dc23ba0e135922",
"revCount": 107,
"lastModified": 1721078157,
"narHash": "sha256-c2AZH9cOnSpPXV8Lwy19/I8EgW7G+E+Zh6YQBZZwzxI=",
"rev": "29e53dd33b1a38f235ef073e768c62821cb6146e",
"revCount": 66,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.2.0/019a4a84-544d-7c59-b26d-e334e320c932/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.3/0190b841-54d3-7b7a-8550-24942bc38caf/source.tar.gz"
},
"original": {
"type": "tarball",
@ -16,12 +16,12 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1769318308,
"narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=",
"rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c",
"revCount": 906484,
"lastModified": 1721548954,
"narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=",
"rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a",
"revCount": 633334,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.906484%2Brev-1cd347bf3355fce6c64ab37d3967b4a2cb4b878c/019bfb68-fb8e-7f55-bb2a-5bee98516c95/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633334%2Brev-63d37ccd2d178d54e7fb691d7ec76000740ea24a/0190d847-0241-7628-8ab0-d49f442300f4/source.tar.gz"
},
"original": {
"type": "tarball",

View file

@ -2,7 +2,6 @@
, installShellFiles
, makeWrapper
, stdenv
, coreutils-full
, ffmpeg-headless
, gawk
, gnugrep
@ -11,11 +10,11 @@
}:
let
runtimePaths = [
coreutils-full
ffmpeg-headless
gawk
gnugrep
gnused
procps
];
versionMatches =
builtins.match ''
@ -34,8 +33,6 @@ 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
'';

View file

@ -1,53 +0,0 @@
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

View file

@ -1,92 +1,16 @@
#!/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.6"
readonly VERSION="0.1.3"
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
function ctrl_c() {
echo " - Trapped: CTRL-C"
pkill ffmpeg
rename_archive
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
function parse_yaml() {
local prefix="${2}"
@ -97,8 +21,6 @@ function parse_yaml() {
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}" '{
@ -119,8 +41,7 @@ function rename_archive() {
# 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
STAMP=$(date +%Y%m%d_%H%M%S)
echo -e " \U1F500 ${sprout_server_archive_path}/${sprout_server_archive_temp}"
echo -e " \U21AA ${sprout_server_archive_path}/stream-sprout-${STAMP}.mkv"
echo " - Rename: ${sprout_server_archive_path}/${sprout_server_archive_temp} to ${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
}
@ -132,9 +53,9 @@ function add_archive() {
if [ -z "${sprout_server_archive_path}" ]; then
sprout_server_archive_path="$(dirname "${PWD}")"
else
mkdir -p "${sprout_server_archive_path}" 2>/dev/null
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}"
echo " - Archive: ${sprout_server_archive_path}/${sprout_server_archive_temp}"
if [ -n "${STREAM_TEE}" ]; then
STREAM_TEE+="|"
fi
@ -154,253 +75,82 @@ function add_service() {
}
function get_stream_tee() {
local SERVICE_ENABLED=""
local SERVICE_KEY=""
local SERVICE_NAME=""
local SERVICE_RTMP=""
local SERVICE=""
local URI=""
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}"
local URI_ENV=""
local KEY_ENV=""
parse_yaml "${STREAM_SPROUT_YAML}" sprout_ | grep '^sprout_services_.*_enabled=' | while read -r SERVICES; do
SERVICE=$(echo "${SERVICES}" | cut -d'_' -f3)
ENABLED=$(echo "${SERVICES}" | cut -d'=' -f2 | tr -d \'\" )
if [[ "${ENABLED,,}" == "true" || "${ENABLED}" == "1" ]]; then
echo " - Service: ${SERVICE}"
# Construct the variable name
URI_ENV="sprout_services_${SERVICE}_rtmp_server"
KEY_ENV="sprout_services_${SERVICE}_key"
# Use indirect expansion to get the value
URI="${!URI_ENV}${!KEY_ENV}"
if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
echo " - Invalid URL: ${SERVICE} is not a valid RTMP URL"
return
fi
add_service "${URI}"
fi
done
add_archive
}
function get_server_url() {
local asterisks=""
local key_length=0
# Check if the sprout_server_url is set and display a deprecation notice if it is
if [ -n "${sprout_server_url}" ]; then
echo -e " \e[31m\U1F6AB\e[0m server:"
echo -e " ╰─url: in the YAML is deprecated. Please configure ip: and port: instead."
# Check that ffmpeg and pkill are available on the PATH
for CMD in ffmpeg pkill; do
if ! command -v "${CMD}" &> /dev/null; then
echo "ERROR! ${CMD} is not installed. Exiting."
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 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
# 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
banner
eval "$(parse_yaml "${STREAM_SPROUT_YAML}" sprout_)"
# trap relevant signals and call cleanup()
trap cleanup INT QUIT TERM
# trap ctrl-c and call ctrl_c() to clean up
trap ctrl_c INT
while true; do
eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
show_version
echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
get_server_url
echo "Stream Sprout v${VERSION} using ${STREAM_SPROUT_CONFIG}"
if [[ ! "${sprout_server_url}" =~ ^rtmp://.* ]]; then
echo " - Invalid URL: ${sprout_server_url} is not a valid RTMP URL."
exit 1
fi
echo -n " - Server: ${sprout_server_url}"
if [ -n "${sprout_server_key}" ]; then
sprout_server_url+="/${sprout_server_key}"
echo " (key required)"
else
echo ""
fi
STREAM_TEE=""
get_stream_tee
FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log)
ffmpeg \
-hide_banner \
-flags +global_header \
-fflags nobuffer \
-listen 1 -i "${sprout_server_url}" \
-listen 1 -i "${sprout_server_url}?rtmp_buffer=0&rtmp_live=live" \
-flvflags no_duration_filesize \
-c:v copy -c:a copy -map 0 \
-movflags +faststart \
-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
-f tee -use_fifo 1 "${STREAM_TEE}" 2>/dev/null
echo " - Server: Stopping..."
rename_archive
echo
unset sprout_server_url
done

View file

@ -1,21 +1,19 @@
server:
ip: 127.0.0.1
port: 1935
app: sprout
key: create your key with uuidgen here
url: "rtmp://127.0.0.1:1935"
key: "create your key with uuidgen here"
archive_stream: false
archive_path: ~/Streams
archive_path: "${HOME}/Streams"
services:
trovo:
enabled: false
rtmp_server: rtmp://livepush.trovo.live/live/
key: your_trovo_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_twitch_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_youtube_stream_key
rtmp_server: "rtmp://a.rtmp.youtube.com/live2/"
key: "your_youtube_stream_key"