diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 8417bbb..0817eee 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -21,21 +21,25 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
-**Stream Sprout output**
-Run `stream-sprout` and include the output of the failure below:
+**Stream Sprout output and logs**
+Run `stream-sprout` and include the output, along with **redacted logs** (*remove your IP address and keys*), and wrap it in the collapsible markdown section below.
Stream Sprout output
```text
- stream-sprout output here
+ stream-sprout output from the time of the error here
+ ```
+
+ Stream Sprout logs
+
+ ```text
+ stream-sprout logs from the time of the error here
```
&
**
@@ -17,45 +17,131 @@
# Introduction
-Stream Sprout uses FFmpeg to re-stream a video source to multiple destinations such as Twitch, YouTube, and Owncast.
+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/) π‘
+
+
+
Build-Depends:
debhelper-compat (= 12),
Standards-Version: 4.5.1
-Homepage: https://github.com/wimpys-world/stream-sprout
-Vcs-Browser: https://github.com/wimpys-world/stream-sprout
-Vcs-Git: https://github.com/wimpys-world/stream-sprout.git
+Homepage: https://github.com/wimpysworld/stream-sprout
+Vcs-Browser: https://github.com/wimpysworld/stream-sprout
+Vcs-Git: https://github.com/wimpysworld/stream-sprout.git
Rules-Requires-Root: no
Package: stream-sprout
@@ -15,11 +15,25 @@ Architecture: all
Depends:
coreutils,
ffmpeg,
- procps,
- yq,
+ grep,
+ mawk,
+ sed,
${misc:Depends},
${shlibs:Depends},
-Description: Restream a video source to multiple destinations such as Twitch, YouTube, and Owncast.
- Stream Sprout uses FFmpeg to re-stream a video source to multiple destinations
- such as Twitch, YouTube, and Owncast.
+Description: Restream a video source to multiple destinations such as Twitch, YouTube, Owncast and Peertube.
+ Stream Sprout is a simple, self-contained, and easy-to-use solution for
+ streaming to multiple destinations such as Twitch, YouTube, Owncast and Peertube
.
+ It uses FFmpeg to receive the video stream from OBS Studio (or anything that
+ can publish a RTMP stream) and then restreams it to multiple destinations;
+ providing similar functionality as services like Restream.io and Livepush.io
+ but without the need to pay for a third-party service or run something like
+ nginx with the RTMP module.
+ .
+ Stream Sprout is configured with a simple YAML file and designed to be run on
+ the same computer as your OBS Studio instance (it can be run remotely too) and
+ does not require root privileges.
+ .
+ There is no transcoding or processing of the video stream. The stream is
+ received and then restreamed to the destinations you configure without
+ modification. Optionally you can also archive the stream to disk.
diff --git a/debian/copyright b/debian/copyright
index e44a6a9..d495b63 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,45 +1,40 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: stream-sprout
Upstream-Contact: Martin Wimpress
-Source: https://github.com/wimpys-world/stream-sprout
+Source: https://github.com/wimpysworld/stream-sprout
Files: *
Copyright: 2024 Martin Wimpress
-License: APACHE-2.0
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
.
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ http://www.apache.org/licenses/LICENSE-2.0
.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ .
+ On Debian systems, the complete text of the Apache License, Version 2
+ can be found in "/usr/share/common-licenses/Apache-2.0".
-# If you want to use GPL v2 or later for the /debian/* files use
-# the following clauses, or change it to suit. Delete these two lines
Files: debian/*
Copyright: 2024 Martin Wimpress
-License: GPL-2+
- This package is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
.
- This package is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ http://www.apache.org/licenses/LICENSE-2.0
.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
.
- On Debian systems, the complete text of the GNU General
- Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
+ On Debian systems, the complete text of the Apache License, Version 2
+ can be found in "/usr/share/common-licenses/Apache-2.0".
diff --git a/devshell.nix b/devshell.nix
index c7a01d0..2e812e4 100644
--- a/devshell.nix
+++ b/devshell.nix
@@ -5,9 +5,11 @@
}:
mkShell {
packages = with pkgs; ([
+ coreutils-full
ffmpeg-headless
- procps
- yq
+ gawk
+ gnugrep
+ gnused
]);
shellHook = ''
diff --git a/flake.lock b/flake.lock
index 3480d0d..6186ae3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,12 +2,12 @@
"nodes": {
"flake-schemas": {
"locked": {
- "lastModified": 1721078157,
- "narHash": "sha256-c2AZH9cOnSpPXV8Lwy19/I8EgW7G+E+Zh6YQBZZwzxI=",
- "rev": "29e53dd33b1a38f235ef073e768c62821cb6146e",
- "revCount": 66,
+ "lastModified": 1761577921,
+ "narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=",
+ "rev": "47849c7625e223d36766968cc6dc23ba0e135922",
+ "revCount": 107,
"type": "tarball",
- "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.3/0190b841-54d3-7b7a-8550-24942bc38caf/source.tar.gz"
+ "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.2.0/019a4a84-544d-7c59-b26d-e334e320c932/source.tar.gz"
},
"original": {
"type": "tarball",
@@ -16,12 +16,12 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1721548954,
- "narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=",
- "rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a",
- "revCount": 633334,
+ "lastModified": 1769318308,
+ "narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=",
+ "rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c",
+ "revCount": 906484,
"type": "tarball",
- "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633334%2Brev-63d37ccd2d178d54e7fb691d7ec76000740ea24a/0190d847-0241-7628-8ab0-d49f442300f4/source.tar.gz"
+ "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.906484%2Brev-1cd347bf3355fce6c64ab37d3967b4a2cb4b878c/019bfb68-fb8e-7f55-bb2a-5bee98516c95/source.tar.gz"
},
"original": {
"type": "tarball",
diff --git a/package.nix b/package.nix
index 92dddc8..ecf32ff 100644
--- a/package.nix
+++ b/package.nix
@@ -2,15 +2,20 @@
, installShellFiles
, makeWrapper
, stdenv
+, coreutils-full
, ffmpeg-headless
+, gawk
+, gnugrep
+, gnused
, procps
-, yq
}:
let
runtimePaths = [
+ coreutils-full
ffmpeg-headless
- procps
- yq
+ gawk
+ gnugrep
+ gnused
];
versionMatches =
builtins.match ''
@@ -29,12 +34,14 @@ stdenv.mkDerivation rec {
installPhase = ''
runHook preInstall
install -Dm755 -t "$out/bin" stream-sprout
+ wrapProgram $out/bin/stream-sprout \
+ --prefix PATH : "${lib.makeBinPath runtimePaths}"
runHook postInstall
'';
meta = {
description = "Re-stream a video source to multiple destinations such as Twitch, YouTube, and Owncast.";
- homepage = "https://github.com/wimpys-world/stream-sprout";
+ homepage = "https://github.com/wimpysworld/stream-sprout";
mainProgram = "stream-sprout";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ flexiondotorg ];
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
new file mode 100644
index 0000000..5183687
--- /dev/null
+++ b/snap/snapcraft.yaml
@@ -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
diff --git a/stream-sprout b/stream-sprout
index e4c412c..9fa9a20 100755
--- a/stream-sprout
+++ b/stream-sprout
@@ -1,51 +1,144 @@
#!/usr/bin/env bash
+# shellcheck disable=SC2154
+
+# Disable echo of control characters like ^C
+stty -echoctl
readonly STREAM_SPROUT_YAML="stream-sprout.yaml"
-readonly VERSION="0.1.0"
+readonly VERSION="0.1.6"
-function ctrl_c() {
- echo " - Trapped: CTRL-C"
- pkill ffmpeg
+function cleanup() {
+ echo -e " \e[31m\U26D4\e[0m Control-C"
+ sleep 0.25
+ if kill -0 "${FFMPEG_PID}" 2>/dev/null; then
+ echo -e " \e[31m\U1F480\e[0m FFmpeg process (${FFMPEG_PID}) has been terminated"
+ kill "${FFMPEG_PID}"
+ else
+ echo -e " \e[31m\U23F9\e[0m FFmpeg process (${FFMPEG_PID}) has ended"
+ fi
rename_archive
exit
}
-function get_archive_path() {
- local ARCHIVE_PATH=""
- ARCHIVE_PATH=$(yq e ".server.archive_path" "${STREAM_SPROUT_CONFIG}")
- # Expand any environment variables in the path
- ARCHIVE_PATH=$(eval echo "${ARCHIVE_PATH}")
- if [ -z "${ARCHIVE_PATH}" ]; then
- echo "./"
+# Function to display help
+function show_help() {
+ echo "Restream a video source to multiple destinations such as Twitch, YouTube, Owncast and Peertube."
+ echo ""
+ echo "Usage: $(basename "${0}") [options]"
+ echo ""
+ echo "Options:"
+ echo " --config Specify a custom config file path."
+ echo " --info Show system information; useful when filing bug reports."
+ echo " --version Show version information."
+ echo " --help Display this help message."
+}
+
+function show_info() {
+ local CONTAINER_ENV
+ local CONTAINER_RUNTIME
+ local CONTAINER_RUNTIMES=("docker" "lxc" "podman")
+ local OS_KERNEL
+ local PRETTY_NAME
+ OS_KERNEL=$(uname -s)
+
+ if [ "${OS_KERNEL}" == "Darwin" ]; then
+ # Get macOS product name and version using swvers
+ if [ -x "$(command -v sw_vers)" ]; then
+ PRETTY_NAME="$(sw_vers -productName) $(sw_vers -productVersion)"
+ else
+ PRETTY_NAME="macOS"
+ fi
+ elif [ -e /etc/os-release ]; then
+ PRETTY_NAME=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
else
- mkdir -p "${ARCHIVE_PATH}" 2> /dev/null
- echo "${ARCHIVE_PATH}"
+ PRETTY_NAME="Unknown OS"
fi
+
+ echo -e "Operating System : ${PRETTY_NAME}"
+ # Check for container environment
+ if [ "${OS_KERNEL}" == "Linux" ]; then
+ if [ -n "${SNAP}" ]; then
+ CONTAINER_ENV="Yes"
+ CONTAINER_RUNTIME="snapd"
+ else
+ for runtime in "${CONTAINER_RUNTIMES[@]}"; do
+ if grep -qa ":/${runtime}/" /proc/1/cgroup; then
+ CONTAINER_ENV="Yes"
+ CONTAINER_RUNTIME="${runtime}"
+ break
+ else
+ CONTAINER_ENV="No"
+ CONTAINER_RUNTIME="Unknown"
+ fi
+ done
+ fi
+ echo -e "Containerized : ${CONTAINER_ENV}"
+ if [ "${CONTAINER_ENV,,}" == "yes" ]; then
+ echo -e "Container Runtime: ${CONTAINER_RUNTIME}"
+ fi
+ fi
+ echo -e "Stream Sprout : ${VERSION}"
+ echo -e "awk : $(awk --version | head -n 1)"
+ echo -e "bash : $(bash --version | head -n 1)"
+ echo -e "ffmpeg : $(ffmpeg -version | head -n 1)"
+}
+
+function show_version() {
+ echo -e "\e[92mStream Sprout\e[0m ${VERSION} using FFmpeg ${FFMPEG_VER}"
+}
+
+# https://stackoverflow.com/questions/5014632/how-can-i-parse-a-yaml-file-from-a-linux-shell-script
+function parse_yaml() {
+ local prefix="${2}"
+ local s=""
+ local w=""
+ local fs=""
+ s='[[:space:]]*'
+ w='[a-zA-Z0-9_]*'
+ fs=$'\034'
+ sed -ne "s|^\(${s}\):|\1|" \
+ -e 's|`||g;s|\$||g;' \
+ -e "s|~|${HOME}|g;" \
+ -e "s|^\(${s}\)\(${w}\)${s}:${s}[\"']\(.*\)[\"']$s\$|\1${fs}\2${fs}\3|p" \
+ -e "s|^\(${s}\)\(${w}\)${s}:${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "${1}" |
+ awk -F"${fs}" '{
+ indent = length($1)/2;
+ vname[indent] = $2;
+ for (i in vname) {if (i > indent) {delete vname[i]}}
+ if (length($3) > 0) {
+ vn=""; for (i=0; i/dev/null
+ fi
+ echo -e " \e[34m\U1F4BE\e[0m ${sprout_server_archive_path}/${sprout_server_archive_temp}"
if [ -n "${STREAM_TEE}" ]; then
STREAM_TEE+="|"
fi
- STREAM_TEE+="[f=matroska]${ARCHIVE_PATH}/${ARCHIVE_TEMP}"
+ STREAM_TEE+="[f=matroska]${sprout_server_archive_path}/${sprout_server_archive_temp}"
fi
}
@@ -62,81 +155,252 @@ function add_service() {
function get_stream_tee() {
local SERVICE_ENABLED=""
- local SERVICES=""
+ local SERVICE_KEY=""
+ local SERVICE_NAME=""
+ local SERVICE_RTMP=""
local URI=""
- # Extract services from the YAML
- SERVICES=$(yq e '.services | keys | .[]' "${STREAM_SPROUT_CONFIG}")
-
- # Iterate over each service
- for SERVICE in ${SERVICES}; do
- # Check if the service is enabled in the YAML configuration
- SERVICE_ENABLED=$(yq e ".services.${SERVICE}.enabled" "${STREAM_SPROUT_CONFIG}")
- if [[ "${SERVICE_ENABLED,,}" == "true" || "${SERVICE_ENABLED}" == "1" ]]; then
- echo " - Service: ${SERVICE}"
- URI=$(yq e ".services.${SERVICE}.rtmp_server" "${STREAM_SPROUT_CONFIG}")
- if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
- echo " - Invalid URL: ${SERVICE} is not a valid RTMP URL."
- return
+ STREAM_TEE=""
+ # Iterate over all the sprout_services variables
+ for var in "${!sprout_services@}"; do
+ # Check the variable matches the pattern: sprout_services_*_enabled
+ if [[ "${var}" =~ ^sprout_services_.*_enabled$ ]]; then
+ # Derive the service name
+ # - First remove `sprout_services_` prefix from the beginning of the value stored in the variable $var.
+ # - Next remove the suffix `_enabled` from the end of the SERVICE_NAME variable's value.
+ SERVICE_NAME="${var#sprout_services_}"
+ SERVICE_NAME="${SERVICE_NAME%_enabled}"
+ # Get the value of the variable $var
+ SERVICE_ENABLED="${!var}"
+ if [[ "${SERVICE_ENABLED,,}" == "true" || "${SERVICE_ENABLED}" == "1" ]]; then
+ echo -e " \e[35m\U1F4E1\e[0m ${SERVICE_NAME}"
+ # TODO: This assumes that the RTMP URL and key are set in the YAML file.
+ # Construct the variable name
+ SERVICE_RTMP="sprout_services_${SERVICE_NAME}_rtmp_server"
+ SERVICE_KEY="sprout_services_${SERVICE_NAME}_key"
+ # Use indirect expansion to get the value
+ # By concatenating these two indirectly referenced values, URI
+ # is set to the full URI needed for streaming. For instance, if
+ # SERVICE_RTMP points to a variable holding rtmp://example.com/live
+ # and SERVICE_KEY points to a variable holding abcd1234, then URI
+ # would be set to rtmp://example.com/live/abcd1234.
+ URI="${!SERVICE_RTMP}/${!SERVICE_KEY}"
+ if [[ ! "${URI}" =~ ^rtmp://.* ]]; then
+ echo -e " \e[31m\U1F6AB\e[0m ${SERVICE_NAME} is not a valid RTMP service URL"
+ continue
+ fi
+ add_service "${URI}"
fi
- URI+=$(yq e ".services.${SERVICE}.key" "${STREAM_SPROUT_CONFIG}")
- add_service "${URI}"
fi
done
add_archive
}
-# Check that ffmpeg and yq are available on the PATH
-for CMD in ffmpeg pkill yq; do
- if ! command -v "${CMD}" &> /dev/null; then
- echo "ERROR! ${CMD} is not installed. Exiting."
+function get_server_url() {
+ local asterisks=""
+ local key_length=0
+ # Check if the sprout_server_url is set and display a deprecation notice if it is
+ if [ -n "${sprout_server_url}" ]; then
+ echo -e " \e[31m\U1F6AB\e[0m server:"
+ echo -e " β°βurl: in the YAML is deprecated. Please configure ip: and port: instead."
exit 1
fi
+ # Validate the sprout_server_ip is valid
+ if [[ ! "${sprout_server_ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo -e " \e[33m\U26A0\e[0m server:"
+ echo -e " β°βip: in the YAML is not valid. Falling back to '127.0.0.1'."
+ sprout_server_ip="127.0.0.1"
+ fi
+ # Validate the sprout_server_port is valid
+ if [[ ! "${sprout_server_port}" =~ ^[0-9]+$ ]] || [[ "${sprout_server_port}" -lt 1024 ]] || [[ "${sprout_server_port}" -gt 65535 ]]; then
+ echo -e " \e[33m\U26A0\e[0m server:"
+ echo -e " β°βport: in the YAML is not valid. Must be between 1024 and 65535. Falling back to '1935'."
+ sprout_server_port="1935"
+ fi
+ # Check that sprout_server_app is not empty
+ if [ -z "${sprout_server_app}" ]; then
+ echo -e " \e[33m\U26A0\e[0m server:"
+ echo -e " β°βapp: is not configured in the YAML. Falling back to 'sprout'."
+ sprout_server_app="sprout"
+ fi
+ # Check that sprout_server_key is not empty
+ if [ -z "${sprout_server_key}" ]; then
+ echo -e " \e[33m\U26A0\e[0m server:"
+ echo -e " β°βkey: is not configured in the YAML. \e[1;97mYour Stream Sprout server is unprotected.\e[0m"
+ fi
+ sprout_server_url="rtmp://${sprout_server_ip}:${sprout_server_port}/${sprout_server_app}"
+ if [ -n "${sprout_server_key}" ]; then
+ # Calculate the length of sprout_server_key
+ key_length=${#sprout_server_key}
+ # Create a string of asterisks equal to the length of sprout_server_key
+ asterisks=$(printf "%*s" "${key_length}" "" | tr ' ' '*')
+ echo -e " \e[36m\U1F310\e[0m ${sprout_server_url}/${asterisks}"
+ # Append the sprout_server_key to the sprout_server_url
+ sprout_server_url+="/${sprout_server_key}"
+ else
+ echo -e " \e[36m\U1F310\e[0m ${sprout_server_url}"
+ fi
+}
+
+function stream_details() {
+ local AUDIO=""
+ local VIDEO=""
+ local AUDIO_CODEC=""
+ local AUDIO_BITRATE=""
+ local AUDIO_FREQ=""
+ local AUDIO_CHANNELS=""
+ local VIDEO_CODEC=""
+ local VIDEO_FPS=""
+ local VIDEO_RES=""
+ local VIDEO_BITRATE=""
+
+ AUDIO="$(grep "Audio:" "${FFMPEG_LOG}" | head -n 1)"
+ VIDEO="$(grep "Video:" "${FFMPEG_LOG}" | head -n 1)"
+
+ # Correcting the parsing to accurately extract the required information
+ AUDIO_CODEC=$(echo "${AUDIO}" | awk -F', ' '{print $1}' | awk '{print $4 " " $5}')
+ AUDIO_FREQ=$(echo "${AUDIO}" | awk -F', ' '{print $2}' | awk '{print $1 " " $2}')
+ AUDIO_CHANNELS=$(echo "${AUDIO}" | awk -F', ' '{print $3}' | awk '{print $1}')
+ AUDIO_BITRATE=$(echo "${AUDIO}" | awk -F', ' '{print $5}' | awk '{print $1 " " $2}')
+ VIDEO_CODEC=$(echo "${VIDEO}" | awk -F', ' '{print $1}' | awk '{print $4 " " $5}')
+ VIDEO_FPS=$(echo "${VIDEO}" | awk -F', ' '{print $7}' | awk '{print $1 " " $2}')
+ VIDEO_RES=$(echo "${VIDEO}" | awk -F', ' '{print $5}' | awk '{print $1}')
+ VIDEO_BITRATE=$(echo "${VIDEO}" | awk -F', ' '{print $6}' | awk '{print $1 " " $2}')
+
+ echo -e " \e[32m\U1F441\e[0m FFmpeg detected a new stream"
+ echo -e " ββ Audio: ${AUDIO_FREQ} ${AUDIO_CODEC} in ${AUDIO_CHANNELS^} ~${AUDIO_BITRATE}"
+ echo -e " β°β Video: ${VIDEO_RES} ${VIDEO_CODEC} at ${VIDEO_FPS} ~${VIDEO_BITRATE}"
+}
+
+function banner() {
+ echo -e $'\E[38;2;254;75;55m \E[39m\E[38;2;254;64;66m_\E[39m\E[38;2;254;54;77m_\E[39m\E[38;2;252;44;89m_\E[39m\E[38;2;249;35;101m_\E[39m\E[38;2;244;27;114m_\E[39m\E[38;2;238;20;126m \E[39m\E[38;2;232;14;138m_\E[39m\E[38;2;224;9;151m \E[39m\E[38;2;215;5;163m \E[39m\E[38;2;206;3;175m \E[39m\E[38;2;195;2;187m \E[39m\E[38;2;184;2;198m \E[39m\E[38;2;173;3;208m \E[39m\E[38;2;161;6;217m \E[39m\E[38;2;148;10;226m \E[39m\E[38;2;136;15;233m \E[39m\E[38;2;124;21;240m \E[39m\E[38;2;111;28;245m \E[39m\E[38;2;99;36;249m \E[39m\E[38;2;87;46;252m \E[39m\E[38;2;75;56;254m \E[39m\E[38;2;64;66;254m \E[39m\E[38;2;53;78;254m \E[39m\E[38;2;43;90;252m \E[39m\E[38;2;34;102;248m \E[39m\E[38;2;26;115;244m \E[39m\E[38;2;19;127;238m \E[39m\E[38;2;13;139;231m \E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m \E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m_\E[39m\E[38;2;2;198;184m_\E[39m\E[38;2;3;208;172m_\E[39m\E[38;2;6;218;160m_\E[39m\E[38;2;10;226;147m_\E[39m\E[38;2;15;234;135m \E[39m\E[38;2;21;240;123m \E[39m\E[38;2;29;245;110m \E[39m\E[38;2;37;250;98m \E[39m\E[38;2;46;252;86m \E[39m\E[38;2;56;254;74m \E[39m\E[38;2;67;254;63m \E[39m\E[38;2;78;254;52m \E[39m\E[38;2;90;251;43m \E[39m\E[38;2;103;248;34m \E[39m\E[38;2;115;244;26m \E[39m\E[38;2;128;238;19m \E[39m\E[38;2;140;231;13m \E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m \E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m \E[39m\E[38;2;199;183;2m_\E[39m\E[38;2;209;171;3m \E[39m\E[38;2;218;159;6m \E[39m\E[38;2;227;147;10m \E[39m\E[38;2;234;134;15m\E[39m'
+ echo -e $'\E[38;2;254;64;66m|\E[39m\E[38;2;254;54;77m \E[39m\E[38;2;252;44;89m \E[39m\E[38;2;249;35;101m \E[39m\E[38;2;244;27;114m_\E[39m\E[38;2;238;20;126m_\E[39m\E[38;2;232;14;138m|\E[39m\E[38;2;224;9;151m \E[39m\E[38;2;215;5;163m|\E[39m\E[38;2;206;3;175m_\E[39m\E[38;2;195;2;187m \E[39m\E[38;2;184;2;198m_\E[39m\E[38;2;173;3;208m_\E[39m\E[38;2;161;6;217m_\E[39m\E[38;2;148;10;226m \E[39m\E[38;2;136;15;233m_\E[39m\E[38;2;124;21;240m_\E[39m\E[38;2;111;28;245m_\E[39m\E[38;2;99;36;249m \E[39m\E[38;2;87;46;252m_\E[39m\E[38;2;75;56;254m_\E[39m\E[38;2;64;66;254m_\E[39m\E[38;2;53;78;254m \E[39m\E[38;2;43;90;252m_\E[39m\E[38;2;34;102;248m_\E[39m\E[38;2;26;115;244m_\E[39m\E[38;2;19;127;238m_\E[39m\E[38;2;13;139;231m_\E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m \E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m|\E[39m\E[38;2;2;198;184m \E[39m\E[38;2;3;208;172m \E[39m\E[38;2;6;218;160m \E[39m\E[38;2;10;226;147m_\E[39m\E[38;2;15;234;135m_\E[39m\E[38;2;21;240;123m|\E[39m\E[38;2;29;245;110m_\E[39m\E[38;2;37;250;98m_\E[39m\E[38;2;46;252;86m_\E[39m\E[38;2;56;254;74m \E[39m\E[38;2;67;254;63m_\E[39m\E[38;2;78;254;52m_\E[39m\E[38;2;90;251;43m_\E[39m\E[38;2;103;248;34m \E[39m\E[38;2;115;244;26m_\E[39m\E[38;2;128;238;19m_\E[39m\E[38;2;140;231;13m_\E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m_\E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m_\E[39m\E[38;2;199;183;2m|\E[39m\E[38;2;209;171;3m \E[39m\E[38;2;218;159;6m|\E[39m\E[38;2;227;147;10m_\E[39m\E[38;2;234;134;15m \E[39m\E[38;2;240;122;22m\E[39m'
+ echo -e $'\E[38;2;254;54;77m|\E[39m\E[38;2;252;44;89m_\E[39m\E[38;2;249;35;101m_\E[39m\E[38;2;244;27;114m \E[39m\E[38;2;238;20;126m \E[39m\E[38;2;232;14;138m \E[39m\E[38;2;224;9;151m|\E[39m\E[38;2;215;5;163m \E[39m\E[38;2;206;3;175m \E[39m\E[38;2;195;2;187m_\E[39m\E[38;2;184;2;198m|\E[39m\E[38;2;173;3;208m \E[39m\E[38;2;161;6;217m \E[39m\E[38;2;148;10;226m_\E[39m\E[38;2;136;15;233m|\E[39m\E[38;2;124;21;240m \E[39m\E[38;2;111;28;245m-\E[39m\E[38;2;99;36;249m_\E[39m\E[38;2;87;46;252m|\E[39m\E[38;2;75;56;254m \E[39m\E[38;2;64;66;254m.\E[39m\E[38;2;53;78;254m\'\E[39m\E[38;2;43;90;252m|\E[39m\E[38;2;34;102;248m \E[39m\E[38;2;26;115;244m \E[39m\E[38;2;19;127;238m \E[39m\E[38;2;13;139;231m \E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m|\E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m \E[39m\E[38;2;2;198;184m|\E[39m\E[38;2;3;208;172m_\E[39m\E[38;2;6;218;160m_\E[39m\E[38;2;10;226;147m \E[39m\E[38;2;15;234;135m \E[39m\E[38;2;21;240;123m \E[39m\E[38;2;29;245;110m|\E[39m\E[38;2;37;250;98m \E[39m\E[38;2;46;252;86m.\E[39m\E[38;2;56;254;74m \E[39m\E[38;2;67;254;63m|\E[39m\E[38;2;78;254;52m \E[39m\E[38;2;90;251;43m \E[39m\E[38;2;103;248;34m_\E[39m\E[38;2;115;244;26m|\E[39m\E[38;2;128;238;19m \E[39m\E[38;2;140;231;13m.\E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m|\E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m|\E[39m\E[38;2;199;183;2m \E[39m\E[38;2;209;171;3m|\E[39m\E[38;2;218;159;6m \E[39m\E[38;2;227;147;10m \E[39m\E[38;2;234;134;15m_\E[39m\E[38;2;240;122;22m|\E[39m\E[38;2;246;110;29m\E[39m'
+ echo -e $'\E[38;2;252;44;89m|\E[39m\E[38;2;249;35;101m_\E[39m\E[38;2;244;27;114m_\E[39m\E[38;2;238;20;126m_\E[39m\E[38;2;232;14;138m_\E[39m\E[38;2;224;9;151m_\E[39m\E[38;2;215;5;163m|\E[39m\E[38;2;206;3;175m_\E[39m\E[38;2;195;2;187m|\E[39m\E[38;2;184;2;198m \E[39m\E[38;2;173;3;208m|\E[39m\E[38;2;161;6;217m_\E[39m\E[38;2;148;10;226m|\E[39m\E[38;2;136;15;233m \E[39m\E[38;2;124;21;240m|\E[39m\E[38;2;111;28;245m_\E[39m\E[38;2;99;36;249m_\E[39m\E[38;2;87;46;252m_\E[39m\E[38;2;75;56;254m|\E[39m\E[38;2;64;66;254m_\E[39m\E[38;2;53;78;254m_\E[39m\E[38;2;43;90;252m,\E[39m\E[38;2;34;102;248m|\E[39m\E[38;2;26;115;244m_\E[39m\E[38;2;19;127;238m|\E[39m\E[38;2;13;139;231m_\E[39m\E[38;2;9;151;224m|\E[39m\E[38;2;5;164;215m_\E[39m\E[38;2;3;176;205m|\E[39m\E[38;2;2;187;195m \E[39m\E[38;2;2;198;184m \E[39m\E[38;2;3;208;172m|\E[39m\E[38;2;6;218;160m_\E[39m\E[38;2;10;226;147m_\E[39m\E[38;2;15;234;135m_\E[39m\E[38;2;21;240;123m_\E[39m\E[38;2;29;245;110m_\E[39m\E[38;2;37;250;98m|\E[39m\E[38;2;46;252;86m \E[39m\E[38;2;56;254;74m \E[39m\E[38;2;67;254;63m_\E[39m\E[38;2;78;254;52m|\E[39m\E[38;2;90;251;43m_\E[39m\E[38;2;103;248;34m|\E[39m\E[38;2;115;244;26m \E[39m\E[38;2;128;238;19m|\E[39m\E[38;2;140;231;13m_\E[39m\E[38;2;152;223;8m_\E[39m\E[38;2;164;214;5m_\E[39m\E[38;2;176;205;3m|\E[39m\E[38;2;188;194;2m_\E[39m\E[38;2;199;183;2m_\E[39m\E[38;2;209;171;3m_\E[39m\E[38;2;218;159;6m|\E[39m\E[38;2;227;147;10m_\E[39m\E[38;2;234;134;15m|\E[39m\E[38;2;240;122;22m \E[39m\E[38;2;246;110;29m \E[39m\E[38;2;250;97;37m\E[39m'
+ echo -e $'\E[38;2;249;35;101m \E[39m\E[38;2;244;27;114m \E[39m\E[38;2;238;20;126m \E[39m\E[38;2;232;14;138m \E[39m\E[38;2;224;9;151m \E[39m\E[38;2;215;5;163m \E[39m\E[38;2;206;3;175m \E[39m\E[38;2;195;2;187m \E[39m\E[38;2;184;2;198m \E[39m\E[38;2;173;3;208m \E[39m\E[38;2;161;6;217m \E[39m\E[38;2;148;10;226m \E[39m\E[38;2;136;15;233m \E[39m\E[38;2;124;21;240m \E[39m\E[38;2;111;28;245m \E[39m\E[38;2;99;36;249m \E[39m\E[38;2;87;46;252m \E[39m\E[38;2;75;56;254m \E[39m\E[38;2;64;66;254m \E[39m\E[38;2;53;78;254m \E[39m\E[38;2;43;90;252m \E[39m\E[38;2;34;102;248m \E[39m\E[38;2;26;115;244m \E[39m\E[38;2;19;127;238m \E[39m\E[38;2;13;139;231m \E[39m\E[38;2;9;151;224m \E[39m\E[38;2;5;164;215m \E[39m\E[38;2;3;176;205m \E[39m\E[38;2;2;187;195m \E[39m\E[38;2;2;198;184m \E[39m\E[38;2;3;208;172m \E[39m\E[38;2;6;218;160m \E[39m\E[38;2;10;226;147m \E[39m\E[38;2;15;234;135m \E[39m\E[38;2;21;240;123m \E[39m\E[38;2;29;245;110m \E[39m\E[38;2;37;250;98m \E[39m\E[38;2;46;252;86m|\E[39m\E[38;2;56;254;74m_\E[39m\E[38;2;67;254;63m|\E[39m\E[38;2;78;254;52m \E[39m\E[38;2;90;251;43m \E[39m\E[38;2;103;248;34m \E[39m\E[38;2;115;244;26m \E[39m\E[38;2;128;238;19m \E[39m\E[38;2;140;231;13m \E[39m\E[38;2;152;223;8m \E[39m\E[38;2;164;214;5m \E[39m\E[38;2;176;205;3m \E[39m\E[38;2;188;194;2m \E[39m\E[38;2;199;183;2m \E[39m\E[38;2;209;171;3m \E[39m\E[38;2;218;159;6m \E[39m\E[38;2;227;147;10m \E[39m\E[38;2;234;134;15m \E[39m\E[38;2;240;122;22m \E[39m\E[38;2;246;110;29m \E[39m\E[38;2;250;97;37m \E[39m\E[38;2;253;85;47m\E[39m'
+}
+
+if ((BASH_VERSINFO[0] < 5)); then
+ echo -e " \e[31m\U1F6AB\e[0m bash 5.0 or newer is required to run this script. You have ${BASH_VERSION}"
+ exit 1
+fi
+
+# Check that ffmpeg are available on the PATH
+if ! command -v ffmpeg &> /dev/null; then
+ echo -e " \e[31m\U1F6AB\e[0m ffmpeg is not installed. Exiting."
+ exit 1
+fi
+
+FFMPEG_VER="$(ffmpeg -version | head -n 1 | cut -d' ' -f3)"
+
+# Parse command line arguments
+while [[ "$#" -gt 0 ]]; do
+ case "${1}" in
+ --config)
+ STREAM_SPROUT_CONFIG="${2}"
+ shift
+ if [ ! -f "${STREAM_SPROUT_CONFIG}" ]; then
+ echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_CONFIG} was not found. Exiting."
+ exit 1
+ fi;;
+ --info)
+ show_info
+ exit 0;;
+ --version)
+ show_version
+ exit 0;;
+ --help)
+ show_help
+ exit 0;;
+ *)
+ echo "Unknown option: ${1}"
+ show_help
+ exit 1;;
+ esac
+ shift
done
-# Check in the current working directory
-if [ -f "./${STREAM_SPROUT_YAML}" ]; then
- STREAM_SPROUT_CONFIG="./${STREAM_SPROUT_YAML}"
-# Check in the user's home directory, considering XDG on Linux and compatibility with macOS
-elif [ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}" ]; then
- STREAM_SPROUT_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}"
-# Check in /etc
-elif [ -f "/etc/${STREAM_SPROUT_YAML}" ]; then
- STREAM_SPROUT_CONFIG="/etc/${STREAM_SPROUT_YAML}"
-else
- echo "ERROR: ${STREAM_SPROUT_YAML} was not found."
- exit 1
-fi
-
-# Check if the file is valid YAML
-if ! yq eval '.' "${STREAM_SPROUT_CONFIG}" &>/dev/null; then
- echo "ERROR: ${STREAM_SPROUT_CONFIG} is not valid YAML."
- exit 1
-fi
-
-# trap ctrl-c and call ctrl_c() to clean up
-trap ctrl_c INT
-
-while true; do
- echo "Stream Sprout v${VERSION} using ${STREAM_SPROUT_CONFIG}"
- SERVER_URL=$(yq e ".server.url" "${STREAM_SPROUT_CONFIG}")
- if [[ ! "${SERVER_URL}" =~ ^rtmp://.* ]]; then
- echo " - Invalid URL: ${SERVER_URL} is not a valid RTMP URL."
+# Check if a custom config path was not provided
+if [ -z "${STREAM_SPROUT_CONFIG}" ]; then
+ # Check in the current working directory
+ if [ -f "./${STREAM_SPROUT_YAML}" ]; then
+ STREAM_SPROUT_CONFIG="./${STREAM_SPROUT_YAML}"
+ # Check in the user's home directory, considering XDG on Linux and compatibility with macOS
+ elif [ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}" ]; then
+ STREAM_SPROUT_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/${STREAM_SPROUT_YAML}"
+ # Check in /etc
+ elif [ -f "/etc/${STREAM_SPROUT_YAML}" ]; then
+ STREAM_SPROUT_CONFIG="/etc/${STREAM_SPROUT_YAML}"
+ else
+ echo -e " \e[31m\U1F6AB\e[0m ${STREAM_SPROUT_YAML} was not found. Exiting."
exit 1
fi
- echo " - Server: ${SERVER_URL}"
- STREAM_TEE=""
+fi
+
+banner
+
+# trap relevant signals and call cleanup()
+trap cleanup INT QUIT TERM
+
+while true; do
+ eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)"
+ show_version
+ echo -e " \U2699 ${STREAM_SPROUT_CONFIG}"
+ get_server_url
get_stream_tee
+ FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log)
ffmpeg \
-hide_banner \
-flags +global_header \
-fflags nobuffer \
- -listen 1 -i "${SERVER_URL}?rtmp_buffer=0&rtmp_live=live" \
+ -listen 1 -i "${sprout_server_url}" \
-flvflags no_duration_filesize \
-c:v copy -c:a copy -map 0 \
-movflags +faststart \
- -f tee -use_fifo 1 "${STREAM_TEE}" 2>/dev/null
- echo " - Server: Stopping..."
+ -f tee -use_fifo 1 "${STREAM_TEE}" >"${FFMPEG_LOG}" 2>&1 &
+
+ # Capture the PID of the ffmpeg process
+ FFMPEG_PID=$!
+
+ echo -e " \U2B07 FFmpeg process (${FFMPEG_PID}) logging to ${FFMPEG_LOG}"
+
+ COUNTER=0
+ # 0 for standing-by
+ # 1 for streaming
+ STREAMING_STATUS=0
+
+ # Monitor the FFmpeg process
+ while sleep 1; do
+ STAMP="[$(date +%H:%M:%S)]"
+ if ! kill -0 "${FFMPEG_PID}" 2>/dev/null; then
+ echo -e " \e[31m\U23F9\e[0m FFmpeg has stopped"
+ break
+ else
+ if grep "Input #0, flv, from 'rtmp://" "${FFMPEG_LOG}" > /dev/null; then
+ NEW_STATUS=1
+ else
+ NEW_STATUS=0
+ fi
+
+ # Check if status changed or if it's time to log the status again
+ if [ ${NEW_STATUS} -ne ${STREAMING_STATUS} ] || (( COUNTER % 30 == 0 )); then
+ # If the status has changed, then show the details
+ if [ ${NEW_STATUS} -ne ${STREAMING_STATUS} ]; then
+ stream_details
+ fi
+
+ if [ ${NEW_STATUS} -eq 1 ]; then
+ echo -e " \e[32m\U25B6\e[0m FFmpeg is streaming ${STAMP}"
+ else
+ echo -e " \e[33m\U23F8\e[0m FFmpeg is standing-by ${STAMP}"
+ fi
+ # Update the current status
+ STREAMING_STATUS=${NEW_STATUS}
+ fi
+ ((COUNTER++))
+ fi
+ done
rename_archive
echo
+ unset sprout_server_url
done
diff --git a/stream-sprout.yaml.example b/stream-sprout.yaml.example
index 739c469..1a4ee24 100644
--- a/stream-sprout.yaml.example
+++ b/stream-sprout.yaml.example
@@ -1,18 +1,21 @@
server:
- url: "rtmp://127.0.0.1:1935"
+ ip: 127.0.0.1
+ port: 1935
+ app: sprout
+ key: create your key with uuidgen here
archive_stream: false
- archive_path: "${HOME}/Streams"
+ archive_path: ~/Streams
services:
trovo:
enabled: false
- rtmp_server: "rtmp://livepush.trovo.live/live/"
- key: ""
+ rtmp_server: rtmp://livepush.trovo.live/live/
+ key: your_trovo_stream_key
twitch:
enabled: true
- rtmp_server: "rtmp://live.twitch.tv/app/"
- 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: ""
+ rtmp_server: rtmp://a.rtmp.youtube.com/live2/
+ key: your_youtube_stream_key