-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorial part 2 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d9ff986
07b6d4d
5299737
6de45b7
293a8dc
5a989b8
ce9fd38
b10c389
46b8ec1
658dba6
8ce9008
6775378
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| _build | ||
| deps | ||
| cover | ||
| doc | ||
| .elixir_ls | ||
| .git | ||
| .gitignore | ||
| .formatter.exs | ||
| .terraform | ||
| .terraform.lock.hcl | ||
| terraform | ||
| k8s | ||
| tutorial | ||
| test | ||
| .expert | ||
| .claude | ||
| *.md | ||
| *.ez | ||
| erl_crash.dump | ||
| output |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # syntax=docker/dockerfile:1.7 | ||
|
|
||
| ARG ELIXIR_VERSION=1.17.3 | ||
| ARG OTP_VERSION=27.1.2 | ||
|
|
||
| # Builder stage - use CUDA base image | ||
| FROM nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04 AS builder | ||
|
|
||
| SHELL ["/bin/bash", "-o", "pipefail", "-c"] | ||
|
|
||
| ENV MIX_ENV=prod \ | ||
| LANG=C.UTF-8 \ | ||
| DEBIAN_FRONTEND=noninteractive \ | ||
| ERL_AFLAGS="+JMsingle true" | ||
|
|
||
| RUN apt-get update | ||
| RUN apt-get install software-properties-common -y | ||
| RUN add-apt-repository ppa:rabbitmq/rabbitmq-erlang | ||
| RUN apt-get update | ||
|
|
||
| # Install build dependencies | ||
| RUN apt-get install -y --no-install-recommends \ | ||
| build-essential \ | ||
| cmake \ | ||
| curl \ | ||
| pkg-config \ | ||
| libvulkan-dev \ | ||
| git \ | ||
| elixir \ | ||
| erlang \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Download and install Erlang and Elixir using direct .deb downloads | ||
|
|
||
| # Install Rust | ||
| ENV RUSTUP_HOME=/usr/local/rustup \ | ||
| CARGO_HOME=/usr/local/cargo \ | ||
| PATH=/usr/local/cargo/bin:$PATH | ||
|
|
||
| RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable \ | ||
| && rustc --version && cargo --version | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| RUN mix local.hex --force && mix local.rebar --force | ||
|
|
||
| COPY mix.exs mix.lock ./ | ||
| COPY config config | ||
| COPY --from=transcoder_plugin . /membrane_transcoder_plugin | ||
| RUN mix deps.get --only prod && mix deps.compile | ||
|
|
||
| COPY lib lib | ||
|
|
||
| ENV MIX_ENV=prod | ||
| RUN mix compile && mix release && cp -r _build/prod/rel/ex_broadcaster /app/release | ||
|
|
||
|
|
||
| # Runtime stage - use CUDA base image for GPU support with Vulkan | ||
| FROM nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04 AS runtime | ||
|
|
||
| ENV LANG=C.UTF-8 \ | ||
| DEBIAN_FRONTEND=noninteractive \ | ||
| NVIDIA_DRIVER_CAPABILITIES=compute,graphics \ | ||
| NVIDIA_VISIBLE_DEVICES=all | ||
|
|
||
| # Install runtime dependencies | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| libstdc++6 \ | ||
| libssl3 \ | ||
| libncurses6 \ | ||
| libsctp1 \ | ||
| ca-certificates \ | ||
| libegl1-mesa-dev \ | ||
| libgl1-mesa-dri \ | ||
| libxcb-xfixes0-dev \ | ||
| mesa-vulkan-drivers \ | ||
| locales \ | ||
| gnupg \ | ||
| wget \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && locale-gen en_US.UTF-8 | ||
|
|
||
| RUN wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | gpg --dearmor -o /usr/share/keyrings/lunarg-signing-key-pub.gpg \ | ||
| && echo "deb [signed-by=/usr/share/keyrings/lunarg-signing-key-pub.gpg] https://packages.lunarg.com/vulkan/ jammy main" \ | ||
| > /etc/apt/sources.list.d/lunarg-vulkan-jammy.list \ | ||
| && apt-get update && apt-get install -y --no-install-recommends libvulkan1 \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| RUN groupadd --system app && useradd --system --gid app --create-home --home /app app | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY --from=builder --chown=app:app /app/release ./ | ||
|
|
||
| USER app | ||
|
|
||
| EXPOSE 1935 8080 | ||
|
|
||
| ENTRYPOINT ["/app/bin/ex_broadcaster"] | ||
| CMD ["start"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,15 +3,15 @@ defmodule ExBroadcaster do | |
| RTMP-to-HLS adaptive transcoding application built on the Membrane Framework. | ||
|
|
||
| Receives an RTMP stream (H.264/AAC) and produces multi-variant HLS output | ||
| using GPU-accelerated transcoding via `membrane_vk_video_plugin`. | ||
| using `Membrane.Transcoder` with Vulkan Video native acceleration via `membrane_vk_video_plugin`. | ||
|
|
||
| ## Variants | ||
|
|
||
| | Name | Resolution | Bitrate | | ||
| |-------|-------------|------------| | ||
| | 1080p | 1920 × 1080 | 4 000 kbps | | ||
| | 720p | 1280 × 720 | 2 500 kbps | | ||
| | 480p | 854 × 480 | 1 000 kbps | | ||
| | Name | Resolution | Bitrate (avg / max) | | ||
| |-------|-------------|----------------------| | ||
| | 1080p | 1920 × 1080 | 5.0 Mbps / 6.0 Mbps | | ||
| | 720p | 1280 × 720 | 2.8 Mbps / 3.5 Mbps | | ||
| | 480p | 854 × 480 | 1.4 Mbps / 1.75 Mbps | | ||
|
Comment on lines
+12
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think these bitrates are corresponding to the ones in code (we have @max_bitrate_factor equal to 1.1 and 5*1.1 is 5.5, not 6 :D) |
||
|
|
||
| ## Usage | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ defmodule ExBroadcaster.Application do | |
|
|
||
| alias ExBroadcaster.Storages.S3Storage | ||
| alias Membrane.HTTPAdaptiveStream.Storages.FileStorage | ||
| alias Membrane.RTMPServer, as: RTMPServer | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this |
||
|
|
||
| @max_concurrent_pipelines Application.compile_env( | ||
| :ex_broadcaster, | ||
|
|
@@ -55,7 +56,7 @@ defmodule ExBroadcaster.Application do | |
| Each pipeline uploads HLS output to its own S3 prefix named after | ||
| the stream key, allowing multiple simultaneous streams. | ||
| """ | ||
| @spec handle_new_client(pid(), String.t(), String.t()) :: client_behaviour_spec() | ||
| @spec handle_new_client(pid(), String.t(), String.t()) :: RTMPServer.client_behaviour_spec() | ||
| def handle_new_client(client_ref, app, stream_key) do | ||
| Logger.info("[App] New RTMP client: app=#{app}, stream_key=#{stream_key}") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,63 +1,73 @@ | ||
| defmodule ExBroadcaster.Pipeline do | ||
| @moduledoc """ | ||
| Membrane pipeline that receives a single RTMP stream, transcodes it to | ||
| multiple H.264 variants using the GPU (via `Membrane.VKVideo.Transcoder`), | ||
| and uploads an adaptive HLS manifest + fMP4 segments to Amazon S3. | ||
| multiple H.264 variants using a single `Membrane.Transcoder` with multiple outputs | ||
| (v0.4.0+ feature) and Vulkan Video native acceleration via `membrane_vk_video_plugin`, | ||
| and uploads an adaptive HLS manifest + fMP4 segments. | ||
|
|
||
| Topology | ||
| -------- | ||
|
|
||
| RTMP.SourceBin | ||
| │ | ||
| ├─ :video ──► H264.Parser ──► VKVideo.Transcoder ─┬─ :output(1080p) ─► CMAF.Muxer(1080p) ─► HLS.Sink | ||
| │ ├─ :output(720p) ─► CMAF.Muxer(720p) ─► │ | ||
| │ └─ :output(480p) ─► CMAF.Muxer(480p) ─► │ | ||
| │ │ | ||
| └─ :audio ──► AAC.Parser ──► Tee ─── :output(1080p) ──────────────────► CMAF.Muxer(1080p) ─► │ | ||
| ├─ :output(720p) ──────────────────► CMAF.Muxer(720p) ─► │ | ||
| └─ :output(480p) ──────────────────► CMAF.Muxer(480p) ─► │ | ||
| ├─ :video ──► Transcoder ──┬─ :output(:p1080) ──► CMAF.Muxer(1080p) ─► | ||
| │ ├─ :output(:p720) ──► CMAF.Muxer(720p) ─► HLS.Sink | ||
| │ └─ :output(:p480) ──► CMAF.Muxer(480p) ─► | ||
| │ | ||
| └─ :audio ──► AAC.Parser ──► Tee ──────────┬─ :output(:p1080) ─► CMAF.Muxer(1080p) ─► | ||
| ├─ :output(:p720) ─► CMAF.Muxer(720p) ─► | ||
| └─ :output(:p480) ─► CMAF.Muxer(480p) ─► | ||
|
|
||
| Each CMAF muxer produces a single muxed audio+video CMAF track delivered | ||
| to the shared HLS sink, which writes segments and a master playlist. | ||
|
|
||
| Each output pad also carries its own target bitrate (`Membrane.Transcoder.Video.VariableBitrate`), | ||
| so the transcoder encodes a proper bitrate ladder alongside the resolution ladder instead of | ||
| leaving every variant at the encoder's default rate control. | ||
|
Comment on lines
+24
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's quite obvious that it happens this way, so let's remove this comment |
||
|
|
||
| GPU requirements | ||
| ---------------- | ||
| `Membrane.VKVideo.Transcoder` requires Linux with a Vulkan-capable GPU | ||
| (NVIDIA or AMD with Mesa) and the Vulkan Video extension. | ||
| When `native_acceleration: :if_available` is set and `membrane_vk_video_plugin` is present, | ||
| `Membrane.Transcoder` uses Vulkan Video hardware acceleration for encoding/decoding. | ||
| This requires Linux with a Vulkan-capable GPU (NVIDIA or AMD with Mesa) and the Vulkan Video extension. | ||
| """ | ||
|
|
||
| use Membrane.Pipeline | ||
|
|
||
| require Membrane.Logger, as: Logger | ||
| require Membrane.Pad | ||
|
|
||
| alias Membrane.HTTPAdaptiveStream | ||
| alias Membrane.MP4.Muxer.CMAF, as: CMAFMuxer | ||
| alias Membrane.VKVideo | ||
| alias Membrane.Pad | ||
| alias Membrane.Transcoder.Video.VariableBitrate | ||
|
|
||
| @max_bitrate_factor 1.1 | ||
|
|
||
| @variants [ | ||
| %{ | ||
| id: :p1080, | ||
| track_name: "1080p", | ||
| width: 1920, | ||
| height: 1080, | ||
| bitrate: 4_000_000, | ||
| framerate: {30, 1} | ||
| framerate: {30, 1}, | ||
| average_bitrate: 5_000_000 | ||
| }, | ||
| %{ | ||
| id: :p720, | ||
| track_name: "720p", | ||
| width: 1280, | ||
| height: 720, | ||
| bitrate: 2_500_000, | ||
| framerate: {30, 1} | ||
| framerate: {30, 1}, | ||
| average_bitrate: 2_800_000 | ||
| }, | ||
| %{ | ||
| id: :p480, | ||
| track_name: "480p", | ||
| width: 854, | ||
| height: 480, | ||
| bitrate: 1_000_000, | ||
| framerate: {30, 1} | ||
| framerate: {30, 1}, | ||
| average_bitrate: 1_400_000 | ||
| } | ||
| ] | ||
|
|
||
|
|
@@ -98,11 +108,10 @@ defmodule ExBroadcaster.Pipeline do | |
| video_branch = | ||
| get_child(:rtmp_source) | ||
| |> via_out(:video) | ||
| |> child(:h264_parser, %Membrane.H264.Parser{ | ||
| output_alignment: :au, | ||
| output_stream_structure: :annexb | ||
| |> child(:transcoder, %Membrane.Transcoder{ | ||
| transcoding_policy: :always, | ||
| native_acceleration: :if_available | ||
| }) | ||
| |> child(:transcoder, Membrane.VKVideo.Transcoder) | ||
|
|
||
| audio_branch = | ||
| get_child(:rtmp_source) | ||
|
|
@@ -126,27 +135,34 @@ defmodule ExBroadcaster.Pipeline do | |
| end | ||
|
|
||
| defp build_variant_spec(variant, segment_duration) do | ||
| %{id: id, track_name: name, width: w, height: h, bitrate: br, framerate: fps} = variant | ||
| %{ | ||
| id: id, | ||
| track_name: name, | ||
| width: w, | ||
| height: h, | ||
| framerate: fps, | ||
| average_bitrate: average_bitrate | ||
| } = variant | ||
|
|
||
| bitrate = %VariableBitrate{ | ||
| average_bitrate: average_bitrate, | ||
| max_bitrate: round(average_bitrate * @max_bitrate_factor) | ||
| } | ||
|
|
||
| video_to_muxer = | ||
| get_child(:transcoder) | ||
| |> via_out(Pad.ref(:output, id), | ||
| options: [ | ||
| width: w, | ||
| height: h, | ||
| tune: :low_latency, | ||
| rate_control: | ||
| {:constant_bitrate, | ||
| %VKVideo.Encoder.ConstantBitrate{ | ||
| bitrate: br | ||
| }}, | ||
| scaling_algorithm: :bilinear | ||
| output_stream_format: %Membrane.H264{ | ||
| width: w, | ||
| height: h, | ||
| framerate: fps, | ||
| alignment: :au, | ||
| stream_structure: :avc1 | ||
| }, | ||
| bitrate: bitrate | ||
| ] | ||
| ) | ||
| |> child({:h264_parser_out, id}, %Membrane.H264.Parser{ | ||
| output_alignment: :au, | ||
| output_stream_structure: :avc1 | ||
| }) | ||
| |> via_in(Pad.ref(:input, {:video, id})) | ||
| |> child({:cmaf_muxer, id}, %CMAFMuxer{segment_min_duration: segment_duration}) | ||
| |> via_in(Pad.ref(:input, id), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,17 +21,17 @@ defmodule ExBroadcaster.MixProject do | |
| defp deps do | ||
| [ | ||
| {:membrane_core, "~> 1.2"}, | ||
| {:membrane_vk_video_plugin, "~> 0.2.1"}, | ||
| {:membrane_transcoder_plugin, "~> 0.4.0"}, | ||
| {:membrane_vk_video_plugin, "~> 0.2.0"}, | ||
| {:membrane_rtmp_plugin, "~> 0.29.3"}, | ||
| {:membrane_http_adaptive_stream_plugin, "~> 0.21.0"}, | ||
| {:membrane_mp4_plugin, "~> 0.36.0"}, | ||
| {:membrane_h26x_plugin, "~> 0.10.5"}, | ||
| {:membrane_aac_plugin, "~> 0.19.0"}, | ||
| {:bandit, "~> 1.0"}, | ||
| {:plug, "~> 1.14"}, | ||
| {:ex_aws_s3, "~> 2.5"}, | ||
| {:hackney, ">= 0.0.0"}, | ||
| {:credo, ">= 0.0.0", only: :dev, runtime: false} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this |
||
| {:credo, ">= 0.0.0", only: :dev, runtime: false, override: true} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would we need this |
||
| ] | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these two used at all? 🤔