diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 0817eee..8417bbb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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:
Stream Sprout output
```text
- stream-sprout output from the time of the error here
- ```
-
- Stream Sprout logs
-
- ```text
- stream-sprout logs from the time of the error here
+ stream-sprout output here
```
-
Build-Depends:
debhelper-compat (= 12),
Standards-Version: 4.5.1
-Homepage: https://github.com/wimpysworld/stream-sprout
-Vcs-Browser: https://github.com/wimpysworld/stream-sprout
-Vcs-Git: https://github.com/wimpysworld/stream-sprout.git
+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
Rules-Requires-Root: no
Package: stream-sprout
@@ -15,25 +15,11 @@ Architecture: all
Depends:
coreutils,
ffmpeg,
- grep,
- mawk,
- sed,
+ procps,
+ yq,
${misc:Depends},
${shlibs:Depends},
-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
+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.
.
- 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 d495b63..e44a6a9 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,40 +1,45 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: stream-sprout
Upstream-Contact: Martin Wimpress
-Source: https://github.com/wimpysworld/stream-sprout
+Source: https://github.com/wimpys-world/stream-sprout
Files: *
Copyright: 2024 Martin Wimpress
-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
+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:
.
- http://www.apache.org/licenses/LICENSE-2.0
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of 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".
+ 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.
+# 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: 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
+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.
.
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
.
- 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.
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
.
- On Debian systems, the complete text of the Apache License, Version 2
- can be found in "/usr/share/common-licenses/Apache-2.0".
+ On Debian systems, the complete text of the GNU General
+ Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
diff --git a/devshell.nix b/devshell.nix
index 2e812e4..c7a01d0 100644
--- a/devshell.nix
+++ b/devshell.nix
@@ -5,11 +5,9 @@
}:
mkShell {
packages = with pkgs; ([
- coreutils-full
ffmpeg-headless
- gawk
- gnugrep
- gnused
+ procps
+ yq
]);
shellHook = ''
diff --git a/flake.lock b/flake.lock
index 6186ae3..3480d0d 100644
--- a/flake.lock
+++ b/flake.lock
@@ -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",
diff --git a/package.nix b/package.nix
index ecf32ff..92dddc8 100644
--- a/package.nix
+++ b/package.nix
@@ -2,20 +2,15 @@
, installShellFiles
, makeWrapper
, stdenv
-, coreutils-full
, ffmpeg-headless
-, gawk
-, gnugrep
-, gnused
, procps
+, yq
}:
let
runtimePaths = [
- coreutils-full
ffmpeg-headless
- gawk
- gnugrep
- gnused
+ procps
+ yq
];
versionMatches =
builtins.match ''
@@ -34,14 +29,12 @@ 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/wimpysworld/stream-sprout";
+ homepage = "https://github.com/wimpys-world/stream-sprout";
mainProgram = "stream-sprout";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ flexiondotorg ];
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
deleted file mode 100644
index 5183687..0000000
--- a/snap/snapcraft.yaml
+++ /dev/null
@@ -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
diff --git a/stream-sprout b/stream-sprout
index 9fa9a20..0a96c53 100755
--- a/stream-sprout
+++ b/stream-sprout
@@ -1,144 +1,51 @@
#!/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.1"
-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 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)
+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 "./"
else
- PRETTY_NAME="Unknown OS"
+ mkdir -p "${ARCHIVE_PATH}" 2> /dev/null
+ echo "${ARCHIVE_PATH}"
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}"
+ ARCHIVE_ENABLED=$(yq e ".server.archive_stream" "${STREAM_SPROUT_CONFIG}")
+ if [[ "${ARCHIVE_ENABLED,,}" == "true" || "${ARCHIVE_ENABLED}" == "1" ]]; then
+ echo " - Archive: ${ARCHIVE_PATH}/${ARCHIVE_TEMP}"
if [ -n "${STREAM_TEE}" ]; then
STREAM_TEE+="|"
fi
- STREAM_TEE+="[f=matroska]${sprout_server_archive_path}/${sprout_server_archive_temp}"
+ STREAM_TEE+="[f=matroska]${ARCHIVE_PATH}/${ARCHIVE_TEMP}"
fi
}
@@ -155,252 +62,88 @@ function add_service() {
function get_stream_tee() {
local SERVICE_ENABLED=""
- local SERVICE_KEY=""
- local SERVICE_NAME=""
- local SERVICE_RTMP=""
+ local SERVICES=""
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}"
+ # 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
fi
+ URI+=$(yq e ".services.${SERVICE}.key" "${STREAM_SPROUT_CONFIG}")
+ 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 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."
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
+# 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 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}"
+ SERVER_URL=$(yq e ".server.url" "${STREAM_SPROUT_CONFIG}")
+ SERVER_KEY=$(yq e ".server.key" "${STREAM_SPROUT_CONFIG}")
+ if [[ ! "${SERVER_URL}" =~ ^rtmp://.* ]]; then
+ echo " - Invalid URL: ${SERVER_URL} is not a valid RTMP URL."
+ exit 1
+ fi
+ echo -n " - Server: ${SERVER_URL}"
+ if [ "${SERVER_KEY}" != "null" ]; then
+ SERVER_URL+="/${SERVER_KEY}"
+ echo " (key required)"
+ else
+ echo ""
+ fi
+ STREAM_TEE=""
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 "${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
diff --git a/stream-sprout.yaml.example b/stream-sprout.yaml.example
index 1a4ee24..556e3a2 100644
--- a/stream-sprout.yaml.example
+++ b/stream-sprout.yaml.example
@@ -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: ""
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: ""
twitch:
enabled: true
- rtmp_server: rtmp://live.twitch.tv/app/
- key: your_twitch_stream_key
+ rtmp_server: "rtmp://live.twitch.tv/app/"
+ 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: ""