Skip to content

Commit 4eeaa4d

Browse files
committed
Fix the oban example
1 parent d02817a commit 4eeaa4d

1 file changed

Lines changed: 53 additions & 54 deletions

File tree

guides/useful_concepts/running_membrane_in_elixir_application.md

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
This guide outlines best practices for integrating Membrane Pipelines into your Elixir application, specifically focusing on how to attach pipelines to your application's supervision tree.
44

5-
6-
75
In most cases, pipelines are used in one of the following scenarios:
86

97
- Static Orchestration: Maintaining a single, permanent pipeline that always runs with your application (e.g. mixing output from multiple IP cameras into a single HLS stream).
@@ -17,7 +15,7 @@ In most cases, pipelines are used in one of the following scenarios:
1715
This approach is ideal when your pipeline architecture is fixed and needs to run continuously from the moment your application starts.
1816
Imagine an application that monitors a security camera feed to detect people or vehicles in real-time. Such an application could consist of two components:
1917

20-
- The Membrane Pipeline which connects to an RTSP stream, decodes the video, and extracts raw frames. It sends these frames to the external process (via a Unix socket or standard input), receives the transformed video back, re-encodes it, and broadcasts the final stream using HLS.
18+
- The Membrane Pipeline which connects to an SRT stream, decodes the video, and extracts raw frames. It sends these frames to the external process (via a Unix socket or standard input), receives the transformed video back, re-encodes it, and broadcasts the final stream using HLS.
2119

2220
- The OS Process being a Python script running a machine learning model like [RF-DETR](https://github.com/roboflow/rf-detr). It reads raw video frames and performs object segmentation, coloring the pixels that correspond to detected objects.
2321

@@ -28,21 +26,7 @@ defmodule MyProject.Pipeline do
2826
use Membrane.Pipeline
2927

3028
@impl true
31-
def handle_init(_ctx, rtsp_url) do
32-
spec = [
33-
child(:source, %Membrane.RTSP.Source{
34-
transport: :tcp,
35-
allowed_media_types: [:video],
36-
stream_uri: rtsp_url,
37-
on_connection_closed: :send_eos
38-
})
39-
]
40-
41-
{[spec: spec], %{}}
42-
end
43-
44-
@impl true
45-
def handle_child_pad_added(:source, :output, _ctx, state) do
29+
def handle_init(_ctx, srt_url) do
4630
hls_config = %Membrane.HTTPAdaptiveStream.SinkBin{
4731
manifest_module: Membrane.HTTPAdaptiveStream.HLS,
4832
target_window_duration: Membrane.Time.seconds(120),
@@ -52,7 +36,12 @@ defmodule MyProject.Pipeline do
5236
}
5337

5438
spec = [
55-
get_child(:source)
39+
child(:source, %Membrane.SRT.Source{
40+
transport: :tcp,
41+
allowed_media_types: [:video],
42+
stream_uri: srt_url,
43+
on_connection_closed: :send_eos
44+
})
5645
|> child(:depayloader, Membrane.H264.RTP.Depayloader)
5746
|> child(:parser, Membrane.H264.Parser)
5847
|> child(:decoder, Membrane.H264.FFmpeg.Decoder)
@@ -62,11 +51,8 @@ defmodule MyProject.Pipeline do
6251
|> child(:hls, hls_config)
6352
]
6453

65-
{[spec: structure], state}
54+
{[spec: spec], %{}}
6655
end
67-
68-
@impl true
69-
def handle_child_pad_added(_child, _pad, _ctx, state), do: {:ok, state}
7056
end
7157
```
7258

@@ -82,10 +68,10 @@ defmodule MyProject.InfrastructureSupervisor do
8268
end
8369

8470
@impl true
85-
def init(rtsp_url) do
71+
def init(srt_url) do
8672
children = [
8773
{MuonTrap.Daemon, ["python", ["run_model.py", "rf-detr-large-2026.pth"], [log_output: :debug]], restart: :transient]}
88-
{MyProject.Pipeline, [input_url: rtsp_url], restart: :transient}
74+
{MyProject.Pipeline, [input_url: srt_url], restart: :transient}
8975
]
9076

9177
Supervisor.init(children, strategy: :one_for_all)
@@ -106,7 +92,7 @@ defmodule MyProject.Application do
10692
@impl true
10793
def start(_type, _args) do
10894
children = [
109-
{MyProject.InfrastructureSupervisor, [rtsp_url: "rtsp://user:password@127.0.0.1:554"]}
95+
{MyProject.InfrastructureSupervisor, [srt_url: "srt://127.0.0.1:9710"]}
11096
# ... other children required by your application
11197
]
11298

@@ -119,7 +105,7 @@ end
119105
## Dynamically spawning the pipelines under the DynamicSupervisor
120106

121107
While the static approach works perfectly for fixed infrastructure, many applications require more flexibility.
122-
Consider a scenario where you need to spawn a new pipeline on demand to ingest an RTSP stream from a camera provided by a user.
108+
Consider a scenario where you need to spawn a new pipeline on demand to ingest an SRT stream from a camera provided by a user.
123109

124110
Since we have already defined the `InfrastructureSupervisor`, scaling to multiple dynamic pipelines is straightforward.
125111

@@ -151,7 +137,7 @@ def mount(params, _session, socket) do
151137
if connected?(socket) do
152138
{:ok, infrastructure_supervisor_pid} = DynamicSupervisor.start_child(
153139
MyProject.PipelineDynamicSupervisor,
154-
{MyProject.InfrastructureSupervisor, params["rtsp_url"]}
140+
{MyProject.InfrastructureSupervisor, params["srt_url"]}
155141
)
156142
{:ok, assign(socket, :infrastructure_supervisor_pid, infrastructure_supervisor_pid)}
157143
else
@@ -177,73 +163,86 @@ end
177163

178164
Batch processing is ideal for "offline tasks" where you need to ensure the job completes successfully.
179165

180-
For example, let's assume we want to transcode video from H.264 (in MP4) to VP8 (in Matroska). To achieve this, we build the pipeline as follows:
166+
For example, let's assume we want to rescale H.264 video in MP4 container. To achieve this, we build the pipeline as follows:
181167

182168
```elixir
183169
defmodule TranscodePipeline do
184170
use Membrane.Pipeline
185171

186172
alias Membrane.File.{Source, Sink}
173+
alias Membrane.H264.FFmpeg.{Decoder, Encoder}
187174
alias Membrane.H264.Parser
188-
alias Membrane.FFmpeg.SWScale.PixelFormatConverter
175+
alias Membrane.FFmpeg.SWScale.Converter
189176

190177
@impl true
191178
def handle_init(_ctx, opts) do
192-
structure = [
179+
spec = [
193180
child(:source, %Source{location: opts[:input_path]})
194181
|> child(:demuxer, Membrane.MP4.Demuxer.ISOM)
195-
|> via_out(:video)
196-
|> child(:parser, Parser)
197-
|> child(:decoder, Membrane.H264.FFmpeg.Decoder)
198-
|> child(:converter, %PixelFormatConverter{format: :I420})
199-
|> child(:encoder, Membrane.VP8.Encoder)
200-
|> child(:muxer, Membrane.Matroska.Muxer)
182+
|> via_out(:output, options: [kind: :video])
183+
|> child(:in_parser, %Parser{output_stream_structure: :annexb})
184+
|> child(:decoder, Decoder)
185+
|> child(:converter, %Converter{output_width: 1080, output_height: 720})
186+
|> child(:encoder, Encoder)
187+
|> child(:out_parser, %Parser{output_stream_structure: :avc1})
201188
|> child(:sink, %Sink{location: opts[:output_path]})
202189
]
203190

204-
{[spec: structure], %{}}
191+
{[spec: spec], %{}}
205192
end
193+
194+
@impl true
195+
def handle_element_end_of_stream(:sink, :input, _ctx, state), do: {[terminate: :normal], state}
196+
197+
@impl true
198+
def handle_element_end_of_stream(_child, _pad, _ctx, state), do: {[], state}
206199
end
207200
```
208201

209-
To ensure reliability, we might wrap this execution in an [Oban](https://hexdocs.pm/oban/Oban.html) worker.
202+
To ensure reliability, we might wrap execution of the pipeline in an [Oban](https://hexdocs.pm/oban/Oban.html) worker.
210203

211204
Oban is the standard library for background job processing in Elixir. It persists jobs to your database, ensuring that your long-running transcoding tasks are fault-tolerant.
212205
If the pipeline crashes or the server restarts, Oban will automatically retry the job until it succeeds.
213206
To install Oban in your project, you can follow this [installation guide](https://hexdocs.pm/oban/installation.html).
214207

215-
Assuming that Oban is installed in your project we can define an Oban worker:
208+
Assuming that Oban is installed in your project and the `:default` queue is configured we can define the Oban worker:
216209

217210
```elixir
211+
218212
defmodule VideoTranscoderWorker do
219-
@timeout_sec :time
220-
221-
use Oban.Worker, queue: :media, unique: [period: 60], timeout: :timer.seconds(10)
213+
use Oban.Worker, queue: :default, unique: [period: 60]
222214

223215
@impl true
224216
def perform(%Oban.Job{args: %{"input_path" => input, "output_path" => output}}) do
225-
{:ok, _supervisor_pid, pipeline_pid} = Membrane.Pipeline.start_link(
226-
TranscodePipeline,
227-
[input_path: input, output_path: output]
228-
)
229-
230-
case Membrane.Pipeline.wait_for_termination(pipeline_pid, 100_000) do
231-
:ok -> :ok
232-
{:error, reason} -> {:error, reason}
217+
{:ok, _supervisor_pid, pipeline_pid} =
218+
Membrane.Pipeline.start_link(
219+
TranscodePipeline,
220+
input_path: input,
221+
output_path: output
222+
)
223+
224+
ref = Process.monitor(pipeline_pid)
225+
226+
receive do
227+
{:DOWN, ^ref, :process, ^pipeline_pid, :normal} ->
228+
:ok
233229
end
234230
end
231+
232+
@impl true
233+
def timeout(_job), do: :timer.minutes(5)
235234
end
236235
```
237236

238237
Now we can run the Oban worker:
239238

240239
```elixir
241240
%{
242-
"input_path" => "uploads/input_video.mp4",
243-
"output_path" => "processed/transcoded_video.mkv"
241+
"input_path" => "input.mp4",
242+
"output_path" => "output.mp4"
244243
}
245244
|> VideoTranscoderWorker.new()
246245
|> Oban.insert()
247246
```
248247

249-
When the job terminates successfully, you will see the output `.mkv` file with transcoded video.
248+
When the job terminates successfully, you will see the output `output.mp4` file with transcoded video.

0 commit comments

Comments
 (0)