From 8ca09f94b512a29cbbd9170da2e78616b8e8e034 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Tue, 23 Jul 2024 23:20:23 +0100 Subject: [PATCH 01/72] chore: bump version to 0.1.5 --- stream-sprout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index 992386c..b7d693f 100755 --- a/stream-sprout +++ b/stream-sprout @@ -5,7 +5,7 @@ stty -echoctl readonly STREAM_SPROUT_YAML="stream-sprout.yaml" -readonly VERSION="0.1.4" +readonly VERSION="0.1.5" function cleanup() { echo -e " \e[31m\U26D4\e[0m Control-C" From 7998737c0eedd9396b11484a3d71c116e73cb8e9 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Tue, 23 Jul 2024 23:59:31 +0100 Subject: [PATCH 02/72] fix: also actually parse the config file --- stream-sprout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index b7d693f..93f4d0c 100755 --- a/stream-sprout +++ b/stream-sprout @@ -105,7 +105,7 @@ function get_stream_tee() { local URI="" local URI_ENV="" local KEY_ENV="" - parse_yaml "${STREAM_SPROUT_YAML}" sprout_ | grep '^sprout_services_.*_enabled=' | while read -r SERVICES; do + parse_yaml "${STREAM_SPROUT_CONFIG}" 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 From f0916a091be50ddfa951b8529c490cb010a1cdf6 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 00:16:57 +0100 Subject: [PATCH 03/72] feat: add Containerfile --- Containerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Containerfile diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..e50a08e --- /dev/null +++ b/Containerfile @@ -0,0 +1,5 @@ +FROM ghcr.io/jrottenberg/ffmpeg:7-alpine +RUN apk update && apk add --no-cache bash coreutils gawk grep sed +COPY --chown=nobody:nobody --chmod=755 stream-sprout /usr/bin/stream-sprout +EXPOSE 1935 +ENTRYPOINT [ "stream-sprout" ] From 4e32f890bc95f3fc0b59b5984a92261bdac306cb Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 00:17:25 +0100 Subject: [PATCH 04/72] docs: update README with details about using the container --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 94260e3..aeb0844 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,52 @@ See the flake on FlakeHub for more details: - 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.4-1_all.deb`. +### Docker & Podman + +#### Pull the container + +The Stream Sprout container image is available from the GitHub Container Registry. +To pull the latest container image: + +```shell +docker pull ghcr.io/wimpysworld/stream-sprout:latest +``` + +Or if you want a specific version: + +```shell +docker pull ghcr.io/wimpysworld/stream-sprout:0.1.4 +``` + +#### 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:latest --config /data/stream-sprout.yaml +``` + +- The `-p 1935:1935` part will expose the RTMP server port `1935` on the host computer. + - If you have configured Stream Sprout to use a different port, you should change the port number here too. +- The `-it` options will run the container in interactive mode. +- The `-v $PWD:/data` part will mount your current directory `$PWD` as `/data` within the container, allowing you to access your files using the `/data` path. + +#### Build the container + +Build the Stream Sprout container image: + +```shell +docker build -t stream-sprout . +``` + ### From source You need to have [FFmpeg](https://ffmpeg.org/) on your system. @@ -196,9 +242,11 @@ services: These are some of the references used to create this project: - - https://trac.ffmpeg.org/wiki/EncodingForStreamingSites - - https://ffmpeg.org/ffmpeg-protocols.html#rtmp - - https://ffmpeg.org/ffmpeg-formats.html#flv - - https://ffmpeg.org/ffmpeg-formats.html#tee-1 - - https://obsproject.com/forum/resources/obs-studio-stream-to-multiple-platforms-or-channels-at-once.932/ - - https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg +- https://trac.ffmpeg.org/wiki/EncodingForStreamingSites +- https://ffmpeg.org/ffmpeg-protocols.html#rtmp +- https://ffmpeg.org/ffmpeg-formats.html#flv +- https://ffmpeg.org/ffmpeg-formats.html#tee-1 +- https://obsproject.com/forum/resources/obs-studio-stream-to-multiple-platforms-or-channels-at-once.932/ +- https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg +- https://dev.to/ajeetraina/run-ffmpeg-within-a-docker-container-a-step-by-step-guide-c0l +- https://github.com/jrottenberg/ffmpeg From dda746f103290dfc46403d95d75d83ed9ca70c54 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 01:05:15 +0100 Subject: [PATCH 05/72] ci: add build container image --- .../workflows/test-build-stream-sprout.yml | 29 +++++++++++++++++++ Containerfile | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 1a73038..1bd07ae 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -9,6 +9,7 @@ on: - debian/** - flake.nix - package.nix + - Containerfile push: branches: - main @@ -17,6 +18,7 @@ on: - debian/** - flake.nix - package.nix + - Containerfile workflow_dispatch: # TODO: arm64 runner @@ -59,3 +61,30 @@ jobs: nix build .#stream-sprout tree ./result + test-container-build: + runs-on: ubuntu-24.04 + steps: + - name: "Checkout πŸ₯‘" + uses: actions/checkout@v4 + - 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: "Build Container πŸ‹" + uses: docker/build-push-action@v6 + with: + context: . + file: ./Containerfile + push: false + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} + platforms: linux/amd64, linux/arm64 + - name: Logout from Container Registry + run: docker logout ghcr.io diff --git a/Containerfile b/Containerfile index e50a08e..13d8c86 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,5 @@ FROM ghcr.io/jrottenberg/ffmpeg:7-alpine -RUN apk update && apk add --no-cache bash coreutils gawk grep sed +RUN apk add --no-cache --update bash coreutils gawk grep sed COPY --chown=nobody:nobody --chmod=755 stream-sprout /usr/bin/stream-sprout EXPOSE 1935 ENTRYPOINT [ "stream-sprout" ] From 4928a51bd1d6e38a99899c143a37ddc90715503f Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 01:30:05 +0100 Subject: [PATCH 06/72] ci: add build/publish container image --- .github/workflows/publish-release.yml | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 14701c0..ac4c20d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -79,3 +79,34 @@ 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@v4 + - 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: "Build Container πŸ‹" + uses: docker/build-push-action@v6 + with: + context: . + file: ./Containerfile + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.ref_name }} + ghcr.io/${{ github.repository }}:${{ github.sha }} + platforms: linux/amd64, linux/arm64 + - name: Logout from Container Registry + run: docker logout ghcr.io From a79438f0d2bf4343563478a5bfb065910053dd94 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 17:58:14 +0100 Subject: [PATCH 07/72] fix: do not build STREAM_TEE inside a subshell. fixes #25 Iterate over the variables already exposed by parse_yaml and avoid populating the global variable STREAM_TEE inside a subshell because when a subshell exits the variables are reset. --- stream-sprout | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/stream-sprout b/stream-sprout index 93f4d0c..0fbd595 100755 --- a/stream-sprout +++ b/stream-sprout @@ -101,25 +101,41 @@ function add_service() { } function get_stream_tee() { - local SERVICE="" + local SERVICE_ENABLED="" + local SERVICE_KEY="" + local SERVICE_NAME="" + local SERVICE_RTMP="" local URI="" - local URI_ENV="" - local KEY_ENV="" - parse_yaml "${STREAM_SPROUT_CONFIG}" 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 -e " \e[35m\U1F4E1\e[0m ${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 -e " \e[31m\U1F6AB\e[0m ${SERVICE} is not a valid RTMP service URL" - return + # 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 - add_service "${URI}" fi done add_archive From 0eba5601d71408fc2aeaf30d337874ed8879a1b2 Mon Sep 17 00:00:00 2001 From: Alan Pope Date: Wed, 24 Jul 2024 19:23:58 +0100 Subject: [PATCH 08/72] feat: Add snap support (#23) * WIP: snapcraft config * chore: tidy up snap workflow * fix: update ld_library_path * fix the version of the snap This uses a combination of most recent git tag and short rev. * Add git as a build package Required because we have a dump plugin and a nill plugin which pull in next to nothing. Making it hard to do a version stamp without the git command --- .github/workflows/test-snap-builds.yml | 38 +++++++++++++++++ snap/snapcraft.yaml | 56 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/test-snap-builds.yml create mode 100644 snap/snapcraft.yaml diff --git a/.github/workflows/test-snap-builds.yml b/.github/workflows/test-snap-builds.yml new file mode 100644 index 0000000..a701bef --- /dev/null +++ b/.github/workflows/test-snap-builds.yml @@ -0,0 +1,38 @@ +name: πŸ§ͺ Test snap builds on x86_64 + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build snap + uses: snapcore/action-build@v1 + id: snapcraft + + - name: Show log on build failure + if: ${{ failure() }} + run: | + cat /home/runner/.local/state/snapcraft/log/snapcraft*.log + exit 1 + + - name: Review snap + uses: diddlesnaps/snapcraft-review-action@v1 + with: + snap: ${{ steps.snapcraft.outputs.snap }} + isClassic: 'false' + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: 'snap' + path: ${{ steps.snapcraft.outputs.snap}} diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..d5eff2c --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,56 @@ +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 ] + armhf: + build-on: [ armhf ] + build-for: [ armhf ] + +parts: + stream-sprout: + after: [ deps ] + plugin: dump + source: . + build-packages: + - git + override-pull: | + craftctl default + craftctl set version=$(git describe --tags --abbrev=0).$(git rev-parse --short HEAD) + prime: + - stream-sprout + - stream-sprout.yaml.example + - LICENSE + - SECURITY.md + + deps: + plugin: nil + stage-packages: + - ffmpeg + - sed + - awk + - 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 From f0262fbd38d2e0fb9e9d9c1f6ec9f3bc3718af47 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 22:26:55 +0100 Subject: [PATCH 09/72] refactor: update example yaml for new server configuration --- stream-sprout.yaml.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stream-sprout.yaml.example b/stream-sprout.yaml.example index e528716..3bd0920 100644 --- a/stream-sprout.yaml.example +++ b/stream-sprout.yaml.example @@ -1,5 +1,7 @@ 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" From 830edfabab028d804766a07f9c1fbd2ae967f668 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 22:28:23 +0100 Subject: [PATCH 10/72] refactor: add get_server_url() to validate server configuration --- stream-sprout | 62 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/stream-sprout b/stream-sprout index 0fbd595..4a89653 100755 --- a/stream-sprout +++ b/stream-sprout @@ -106,6 +106,8 @@ function get_stream_tee() { local SERVICE_NAME="" local SERVICE_RTMP="" 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 @@ -141,6 +143,52 @@ function get_stream_tee() { 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." + 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="" @@ -237,18 +285,7 @@ while true; do eval "$(parse_yaml "${STREAM_SPROUT_CONFIG}" sprout_)" show_version echo -e " \U2699 ${STREAM_SPROUT_CONFIG}" - if [[ ! "${sprout_server_url}" =~ ^rtmp://.* ]]; then - echo -e " \e[31m\U1F6AB\e[0m ${sprout_server_url} is not a valid RTMP server URL." - exit 1 - fi - echo -en " \e[36m\U1F310\e[0m ${sprout_server_url}" - if [ -n "${sprout_server_key}" ]; then - sprout_server_url+="/${sprout_server_key}" - echo " (key required)" - else - echo "" - fi - STREAM_TEE="" + get_server_url get_stream_tee FFMPEG_LOG=$(mktemp /tmp/stream-sprout.XXXXXX.log) ffmpeg \ @@ -304,4 +341,5 @@ while true; do done rename_archive echo + unset sprout_server_url done From f3b1271813955200a6d89e121f30079544da5a82 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 22:35:52 +0100 Subject: [PATCH 11/72] docs: update documentation to explain the new server config --- .github/obs-settings.png | Bin 20630 -> 26667 bytes README.md | 24 ++++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/obs-settings.png b/.github/obs-settings.png index 50031cae853f5f842bc7a7edf0043076d0bb2b31..21724350f533bcdb42197bc4a26b87109b33250f 100644 GIT binary patch literal 26667 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 LNkvXXu0mjf8IaJ8 diff --git a/README.md b/README.md index aeb0844..a2107b3 100644 --- a/README.md +++ b/README.md @@ -151,24 +151,31 @@ If you don't specify a configuration file, Stream Sprout will look for a configu ### Server +Here's an example configuration for the Stream Sprout `server:` section. + ```yaml 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" ``` -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. +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`. 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. - ### Services `services:` are arbitrarily named. @@ -227,7 +234,7 @@ services: - Go to `Settings` > `Stream` - Select `Custom` from the `Service` dropdown - Copy the server `url:` from your Stream Sprout configuration to the `Server` field: - - `rtmp://127.0.0.1:1935` (*default*) + - `rtmp://127.0.0.1:1935/sprout` (*default*) - Copy the `key:` (if you specified one) from your Stream Sprout configuration to the `Stream Key` field ![OBS Studio Stream Settings](.github/obs-settings.png) @@ -236,6 +243,7 @@ services: - 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.* + - https://superuser.com/questions/1438939/live-streaming-over-rtmps-using-ffmpeg - Each destination you add will increase your bandwidth requirements. ## References From cd6a8185c6d36d96bb78ddf77a904e514ccc5366 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 01:33:59 +0100 Subject: [PATCH 12/72] fix: avoid injection of substitution commands when parsing yaml --- stream-sprout | 1 + 1 file changed, 1 insertion(+) diff --git a/stream-sprout b/stream-sprout index 4a89653..e02a85c 100755 --- a/stream-sprout +++ b/stream-sprout @@ -46,6 +46,7 @@ function parse_yaml() { w='[a-zA-Z0-9_]*' fs=$'\034' sed -ne "s|^\(${s}\):|\1|" \ + -e 's|`||g;s|\$||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}" '{ From 1f5231c6e91f9dacfed98c92ca08420cad708da1 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:16:43 +0100 Subject: [PATCH 13/72] fix: get the complete video codec information in stream_details() --- stream-sprout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index e02a85c..e2bb7de 100755 --- a/stream-sprout +++ b/stream-sprout @@ -210,7 +210,7 @@ function stream_details() { 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 $3}' | awk '{print $1}') + 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}') From 5801fecb3d2138dd1291f94e5b9b68836c8b12e1 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:17:24 +0100 Subject: [PATCH 14/72] fix: remove incorrect query parameters from server URL --- stream-sprout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index e2bb7de..ae1ad03 100755 --- a/stream-sprout +++ b/stream-sprout @@ -293,7 +293,7 @@ while true; do -hide_banner \ -flags +global_header \ -fflags nobuffer \ - -listen 1 -i "${sprout_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 \ From 1cc2100527e8cf7293ac3ea7b5758264f1908336 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:19:36 +0100 Subject: [PATCH 15/72] docs: update versions in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2107b3..e05aeec 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ 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.4-1_all.deb`. +- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`. ### macOS @@ -81,7 +81,7 @@ See the flake on FlakeHub for more details: ### 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.4-1_all.deb`. +- Install it with `apt-get install ./stream-sprout_0.1.5-1_all.deb`. ### Docker & Podman @@ -97,7 +97,7 @@ docker pull ghcr.io/wimpysworld/stream-sprout:latest Or if you want a specific version: ```shell -docker pull ghcr.io/wimpysworld/stream-sprout:0.1.4 +docker pull ghcr.io/wimpysworld/stream-sprout:0.1.5 ``` #### Run the container From ec278996b9552c2822c9225fd1501165cd876f52 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:22:30 +0100 Subject: [PATCH 16/72] refactor: change awk to mawk in snapcraft.yaml; conform to Ubuntu defaults Ubuntu ships mawk by default, Stage mawk in the snap so the behaviour between the .deb and snap is consistent. --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d5eff2c..c717578 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -41,7 +41,7 @@ parts: stage-packages: - ffmpeg - sed - - awk + - mawk - grep apps: From cb86dfc5f425a78cd20f33b9aa04e08ae618e6de Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:29:22 +0100 Subject: [PATCH 17/72] chore: white space clean up --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c717578..8c675cc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -29,7 +29,7 @@ parts: - git override-pull: | craftctl default - craftctl set version=$(git describe --tags --abbrev=0).$(git rev-parse --short HEAD) + craftctl set version=$(git describe --tags --abbrev=0).$(git rev-parse --short HEAD) prime: - stream-sprout - stream-sprout.yaml.example From f20bde1521d92654db17d8b9b1f566f76df2a17b Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:34:13 +0100 Subject: [PATCH 18/72] docs: add snap to install instructions --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e05aeec..7be0cb8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ See the flake on FlakeHub for more details: - +### 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) πŸ“¦οΈ From 9830543ac523c1fac95c039fa76e91ffdc171924 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Wed, 24 Jul 2024 23:45:51 +0100 Subject: [PATCH 19/72] docs: minor updates for clarity --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7be0cb8..b5f4e8c 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ Stream Sprout 🌱 is a simple, self-contained, and easy-to-use solution for str Stream Sprout -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 too) 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. @@ -97,7 +97,7 @@ sudo snap install stream-sprout #### Pull the container -The Stream Sprout container image is available from the GitHub Container Registry. +The Stream Sprout container image is available from the GitHub Container Registry for amd64 and arm64. To pull the latest container image: ```shell From 86352205af3dcf5a91478f388ae8b7347d230720 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 02:18:41 +0100 Subject: [PATCH 20/72] refactor: move test-snap-build alongside the other test build jobs --- .../workflows/test-build-stream-sprout.yml | 25 ++++++++++++ .github/workflows/test-snap-builds.yml | 38 ------------------- 2 files changed, 25 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/test-snap-builds.yml diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 1bd07ae..b83c43d 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -10,6 +10,7 @@ on: - flake.nix - package.nix - Containerfile + - snap/snapcraft.yaml push: branches: - main @@ -19,6 +20,7 @@ on: - flake.nix - package.nix - Containerfile + - snap/snapcraft.yaml workflow_dispatch: # TODO: arm64 runner @@ -88,3 +90,26 @@ jobs: 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@v4 + - 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@v2 + with: + name: snap + path: ${{ steps.snapcraft.outputs.snap}} diff --git a/.github/workflows/test-snap-builds.yml b/.github/workflows/test-snap-builds.yml deleted file mode 100644 index a701bef..0000000 --- a/.github/workflows/test-snap-builds.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: πŸ§ͺ Test snap builds on x86_64 - -on: - workflow_dispatch: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build snap - uses: snapcore/action-build@v1 - id: snapcraft - - - name: Show log on build failure - if: ${{ failure() }} - run: | - cat /home/runner/.local/state/snapcraft/log/snapcraft*.log - exit 1 - - - name: Review snap - uses: diddlesnaps/snapcraft-review-action@v1 - with: - snap: ${{ steps.snapcraft.outputs.snap }} - isClassic: 'false' - - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: 'snap' - path: ${{ steps.snapcraft.outputs.snap}} From 6f89206695808d009d637e0eca050beb0568e40d Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 02:32:52 +0100 Subject: [PATCH 21/72] fix: get current version from stream-sprout VERSION --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 8c675cc..3ad0b86 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -29,7 +29,7 @@ parts: - git override-pull: | craftctl default - craftctl set version=$(git describe --tags --abbrev=0).$(git rev-parse --short HEAD) + craftctl set version=$(grep "^readonly VERSION" stream-sprout | cut -d'"' -f2)-$(git rev-parse --short HEAD) prime: - stream-sprout - stream-sprout.yaml.example From 7b548aa7dfe451c1f344565fb59739b42e678d94 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 02:51:12 +0100 Subject: [PATCH 22/72] refactor: give the snap artefact a more descriptive name --- .github/workflows/test-build-stream-sprout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index b83c43d..6b02186 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -111,5 +111,5 @@ jobs: - name: Upload artifacts ‴️ uses: actions/upload-artifact@v2 with: - name: snap + name: stream-sprout-snap path: ${{ steps.snapcraft.outputs.snap}} From 132b2401333c17d81de29deb2268b6f8db2478fd Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 08:34:16 +0100 Subject: [PATCH 23/72] ci: tag containers with alpine so I can namespace future variants --- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/test-build-stream-sprout.yml | 5 +++-- README.md | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ac4c20d..341d5fa 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -104,9 +104,9 @@ jobs: file: ./Containerfile push: true tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ github.ref_name }} - ghcr.io/${{ github.repository }}:${{ github.sha }} + ghcr.io/${{ github.repository }}:latest-alpine + ghcr.io/${{ github.repository }}:${{ github.ref_name }}-alpine + ghcr.io/${{ github.repository }}:${{ github.sha }}-alpine platforms: linux/amd64, linux/arm64 - name: Logout from Container Registry run: docker logout ghcr.io diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 6b02186..07ab7a9 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -85,8 +85,9 @@ jobs: file: ./Containerfile push: false tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ github.sha }} + ghcr.io/${{ github.repository }}:latest-alpine + ghcr.io/${{ github.repository }}:${{ github.ref_name }}-alpine + ghcr.io/${{ github.repository }}:${{ github.sha }}-alpine platforms: linux/amd64, linux/arm64 - name: Logout from Container Registry run: docker logout ghcr.io diff --git a/README.md b/README.md index b5f4e8c..3d17a11 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,13 @@ The Stream Sprout container image is available from the GitHub Container Registr To pull the latest container image: ```shell -docker pull ghcr.io/wimpysworld/stream-sprout:latest +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 +docker pull ghcr.io/wimpysworld/stream-sprout:0.1.5-alpine ``` #### Run the container @@ -123,7 +123,7 @@ docker run -p 1935:1935 -it -v $PWD:/data stream-sprout --config /data/stream-sp 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:latest --config /data/stream-sprout.yaml +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. From c78953780a9e0d17ffa7fbca8fdca94fa1237603 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 08:52:01 +0100 Subject: [PATCH 24/72] feat: expand ~ to $HOME in the yaml parser --- stream-sprout | 1 + stream-sprout.yaml.example | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index ae1ad03..69e2bd7 100755 --- a/stream-sprout +++ b/stream-sprout @@ -47,6 +47,7 @@ function parse_yaml() { 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}" '{ diff --git a/stream-sprout.yaml.example b/stream-sprout.yaml.example index 3bd0920..86d5d77 100644 --- a/stream-sprout.yaml.example +++ b/stream-sprout.yaml.example @@ -4,7 +4,7 @@ server: app: sprout key: "create your key with uuidgen here" archive_stream: false - archive_path: "${HOME}/Streams" + archive_path: ~/Streams services: trovo: From e25f977afd4d56cc83e27a5638cff0616ff52229 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 10:00:41 +0100 Subject: [PATCH 25/72] docs: add warnings about exposing Stream Sprout on the public internet --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d17a11..021ff63 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Stream Sprout 🌱 is a simple, self-contained, and easy-to-use solution for str 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, too) 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, [**with appropriate security measures**](#-ffmpeg-rtmp-server-accepts-any-rtmp-stream-on-the-listening-port-), and does not require root privileges. There is no transcoding or processing of the video stream 🎞️ The stream is received and then restreamed to the destinations you configure without modification. @@ -168,9 +168,9 @@ server: ip: 127.0.0.1 port: 1935 app: sprout - key: "create your key with uuidgen here" + key: create your key with uuidgen here archive_stream: false - archive_path: "${HOME}/Streams" + archive_path: ~/Streams ``` The `server:` section is used to configure the RTMP server that Stream Sprout creates. @@ -183,6 +183,18 @@ The `server:` section is used to configure the RTMP server that Stream Sprout cr 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. @@ -251,8 +263,17 @@ services: ## Limitations -- Stream Sprout does not support secure RTMP (RTMPS) at this time. +- 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 restreaming using secure RTMP (RTMPS). - *At least I don't think it does, but I haven't fully tested it.* + - Kick only appears to support rtmps:// URLs and Stream Sprout restreams do not appear on Kick. - https://superuser.com/questions/1438939/live-streaming-over-rtmps-using-ffmpeg - Each destination you add will increase your bandwidth requirements. From 3ae05ef3626d6d549599aa0dce969fed590a9856 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 10:17:31 +0100 Subject: [PATCH 26/72] style: remove quotes from example yaml --- stream-sprout.yaml.example | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stream-sprout.yaml.example b/stream-sprout.yaml.example index 86d5d77..1a4ee24 100644 --- a/stream-sprout.yaml.example +++ b/stream-sprout.yaml.example @@ -2,20 +2,20 @@ server: ip: 127.0.0.1 port: 1935 app: sprout - key: "create your key with uuidgen here" + key: create your key with uuidgen here archive_stream: false archive_path: ~/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 From 46d611bbda182a47b916335f519edc8febd52769 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 10:27:35 +0100 Subject: [PATCH 27/72] docs: update bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 ```
**System information** -- OS: [e.g. Ubuntu 20.04] -- stream-sprout version: [e.g. 0.1.0] -- FFmpeg version: [e.g. 4.2.4] +Run `stream-sprout --info` and include the output here. **Screenshots** If applicable, add screenshots to help explain your problem. From 4495463a74e89f65eeedbaac26117361054532c9 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 14:15:16 +0100 Subject: [PATCH 28/72] ci: use the actual stream-sprout version for tagging container releases --- .github/workflows/publish-release.yml | 7 ++++++- .github/workflows/test-build-stream-sprout.yml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 341d5fa..dd1890b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -97,6 +97,11 @@ jobs: 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: @@ -105,7 +110,7 @@ jobs: push: true tags: | ghcr.io/${{ github.repository }}:latest-alpine - ghcr.io/${{ github.repository }}:${{ github.ref_name }}-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 diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 07ab7a9..35b6a7c 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -78,6 +78,11 @@ jobs: 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: @@ -86,7 +91,7 @@ jobs: push: false tags: | ghcr.io/${{ github.repository }}:latest-alpine - ghcr.io/${{ github.repository }}:${{ github.ref_name }}-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 From 544822aaa9c714c3537ddd29f3029d48af153515 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 14:16:36 +0100 Subject: [PATCH 29/72] ci: run test builds if ci jobs change --- .github/workflows/test-build-stream-sprout.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 35b6a7c..265f7fd 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -5,6 +5,7 @@ on: branches: - main paths: + - .github/workflows/*.yml - stream-sprout - debian/** - flake.nix @@ -15,6 +16,7 @@ on: branches: - main paths: + - .github/workflows/*.yml - stream-sprout - debian/** - flake.nix From a11f8f57b0b02f1c9f53b9521c105fdde11fdc71 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 25 Jul 2024 14:12:57 +0100 Subject: [PATCH 30/72] feat: add show_info() --- stream-sprout | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/stream-sprout b/stream-sprout index 69e2bd7..f74fec6 100755 --- a/stream-sprout +++ b/stream-sprout @@ -28,10 +28,61 @@ function show_help() { 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 + 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}" } @@ -247,6 +298,9 @@ while [[ "$#" -gt 0 ]]; do 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;; From 5514c3da26832866f2da20bc1adde8eefa5a132f Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Fri, 26 Jul 2024 07:20:02 +0100 Subject: [PATCH 31/72] fix: actually wrap the runtime requirement in the nix package --- package.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.nix b/package.nix index de6da02..ecf32ff 100644 --- a/package.nix +++ b/package.nix @@ -34,6 +34,8 @@ 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 ''; From 198c807a076c81b57fe6fd7c02c6253ce3a138ab Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Fri, 26 Jul 2024 07:31:47 +0100 Subject: [PATCH 32/72] fix: check the version of bash is new enough --- stream-sprout | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stream-sprout b/stream-sprout index f74fec6..eb911d8 100755 --- a/stream-sprout +++ b/stream-sprout @@ -280,6 +280,11 @@ function banner() { 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." From 84b36880cba6067b1f480f75f15d737fc9df8be5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 28 Jul 2024 13:40:47 +0000 Subject: [PATCH 33/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'flake-schemas': 'https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.3/0190b841-54d3-7b7a-8550-24942bc38caf/source.tar.gz?narHash=sha256-c2AZH9cOnSpPXV8Lwy19/I8EgW7G%2BE%2BZh6YQBZZwzxI%3D' (2024-07-15) β†’ 'https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/source.tar.gz?narHash=sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw%3D' (2024-07-26) β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633334%2Brev-63d37ccd2d178d54e7fb691d7ec76000740ea24a/0190d847-0241-7628-8ab0-d49f442300f4/source.tar.gz?narHash=sha256-7cCC8%2BTdq1%2B3OPyc3%2BgVo9dzUNkNIQfwSDJ2HSi2u3o%3D' (2024-07-21) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633516%2Brev-8c50662509100d53229d4be607f1a3a31157fa12/0190f691-c019-7d99-b723-4b2dd6dfd38f/source.tar.gz?narHash=sha256-2ShmEaFi0kJVOEEu5gmlykN5dwjWYWYUJmlRTvZQRpU%3D' (2024-07-27) --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 3480d0d..0783f3a 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": 1721999734, + "narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=", + "rev": "0a5c42297d870156d9c57d8f99e476b738dcd982", + "revCount": 75, "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.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/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": 1722087241, + "narHash": "sha256-2ShmEaFi0kJVOEEu5gmlykN5dwjWYWYUJmlRTvZQRpU=", + "rev": "8c50662509100d53229d4be607f1a3a31157fa12", + "revCount": 633516, "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.2405.633516%2Brev-8c50662509100d53229d4be607f1a3a31157fa12/0190f691-c019-7d99-b723-4b2dd6dfd38f/source.tar.gz" }, "original": { "type": "tarball", From 9de404f4b2eb9b3831a203a26bb0ad7a13bdb5a7 Mon Sep 17 00:00:00 2001 From: Alan Pope Date: Tue, 30 Jul 2024 10:37:08 +0100 Subject: [PATCH 34/72] feat: Add SBOM generation and vulnerability scanning in workflows (#39) * feat: generate container sbom during release * No need to publish separately, it's automatic * feat: Add regular vulnerability scanning * syntax * specify container file * vital missing step * Display grype output in the log in table format --- .github/workflows/publish-release.yml | 6 +++++ .github/workflows/scan-container.yaml | 35 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/workflows/scan-container.yaml diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index dd1890b..be7c83a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -113,5 +113,11 @@ jobs: 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 diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml new file mode 100644 index 0000000..799179b --- /dev/null +++ b/.github/workflows/scan-container.yaml @@ -0,0 +1,35 @@ +name: "Vulnerability 🐞 scan πŸ” container" + +on: + schedule: + - cron: "0 10 * * 2" + workflow_dispatch: + +jobs: + vulnerability-scan: + name: "Build and scan" + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: build local container + uses: docker/build-push-action@v4 + with: + context: . + file: ./Containerfile + tags: localbuild/testimage:latest + push: false + load: true + + - name: Scan image + uses: anchore/scan-action@v3 + with: + image: "localbuild/testimage:latest" + output-format: table + + - name: Inspect action report + run: cat ${{ steps.scan.outputs.table }} \ No newline at end of file From f1b552c2bde103006f2c0eb4f72c24e263b9fd64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:37:31 +0100 Subject: [PATCH 35/72] chore(deps): bump actions/upload-artifact from 2 to 4 (#38) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test-build-stream-sprout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 265f7fd..e7936a0 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -117,7 +117,7 @@ jobs: snap: ${{ steps.snapcraft.outputs.snap }} isClassic: false - name: Upload artifacts ‴️ - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: stream-sprout-snap path: ${{ steps.snapcraft.outputs.snap}} From 8e3b4dc089a4b5ad6676c072911a1ec520be3b5f Mon Sep 17 00:00:00 2001 From: Alan Pope Date: Tue, 30 Jul 2024 13:49:10 +0100 Subject: [PATCH 36/72] chore: remove armhf snap build The armhf snap hasn't been published, and I doubt anyone would use it if it were. It also blocks other architectures and revisions from being reviewed as it fails review in the store. Other architectures do not fail. ``` Found files with executable stack. This adds PROT_EXEC to mmap(2) during mediation which may cause security denials. Either adjust your program to not require an executable stack, strip it with 'execstack --clear-execstack ...' or remove the affected file from your snap. Affected files: usr/lib/arm-linux-gnueabihf/libx264.so.164 functional-snap-v2_execstack ``` --- snap/snapcraft.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3ad0b86..5183687 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -16,9 +16,6 @@ platforms: arm64: build-on: [ arm64 ] build-for: [arm64 ] - armhf: - build-on: [ armhf ] - build-for: [ armhf ] parts: stream-sprout: From 43d6b9ad885568482c67899da495a78ad6d3395d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:11:37 +0000 Subject: [PATCH 37/72] chore(deps): bump docker/setup-buildx-action from 2 to 3 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index 799179b..756a6cc 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: build local container uses: docker/build-push-action@v4 From a9ed96eaea23848ae3a881acbdc6bee6a03afbd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:11:40 +0000 Subject: [PATCH 38/72] chore(deps): bump docker/build-push-action from 4 to 6 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 6. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v6) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index 756a6cc..2a58bfb 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -17,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: build local container - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . file: ./Containerfile From 1cb4c8ced3459548cd99a33fbfe49447ed235e94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:11:43 +0000 Subject: [PATCH 39/72] chore(deps): bump anchore/scan-action from 3 to 4 Bumps [anchore/scan-action](https://github.com/anchore/scan-action) from 3 to 4. - [Release notes](https://github.com/anchore/scan-action/releases) - [Changelog](https://github.com/anchore/scan-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/anchore/scan-action/compare/v3...v4) --- updated-dependencies: - dependency-name: anchore/scan-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index 2a58bfb..c97c63d 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -26,7 +26,7 @@ jobs: load: true - name: Scan image - uses: anchore/scan-action@v3 + uses: anchore/scan-action@v4 with: image: "localbuild/testimage:latest" output-format: table From 6984d04f7a13d624da720fbbf6e003e825576d83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Aug 2024 13:41:00 +0000 Subject: [PATCH 40/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633516%2Brev-8c50662509100d53229d4be607f1a3a31157fa12/0190f691-c019-7d99-b723-4b2dd6dfd38f/source.tar.gz?narHash=sha256-2ShmEaFi0kJVOEEu5gmlykN5dwjWYWYUJmlRTvZQRpU%3D' (2024-07-27) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.634418%2Brev-2527da1ef492c495d5391f3bcf9c1dd9f4514e32/019193c7-3325-7c5c-9d46-f2d05135ea41/source.tar.gz?narHash=sha256-XROVLf9ti4rrNCFLr%2BDmXRZtPjCQTW4cYy59owTEmxk%3D' (2024-08-24) --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 0783f3a..4ac2238 100644 --- a/flake.lock +++ b/flake.lock @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722087241, - "narHash": "sha256-2ShmEaFi0kJVOEEu5gmlykN5dwjWYWYUJmlRTvZQRpU=", - "rev": "8c50662509100d53229d4be607f1a3a31157fa12", - "revCount": 633516, + "lastModified": 1724531977, + "narHash": "sha256-XROVLf9ti4rrNCFLr+DmXRZtPjCQTW4cYy59owTEmxk=", + "rev": "2527da1ef492c495d5391f3bcf9c1dd9f4514e32", + "revCount": 634418, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633516%2Brev-8c50662509100d53229d4be607f1a3a31157fa12/0190f691-c019-7d99-b723-4b2dd6dfd38f/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.634418%2Brev-2527da1ef492c495d5391f3bcf9c1dd9f4514e32/019193c7-3325-7c5c-9d46-f2d05135ea41/source.tar.gz" }, "original": { "type": "tarball", From 39c182ecf70e4fe4a37eb244228a56da33a09e81 Mon Sep 17 00:00:00 2001 From: Dale Visser Date: Wed, 21 Aug 2024 09:47:18 -0400 Subject: [PATCH 41/72] docs: Fix README link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 021ff63..8f8a19c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Stream Sprout is developed on Linux 🐧 and should work on macOS 🍏 or any ot ## Get Started - [Install](#installation) Stream Sprout πŸ§‘β€πŸ’» -- [Configure](#configuration) Stream Sprout πŸ§‘β€πŸ’» +- [Configure](#configure-stream-sprout) Stream Sprout πŸ§‘β€πŸ’» - [Configure](#configure-obs-studio) OBS Studio πŸŽ›οΈ - Start `stream-sprout` ⌨️ - Click the *Start Streaming* button in OBS Studio πŸ–±οΈ From 5aa579111ea48d09c02accd00de41a5f9ea9a8a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:46:35 +0000 Subject: [PATCH 42/72] chore(deps): bump DeterminateSystems/nix-installer-action from 13 to 14 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 13 to 14. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v13...v14) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index ee7dfb2..73dbfbc 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v13 + - uses: DeterminateSystems/nix-installer-action@v14 - uses: DeterminateSystems/magic-nix-cache-action@v7 - uses: DeterminateSystems/flake-checker-action@v8 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 0a2c2bc..b8d4ec9 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v13 + - uses: DeterminateSystems/nix-installer-action@v14 - uses: DeterminateSystems/magic-nix-cache-action@v7 - uses: DeterminateSystems/update-flake-lock@v23 with: diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index e7936a0..083221a 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -57,7 +57,7 @@ jobs: - name: "Checkout πŸ₯‘" uses: "actions/checkout@v4" - name: "Install Nix ❄️" - uses: "DeterminateSystems/nix-installer-action@v13" + uses: "DeterminateSystems/nix-installer-action@v14" - name: "Enable Magic Nix Cache πŸͺ„" uses: "DeterminateSystems/magic-nix-cache-action@v7" - name: "Build & Test .nix ❄️" From ed5d5d136b85a99b315a2e43f35c72462f2fd943 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Sep 2024 13:40:54 +0000 Subject: [PATCH 43/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.634418%2Brev-2527da1ef492c495d5391f3bcf9c1dd9f4514e32/019193c7-3325-7c5c-9d46-f2d05135ea41/source.tar.gz?narHash=sha256-XROVLf9ti4rrNCFLr%2BDmXRZtPjCQTW4cYy59owTEmxk%3D' (2024-08-24) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635490%2Brev-f65141456289e81ea0d5a05af8898333cab5c53d/019237db-783b-7330-a22e-7d60c20ce855/source.tar.gz?narHash=sha256-pojbL/qteElw/nIXlN8kmHn/w6PQbEHr7Iz%2BWOXs0EM%3D' (2024-09-27) --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 4ac2238..a6a2b4a 100644 --- a/flake.lock +++ b/flake.lock @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1724531977, - "narHash": "sha256-XROVLf9ti4rrNCFLr+DmXRZtPjCQTW4cYy59owTEmxk=", - "rev": "2527da1ef492c495d5391f3bcf9c1dd9f4514e32", - "revCount": 634418, + "lastModified": 1727397532, + "narHash": "sha256-pojbL/qteElw/nIXlN8kmHn/w6PQbEHr7Iz+WOXs0EM=", + "rev": "f65141456289e81ea0d5a05af8898333cab5c53d", + "revCount": 635490, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.634418%2Brev-2527da1ef492c495d5391f3bcf9c1dd9f4514e32/019193c7-3325-7c5c-9d46-f2d05135ea41/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635490%2Brev-f65141456289e81ea0d5a05af8898333cab5c53d/019237db-783b-7330-a22e-7d60c20ce855/source.tar.gz" }, "original": { "type": "tarball", From 901586e4bfcf14bdcb429c70d80b86fc4edcb155 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:46:41 +0000 Subject: [PATCH 44/72] chore(deps): bump DeterminateSystems/update-flake-lock from 23 to 24 Bumps [DeterminateSystems/update-flake-lock](https://github.com/determinatesystems/update-flake-lock) from 23 to 24. - [Release notes](https://github.com/determinatesystems/update-flake-lock/releases) - [Commits](https://github.com/determinatesystems/update-flake-lock/compare/v23...v24) --- updated-dependencies: - dependency-name: DeterminateSystems/update-flake-lock dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index b8d4ec9..49573e4 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -15,6 +15,6 @@ jobs: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v14 - uses: DeterminateSystems/magic-nix-cache-action@v7 - - uses: DeterminateSystems/update-flake-lock@v23 + - uses: DeterminateSystems/update-flake-lock@v24 with: pr-title: "chore: update flake.lock" From 0bb875c287f8bf0559c042ea24024e61918815c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:46:39 +0000 Subject: [PATCH 45/72] chore(deps): bump DeterminateSystems/flake-checker-action from 8 to 9 Bumps [DeterminateSystems/flake-checker-action](https://github.com/determinatesystems/flake-checker-action) from 8 to 9. - [Release notes](https://github.com/determinatesystems/flake-checker-action/releases) - [Commits](https://github.com/determinatesystems/flake-checker-action/compare/v8...v9) --- updated-dependencies: - dependency-name: DeterminateSystems/flake-checker-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 73dbfbc..d8e6bf1 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -18,4 +18,4 @@ jobs: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v14 - uses: DeterminateSystems/magic-nix-cache-action@v7 - - uses: DeterminateSystems/flake-checker-action@v8 + - uses: DeterminateSystems/flake-checker-action@v9 From 6ec390f406000866d3d700ca906ec22d922d88f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:22:34 +0000 Subject: [PATCH 46/72] chore(deps): bump DeterminateSystems/magic-nix-cache-action from 7 to 8 Bumps [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) from 7 to 8. - [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases) - [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/v7...v8) --- updated-dependencies: - dependency-name: DeterminateSystems/magic-nix-cache-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index d8e6bf1..5186ecc 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -17,5 +17,5 @@ jobs: with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v14 - - uses: DeterminateSystems/magic-nix-cache-action@v7 + - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/flake-checker-action@v9 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 49573e4..4756f9c 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v14 - - uses: DeterminateSystems/magic-nix-cache-action@v7 + - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/update-flake-lock@v24 with: pr-title: "chore: update flake.lock" diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 083221a..2373b05 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -59,7 +59,7 @@ jobs: - name: "Install Nix ❄️" uses: "DeterminateSystems/nix-installer-action@v14" - name: "Enable Magic Nix Cache πŸͺ„" - uses: "DeterminateSystems/magic-nix-cache-action@v7" + uses: "DeterminateSystems/magic-nix-cache-action@v8" - name: "Build & Test .nix ❄️" run: | nix build .#stream-sprout From e5a0db3a8ff42b7b232ba2df167c80722a92b37f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Oct 2024 13:44:36 +0000 Subject: [PATCH 47/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635490%2Brev-f65141456289e81ea0d5a05af8898333cab5c53d/019237db-783b-7330-a22e-7d60c20ce855/source.tar.gz?narHash=sha256-pojbL/qteElw/nIXlN8kmHn/w6PQbEHr7Iz%2BWOXs0EM%3D' (2024-09-27) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.636163%2Brev-cd3e8833d70618c4eea8df06f95b364b016d4950/0192cd43-85cd-7ff3-b9be-a3f7995e917d/source.tar.gz?narHash=sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk%3D' (2024-10-26) --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index a6a2b4a..7454b15 100644 --- a/flake.lock +++ b/flake.lock @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727397532, - "narHash": "sha256-pojbL/qteElw/nIXlN8kmHn/w6PQbEHr7Iz+WOXs0EM=", - "rev": "f65141456289e81ea0d5a05af8898333cab5c53d", - "revCount": 635490, + "lastModified": 1729973466, + "narHash": "sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk=", + "rev": "cd3e8833d70618c4eea8df06f95b364b016d4950", + "revCount": 636163, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635490%2Brev-f65141456289e81ea0d5a05af8898333cab5c53d/019237db-783b-7330-a22e-7d60c20ce855/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.636163%2Brev-cd3e8833d70618c4eea8df06f95b364b016d4950/0192cd43-85cd-7ff3-b9be-a3f7995e917d/source.tar.gz" }, "original": { "type": "tarball", From 3f91c0f57351f97ebba7206b3ec89335e45d1698 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:31:27 +0000 Subject: [PATCH 48/72] chore(deps): bump anchore/scan-action from 4 to 5 Bumps [anchore/scan-action](https://github.com/anchore/scan-action) from 4 to 5. - [Release notes](https://github.com/anchore/scan-action/releases) - [Changelog](https://github.com/anchore/scan-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/anchore/scan-action/compare/v4...v5) --- updated-dependencies: - dependency-name: anchore/scan-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index c97c63d..09c26fd 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -26,7 +26,7 @@ jobs: load: true - name: Scan image - uses: anchore/scan-action@v4 + uses: anchore/scan-action@v5 with: image: "localbuild/testimage:latest" output-format: table From 1a19e85d9451fc471f4b81d16a0f1fe1bfdae578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:17:51 +0000 Subject: [PATCH 49/72] chore(deps): bump DeterminateSystems/nix-installer-action from 14 to 15 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 14 to 15. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v14...v15) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 5186ecc..3a600d0 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v14 + - uses: DeterminateSystems/nix-installer-action@v15 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/flake-checker-action@v9 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 4756f9c..175dd56 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v14 + - uses: DeterminateSystems/nix-installer-action@v15 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/update-flake-lock@v24 with: diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 2373b05..2adf190 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -57,7 +57,7 @@ jobs: - name: "Checkout πŸ₯‘" uses: "actions/checkout@v4" - name: "Install Nix ❄️" - uses: "DeterminateSystems/nix-installer-action@v14" + uses: "DeterminateSystems/nix-installer-action@v15" - name: "Enable Magic Nix Cache πŸͺ„" uses: "DeterminateSystems/magic-nix-cache-action@v8" - name: "Build & Test .nix ❄️" From 48c4943d72bbe5ec60632480d0842aa7ec55e856 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 14 Aug 2025 13:54:37 +0000 Subject: [PATCH 50/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.636163%2Brev-cd3e8833d70618c4eea8df06f95b364b016d4950/0192cd43-85cd-7ff3-b9be-a3f7995e917d/source.tar.gz?narHash=sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk%3D' (2024-10-26) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808080%2Brev-ddae11e58c0c345bf66efbddbf2192ed0e58f896/01989f5e-b09d-7b09-9699-5d522e6f12ce/source.tar.gz?narHash=sha256-3sWA5WJybUE16kIMZ3%2BuxcxKZY/JRR4DFBqLdSLBo7w%3D' (2025-08-11) --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 7454b15..1aa9ec4 100644 --- a/flake.lock +++ b/flake.lock @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1729973466, - "narHash": "sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk=", - "rev": "cd3e8833d70618c4eea8df06f95b364b016d4950", - "revCount": 636163, + "lastModified": 1754937576, + "narHash": "sha256-3sWA5WJybUE16kIMZ3+uxcxKZY/JRR4DFBqLdSLBo7w=", + "rev": "ddae11e58c0c345bf66efbddbf2192ed0e58f896", + "revCount": 808080, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.636163%2Brev-cd3e8833d70618c4eea8df06f95b364b016d4950/0192cd43-85cd-7ff3-b9be-a3f7995e917d/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808080%2Brev-ddae11e58c0c345bf66efbddbf2192ed0e58f896/01989f5e-b09d-7b09-9699-5d522e6f12ce/source.tar.gz" }, "original": { "type": "tarball", From 84a1e431371ea2de7ddcba18aca1efdcac3ba971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:45:23 +0000 Subject: [PATCH 51/72] chore(deps): bump DeterminateSystems/nix-installer-action from 15 to 16 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 15 to 16. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v15...v16) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 3a600d0..71387b5 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v15 + - uses: DeterminateSystems/nix-installer-action@v16 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/flake-checker-action@v9 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 175dd56..4b356ef 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v15 + - uses: DeterminateSystems/nix-installer-action@v16 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/update-flake-lock@v24 with: diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 2adf190..e522e07 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -57,7 +57,7 @@ jobs: - name: "Checkout πŸ₯‘" uses: "actions/checkout@v4" - name: "Install Nix ❄️" - uses: "DeterminateSystems/nix-installer-action@v15" + uses: "DeterminateSystems/nix-installer-action@v16" - name: "Enable Magic Nix Cache πŸͺ„" uses: "DeterminateSystems/magic-nix-cache-action@v8" - name: "Build & Test .nix ❄️" From 1d7e3e82476872f290470ddd9219c40a0b487b06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:36:35 +0000 Subject: [PATCH 52/72] chore(deps): bump anchore/scan-action from 5 to 6 Bumps [anchore/scan-action](https://github.com/anchore/scan-action) from 5 to 6. - [Release notes](https://github.com/anchore/scan-action/releases) - [Changelog](https://github.com/anchore/scan-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/anchore/scan-action/compare/v5...v6) --- updated-dependencies: - dependency-name: anchore/scan-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index 09c26fd..b5e4de1 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -26,7 +26,7 @@ jobs: load: true - name: Scan image - uses: anchore/scan-action@v5 + uses: anchore/scan-action@v6 with: image: "localbuild/testimage:latest" output-format: table From 9fbbde4d6ce269a06fee5b25ba65fd484a57f038 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:18:39 +0000 Subject: [PATCH 53/72] chore(deps): bump DeterminateSystems/update-flake-lock from 24 to 27 Bumps [DeterminateSystems/update-flake-lock](https://github.com/determinatesystems/update-flake-lock) from 24 to 27. - [Release notes](https://github.com/determinatesystems/update-flake-lock/releases) - [Commits](https://github.com/determinatesystems/update-flake-lock/compare/v24...v27) --- updated-dependencies: - dependency-name: DeterminateSystems/update-flake-lock dependency-version: '27' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 4b356ef..24162b6 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -15,6 +15,6 @@ jobs: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v16 - uses: DeterminateSystems/magic-nix-cache-action@v8 - - uses: DeterminateSystems/update-flake-lock@v24 + - uses: DeterminateSystems/update-flake-lock@v27 with: pr-title: "chore: update flake.lock" From 57a1f800d2e26996badd97176f77d881a3de470f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:27:32 +0000 Subject: [PATCH 54/72] chore(deps): bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/lint-shellcheck.yml | 2 +- .github/workflows/publish-release.yml | 8 ++++---- .github/workflows/scan-container.yaml | 2 +- .github/workflows/test-build-stream-sprout.yml | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 71387b5..6cd7d5c 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -13,7 +13,7 @@ jobs: name: Flake Checker runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v16 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 24162b6..09b1dfe 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -10,7 +10,7 @@ jobs: name: Flake Lock Updater runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v16 diff --git a/.github/workflows/lint-shellcheck.yml b/.github/workflows/lint-shellcheck.yml index bd125ea..658e48b 100644 --- a/.github/workflows/lint-shellcheck.yml +++ b/.github/workflows/lint-shellcheck.yml @@ -10,7 +10,7 @@ jobs: name: Shellcheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index be7c83a..6c1a719 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: name: "Check versions βš–οΈ" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 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@v4 + - uses: actions/checkout@v5 - name: "Build .deb πŸ₯" env: DEBFULLNAME: "Martin Wimpress" @@ -69,7 +69,7 @@ jobs: id-token: "write" contents: "read" steps: - - uses: "actions/checkout@v4" + - uses: "actions/checkout@v5" with: ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" - uses: "DeterminateSystems/nix-installer-action@main" @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index b5e4de1..652e160 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index e522e07..a086a8f 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Build & Test .deb πŸ₯" env: DEBFULLNAME: "Martin Wimpress" @@ -55,7 +55,7 @@ jobs: contents: "read" steps: - name: "Checkout πŸ₯‘" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Install Nix ❄️" uses: "DeterminateSystems/nix-installer-action@v16" - name: "Enable Magic Nix Cache πŸͺ„" @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout πŸ₯‘ - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Build snap 🐊 uses: snapcore/action-build@v1 id: snapcraft From 7c57494674af638d8c2771d26329314ba461551f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:29:12 +0000 Subject: [PATCH 55/72] chore(deps): bump DeterminateSystems/nix-installer-action from 16 to 19 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 16 to 19. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v16...v19) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-version: '19' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 6cd7d5c..8e26cea 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v16 + - uses: DeterminateSystems/nix-installer-action@v19 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/flake-checker-action@v9 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 09b1dfe..89396a7 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v16 + - uses: DeterminateSystems/nix-installer-action@v19 - uses: DeterminateSystems/magic-nix-cache-action@v8 - uses: DeterminateSystems/update-flake-lock@v27 with: diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index a086a8f..ec4586b 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -57,7 +57,7 @@ jobs: - name: "Checkout πŸ₯‘" uses: "actions/checkout@v5" - name: "Install Nix ❄️" - uses: "DeterminateSystems/nix-installer-action@v16" + uses: "DeterminateSystems/nix-installer-action@v19" - name: "Enable Magic Nix Cache πŸͺ„" uses: "DeterminateSystems/magic-nix-cache-action@v8" - name: "Build & Test .nix ❄️" From c156db1f6417d6c85e4feff2f5b31c5b742a24b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:22:09 +0000 Subject: [PATCH 56/72] chore(deps): bump DeterminateSystems/flake-checker-action from 9 to 12 Bumps [DeterminateSystems/flake-checker-action](https://github.com/determinatesystems/flake-checker-action) from 9 to 12. - [Release notes](https://github.com/determinatesystems/flake-checker-action/releases) - [Commits](https://github.com/determinatesystems/flake-checker-action/compare/v9...v12) --- updated-dependencies: - dependency-name: DeterminateSystems/flake-checker-action dependency-version: '12' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 8e26cea..fee568f 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -18,4 +18,4 @@ jobs: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v19 - uses: DeterminateSystems/magic-nix-cache-action@v8 - - uses: DeterminateSystems/flake-checker-action@v9 + - uses: DeterminateSystems/flake-checker-action@v12 From c470ca46e45bcf22646ce28d9554c4e2b8e2a7d9 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Tue, 26 Aug 2025 23:25:22 +0100 Subject: [PATCH 57/72] refactor(dockerfile): switch from custom ffmpeg to jellyfin-ffmpeg - Replace custom ffmpeg image with alpine base and jellyfin-ffmpeg package - Add symlinks for ffmpeg and ffprobe to standard locations - Set USER directive to run as nobody for improved security --- Containerfile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Containerfile b/Containerfile index 13d8c86..a2c2a94 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,18 @@ -FROM ghcr.io/jrottenberg/ffmpeg:7-alpine -RUN apk add --no-cache --update bash coreutils gawk grep sed +FROM alpine:latest + +RUN apk add --no-cache --update \ + bash \ + coreutils \ + 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" ] From a79d451d0c81814af5f6be07bf34c4aef64562fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 22:28:40 +0000 Subject: [PATCH 58/72] chore(deps): bump DeterminateSystems/magic-nix-cache-action from 8 to 13 Bumps [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) from 8 to 13. - [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases) - [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/v8...v13) --- updated-dependencies: - dependency-name: DeterminateSystems/magic-nix-cache-action dependency-version: '13' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index fee568f..ebb5660 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -17,5 +17,5 @@ jobs: with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v19 - - uses: DeterminateSystems/magic-nix-cache-action@v8 + - uses: DeterminateSystems/magic-nix-cache-action@v13 - uses: DeterminateSystems/flake-checker-action@v12 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 89396a7..c214391 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v19 - - uses: DeterminateSystems/magic-nix-cache-action@v8 + - uses: DeterminateSystems/magic-nix-cache-action@v13 - uses: DeterminateSystems/update-flake-lock@v27 with: pr-title: "chore: update flake.lock" diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index ec4586b..e7eacc0 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -59,7 +59,7 @@ jobs: - name: "Install Nix ❄️" uses: "DeterminateSystems/nix-installer-action@v19" - name: "Enable Magic Nix Cache πŸͺ„" - uses: "DeterminateSystems/magic-nix-cache-action@v8" + uses: "DeterminateSystems/magic-nix-cache-action@v13" - name: "Build & Test .nix ❄️" run: | nix build .#stream-sprout From bd1676efa6e51ba32e9701b12f508200216b0ca1 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Tue, 26 Aug 2025 23:35:43 +0100 Subject: [PATCH 59/72] chore: bump version from 0.1.5 to 0.1.6 Update version number in the stream-sprout script for a new release --- stream-sprout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-sprout b/stream-sprout index eb911d8..9fa9a20 100755 --- a/stream-sprout +++ b/stream-sprout @@ -5,7 +5,7 @@ stty -echoctl readonly STREAM_SPROUT_YAML="stream-sprout.yaml" -readonly VERSION="0.1.5" +readonly VERSION="0.1.6" function cleanup() { echo -e " \e[31m\U26D4\e[0m Control-C" From 382dff7a48e73114e29dbcdd450f461452a640e2 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Tue, 26 Aug 2025 23:51:25 +0100 Subject: [PATCH 60/72] chore: remove coreutils from container dependencies Removes the coreutils package from the Alpine container dependencies. --- Containerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Containerfile b/Containerfile index a2c2a94..5e0b5dc 100644 --- a/Containerfile +++ b/Containerfile @@ -2,7 +2,6 @@ FROM alpine:latest RUN apk add --no-cache --update \ bash \ - coreutils \ jellyfin-ffmpeg \ gawk \ grep \ From 5d864aacb84de0e08ab0de5238526c68f479c964 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:54:51 +0000 Subject: [PATCH 61/72] chore(deps): bump amannn/action-semantic-pull-request from 5 to 6 Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5 to 6. - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v5...v6) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index c70997b..b6dbcb5 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -15,7 +15,7 @@ jobs: name: Validate pull request title runs-on: ubuntu-22.04 steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From f76da1c62b18a97488837292508864ef5cbe1a18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:11:36 +0000 Subject: [PATCH 62/72] chore(deps): bump anchore/scan-action from 6 to 7 Bumps [anchore/scan-action](https://github.com/anchore/scan-action) from 6 to 7. - [Release notes](https://github.com/anchore/scan-action/releases) - [Changelog](https://github.com/anchore/scan-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/scan-action/compare/v6...v7) --- updated-dependencies: - dependency-name: anchore/scan-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/scan-container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index 652e160..b218e70 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -26,7 +26,7 @@ jobs: load: true - name: Scan image - uses: anchore/scan-action@v6 + uses: anchore/scan-action@v7 with: image: "localbuild/testimage:latest" output-format: table From 90eb31a5e561f30e5bc9076723c56be6ef584a4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:52:54 +0000 Subject: [PATCH 63/72] chore(deps): bump actions/upload-artifact from 4 to 5 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test-build-stream-sprout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index e7eacc0..299d6d6 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -117,7 +117,7 @@ jobs: snap: ${{ steps.snapcraft.outputs.snap }} isClassic: false - name: Upload artifacts ‴️ - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: stream-sprout-snap path: ${{ steps.snapcraft.outputs.snap}} From b3be5c43d918573916d772427e11334bf3d88590 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 19:24:34 +0000 Subject: [PATCH 64/72] chore(deps): bump DeterminateSystems/nix-installer-action from 19 to 21 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 19 to 21. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v19...v21) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-version: '21' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/test-build-stream-sprout.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index ebb5660..2870650 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -16,6 +16,6 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v19 + - uses: DeterminateSystems/nix-installer-action@v21 - uses: DeterminateSystems/magic-nix-cache-action@v13 - uses: DeterminateSystems/flake-checker-action@v12 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index c214391..1d6b35e 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: DeterminateSystems/nix-installer-action@v19 + - uses: DeterminateSystems/nix-installer-action@v21 - uses: DeterminateSystems/magic-nix-cache-action@v13 - uses: DeterminateSystems/update-flake-lock@v27 with: diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 299d6d6..d4b7d43 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -57,7 +57,7 @@ jobs: - name: "Checkout πŸ₯‘" uses: "actions/checkout@v5" - name: "Install Nix ❄️" - uses: "DeterminateSystems/nix-installer-action@v19" + uses: "DeterminateSystems/nix-installer-action@v21" - name: "Enable Magic Nix Cache πŸͺ„" uses: "DeterminateSystems/magic-nix-cache-action@v13" - name: "Build & Test .nix ❄️" From c417dc1b10b9d29da261c8f532b41e0706237bb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:36:47 +0000 Subject: [PATCH 65/72] chore(deps): bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-checker.yml | 2 +- .github/workflows/flake-updater.yml | 2 +- .github/workflows/lint-shellcheck.yml | 2 +- .github/workflows/publish-release.yml | 8 ++++---- .github/workflows/scan-container.yaml | 2 +- .github/workflows/test-build-stream-sprout.yml | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flake-checker.yml b/.github/workflows/flake-checker.yml index 2870650..6f0b13f 100644 --- a/.github/workflows/flake-checker.yml +++ b/.github/workflows/flake-checker.yml @@ -13,7 +13,7 @@ jobs: name: Flake Checker runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v21 diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 1d6b35e..738cadc 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -10,7 +10,7 @@ jobs: name: Flake Lock Updater runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v21 diff --git a/.github/workflows/lint-shellcheck.yml b/.github/workflows/lint-shellcheck.yml index 658e48b..81b8108 100644 --- a/.github/workflows/lint-shellcheck.yml +++ b/.github/workflows/lint-shellcheck.yml @@ -10,7 +10,7 @@ jobs: name: Shellcheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6c1a719..63153dd 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: name: "Check versions βš–οΈ" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 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@v5 + - uses: actions/checkout@v6 - name: "Build .deb πŸ₯" env: DEBFULLNAME: "Martin Wimpress" @@ -69,7 +69,7 @@ jobs: id-token: "write" contents: "read" steps: - - uses: "actions/checkout@v5" + - uses: "actions/checkout@v6" with: ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" - uses: "DeterminateSystems/nix-installer-action@main" @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx diff --git a/.github/workflows/scan-container.yaml b/.github/workflows/scan-container.yaml index b218e70..b60b1a0 100644 --- a/.github/workflows/scan-container.yaml +++ b/.github/workflows/scan-container.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index d4b7d43..428e7ab 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Build & Test .deb πŸ₯" env: DEBFULLNAME: "Martin Wimpress" @@ -55,7 +55,7 @@ jobs: contents: "read" steps: - name: "Checkout πŸ₯‘" - uses: "actions/checkout@v5" + uses: "actions/checkout@v6" - name: "Install Nix ❄️" uses: "DeterminateSystems/nix-installer-action@v21" - name: "Enable Magic Nix Cache πŸͺ„" @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Checkout πŸ₯‘" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Container Buildx @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout πŸ₯‘ - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Build snap 🐊 uses: snapcore/action-build@v1 id: snapcraft From a811fe527e67f16b8113b043b32b4233a72ffa22 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 13:59:33 +0000 Subject: [PATCH 66/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'flake-schemas': 'https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/source.tar.gz' (2024-07-26) β†’ 'https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.2.0/019a4a84-544d-7c59-b26d-e334e320c932/source.tar.gz' (2025-10-27) β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808080%2Brev-ddae11e58c0c345bf66efbddbf2192ed0e58f896/01989f5e-b09d-7b09-9699-5d522e6f12ce/source.tar.gz' (2025-08-11) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.905687%2Brev-1327e798cb055f96f92685df444e9a2c326ab5ed/019bb874-9b65-73ec-9dd5-8f14598e59e0/source.tar.gz' (2026-01-12) --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 1aa9ec4..3475f90 100644 --- a/flake.lock +++ b/flake.lock @@ -2,12 +2,12 @@ "nodes": { "flake-schemas": { "locked": { - "lastModified": 1721999734, - "narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=", - "rev": "0a5c42297d870156d9c57d8f99e476b738dcd982", - "revCount": 75, + "lastModified": 1761577921, + "narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=", + "rev": "47849c7625e223d36766968cc6dc23ba0e135922", + "revCount": 107, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/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": 1754937576, - "narHash": "sha256-3sWA5WJybUE16kIMZ3+uxcxKZY/JRR4DFBqLdSLBo7w=", - "rev": "ddae11e58c0c345bf66efbddbf2192ed0e58f896", - "revCount": 808080, + "lastModified": 1768242861, + "narHash": "sha256-F4IIxa5xDHjtrmMcayM8lHctUq1oGltfBQu2+oqDWP4=", + "rev": "1327e798cb055f96f92685df444e9a2c326ab5ed", + "revCount": 905687, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808080%2Brev-ddae11e58c0c345bf66efbddbf2192ed0e58f896/01989f5e-b09d-7b09-9699-5d522e6f12ce/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.905687%2Brev-1327e798cb055f96f92685df444e9a2c326ab5ed/019bb874-9b65-73ec-9dd5-8f14598e59e0/source.tar.gz" }, "original": { "type": "tarball", From 2bda8192f123ec7b48afebf9703dffb2e79cce66 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 22 Jan 2026 16:06:33 +0000 Subject: [PATCH 67/72] docs(agents): add AGENTS.md for AI agent context Provides comprehensive project documentation including: - Overview of Stream Sprout RTMP restreaming tool - Tech stack and build instructions - Code style and linting requirements - Project structure and configuration details - Commit guidelines and security considerations Signed-off-by: Martin Wimpress --- AGENTS.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2a806d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# AGENTS.md + +## Overview + +Stream Sprout is a bash-based RTMP restreaming tool that forwards a single video source (from OBS Studio or similar) to multiple destinations like Twitch, YouTube, Owncast, and Peertube simultaneously. It uses FFmpeg's tee muxer to copy streams without transcoding. + +## Tech Stack + +- **Language:** Bash 5.0+ (single script: `stream-sprout`) +- **Runtime dependency:** FFmpeg (RTMP server and restreaming) +- **Configuration:** YAML parsed via awk/sed +- **Packaging:** Nix flake, Debian .deb, Snap, Docker/Podman + +## Build and Run Commands + +```bash +# Run directly (requires ffmpeg, bash 5.0+, awk, grep, sed) +./stream-sprout --config stream-sprout.yaml + +# Show version and FFmpeg info +./stream-sprout --version + +# Show system info (useful for bug reports) +./stream-sprout --info + +# Nix build +nix build + +# Enter development shell with all dependencies +nix develop + +# Docker build and run +docker build -t stream-sprout . +docker run -p 1935:1935 -it -v $PWD:/data stream-sprout --config /data/stream-sprout.yaml +``` + +## Linting + +ShellCheck is enforced via CI on all pull requests. + +```bash +# Run locally before committing +shellcheck stream-sprout +``` + +The script includes `# shellcheck disable=SC2154` for variables set dynamically via `eval` from YAML parsing. + +## Code Style + +- Bash scripts use `#!/usr/bin/env bash` +- Functions use `function name() {}` syntax +- Use `local` for function-scoped variables +- Use `readonly` for constants +- Validation with informative error messages using Unicode icons and ANSI colours +- Version is tracked in the script: `readonly VERSION="x.y.z"` + +## Project Structure + +``` +stream-sprout # Main bash script (single file) +stream-sprout.yaml # Local config (gitignored) +stream-sprout.yaml.example # Example configuration +package.nix # Nix package definition +devshell.nix # Nix development shell +flake.nix # Nix flake +Dockerfile # Alpine-based container +``` + +## Configuration + +YAML config with two main sections: + +- `server:` - RTMP server settings (ip, port, app, key, archive options) +- `services:` - Destination services (each with enabled, rtmp_server, key) + +Config search order: `./stream-sprout.yaml`, `$XDG_CONFIG_HOME/stream-sprout.yaml`, `/etc/stream-sprout.yaml` + +## PR and Commit Guidelines + +- **Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/)** +- PR titles are validated against Conventional Commits format +- Single-commit PRs must have matching PR title and commit message +- ShellCheck must pass with no warnings + +Common prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:` + +## Version Updates + +When changing version: + +1. Update `VERSION` in `stream-sprout` script +2. The Nix package extracts version automatically from the script + +## Constraints + +- Requires bash 5.0 or newer +- FFmpeg must be available on PATH +- RTMP only (no RTMPS support currently) +- FFmpeg does not enforce stream keys (documented security limitation) + +## Security Considerations + +- Stream keys are stored in plain text in YAML config +- FFmpeg accepts any RTMP stream on the configured port regardless of app/key path +- Do not expose the RTMP port to untrusted networks without additional protection (VPN, firewall, SSH tunnel) From da76e1c219b9b561e74189197c2531abd3a045ed Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 22 Jan 2026 16:09:33 +0000 Subject: [PATCH 68/72] chore: symlink Dockerfile to Containerfile Signed-off-by: Martin Wimpress --- Dockerfile | 1 + 1 file changed, 1 insertion(+) create mode 120000 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 0000000..5240dc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +Containerfile \ No newline at end of file From 7a63368963dd45e8672bd87588fca2bea1bf1494 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Thu, 22 Jan 2026 16:22:11 +0000 Subject: [PATCH 69/72] docs(readme): remove uncertain RTMPS support statement The statement about untested RTMPS support was speculative and potentially confusing to users. Removed as it has now been verified as working. Signed-off-by: Martin Wimpress --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 8f8a19c..c2c81e7 100644 --- a/README.md +++ b/README.md @@ -271,10 +271,6 @@ services: [rtmp @ 0x2ca9be80] Unexpected stream STREAMBOMB, expecting c5b559b2-589d-4925-a28e-20d1954fd6c5 Last message repeated 1 times ``` -- Stream Sprout does not support restreaming using secure RTMP (RTMPS). - - *At least I don't think it does, but I haven't fully tested it.* - - Kick only appears to support rtmps:// URLs and Stream Sprout restreams do not appear on Kick. - - https://superuser.com/questions/1438939/live-streaming-over-rtmps-using-ffmpeg - Each destination you add will increase your bandwidth requirements. ## References From 8393e053b956067c1142ffddddf37e93eb0d959b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:10:51 +0000 Subject: [PATCH 70/72] chore(deps): bump actions/upload-artifact from 5 to 6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test-build-stream-sprout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-stream-sprout.yml b/.github/workflows/test-build-stream-sprout.yml index 428e7ab..e9a43b0 100644 --- a/.github/workflows/test-build-stream-sprout.yml +++ b/.github/workflows/test-build-stream-sprout.yml @@ -117,7 +117,7 @@ jobs: snap: ${{ steps.snapcraft.outputs.snap }} isClassic: false - name: Upload artifacts ‴️ - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: stream-sprout-snap path: ${{ steps.snapcraft.outputs.snap}} From 5df0f028e28497d4dd3e4abef9aeee1ca21d0376 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:10:55 +0000 Subject: [PATCH 71/72] chore(deps): bump DeterminateSystems/update-flake-lock from 27 to 28 Bumps [DeterminateSystems/update-flake-lock](https://github.com/determinatesystems/update-flake-lock) from 27 to 28. - [Release notes](https://github.com/determinatesystems/update-flake-lock/releases) - [Commits](https://github.com/determinatesystems/update-flake-lock/compare/v27...v28) --- updated-dependencies: - dependency-name: DeterminateSystems/update-flake-lock dependency-version: '28' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake-updater.yml b/.github/workflows/flake-updater.yml index 738cadc..a0c019d 100644 --- a/.github/workflows/flake-updater.yml +++ b/.github/workflows/flake-updater.yml @@ -15,6 +15,6 @@ jobs: fetch-depth: 0 - uses: DeterminateSystems/nix-installer-action@v21 - uses: DeterminateSystems/magic-nix-cache-action@v13 - - uses: DeterminateSystems/update-flake-lock@v27 + - uses: DeterminateSystems/update-flake-lock@v28 with: pr-title: "chore: update flake.lock" From 697558934197ec3622aa4d7e48d6a062d5a99004 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 14:03:51 +0000 Subject: [PATCH 72/72] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: β€’ Updated input 'nixpkgs': 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.905687%2Brev-1327e798cb055f96f92685df444e9a2c326ab5ed/019bb874-9b65-73ec-9dd5-8f14598e59e0/source.tar.gz' (2026-01-12) β†’ 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.906484%2Brev-1cd347bf3355fce6c64ab37d3967b4a2cb4b878c/019bfb68-fb8e-7f55-bb2a-5bee98516c95/source.tar.gz' (2026-01-25) --- flake.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 3475f90..6186ae3 100644 --- a/flake.lock +++ b/flake.lock @@ -16,12 +16,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768242861, - "narHash": "sha256-F4IIxa5xDHjtrmMcayM8lHctUq1oGltfBQu2+oqDWP4=", - "rev": "1327e798cb055f96f92685df444e9a2c326ab5ed", - "revCount": 905687, + "lastModified": 1769318308, + "narHash": "sha256-Mjx6p96Pkefks3+aA+72lu1xVehb6mv2yTUUqmSet6Q=", + "rev": "1cd347bf3355fce6c64ab37d3967b4a2cb4b878c", + "revCount": 906484, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.905687%2Brev-1327e798cb055f96f92685df444e9a2c326ab5ed/019bb874-9b65-73ec-9dd5-8f14598e59e0/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",