Skip to content

Commit cf7e242

Browse files
committed
WIP guide
1 parent a5c43db commit cf7e242

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Using Membrane in Elixir Applications
2+
This guide provides tips for using Membrane Pipelines in your Elixir application.
3+
4+
5+
## Telemetry
6+
7+
## Spawning under supervisor
8+
In most cases, pipelines are used for:
9+
10+
* Batch Processing: Handling "offline" tasks that have a clear start and end. For example, converting an MP4 file (with H.264 stream) into a Matroska container (with VP8 stream).
11+
* Static Orchestration:
12+
* Dynamic Orchestration: Spawning pipelines on demand based on user actions. For example, starting a unique pipeline for every user who joins a videoconferencing room.
13+
14+
15+
### Static Orchestration with the pipeline spawned under Supervisor upon system startup
16+
17+
This scenario is applicable when the architecture of the system is fixed and known at the startup of you application.
18+
You can create a dedicated supervisor to manage the pipeline and any "side-car" processes (like a metrics reporter) as a single unit.
19+
```
20+
defmodule MyProject.InfrastructureSupervisor do
21+
use Supervisor
22+
23+
def start_link(init_arg) do
24+
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
25+
end
26+
27+
@impl true
28+
def init(_init_arg) do
29+
children = [
30+
{MyProject.MetricsReporter, name: MetricsReporter},
31+
{MyProject.Pipeline, [input_url: "rtmp://127.0.0.1:1935/app/key"]}
32+
]
33+
34+
Supervisor.init(children, strategy: :one_for_all)
35+
end
36+
end
37+
```
38+
Use `:one_for_all` strategy to make sure the `MetricsReporter` get restarted each time the pipeline restarts.
39+
40+
To launch this when your app boots, add the InfrastructureSupervisor to the children list in your application.ex:
41+
42+
```
43+
defmodule MyProject.Application do
44+
use Application
45+
46+
@impl true
47+
def start(_type, _args) do
48+
children = [
49+
MyProject.InfrastructureSupervisor
50+
]
51+
52+
opts = [strategy: :one_for_one, name: MyProject.Supervisor]
53+
Supervisor.start_link(children, opts)
54+
end
55+
end
56+
```
57+
58+
### Dynamic Spawning the pipelines spawned dynamically under DynamicSupervisor
59+
60+
Since we already have the InfrastructureSupervisor, implementation is straightforward.
61+
Instead of starting the `InfrastructureSupervisor` directly in the application, you start a `DynamicSupervisor` there.
62+
63+
```
64+
65+
defmodule MyProject.Application do
66+
use Application
67+
68+
@impl true
69+
def start(_type, _args) do
70+
children = [
71+
{DynamicSupervisor, strategy: :one_for_one, name: MyProject.DynamicSupervisor}
72+
]
73+
74+
opts = [strategy: :one_for_one, name: MyProject.Supervisor]
75+
Supervisor.start_link(children, opts)
76+
end
77+
end
78+
```
79+
Then, whenever a new pipeline is needed, you use `DynamicSupervisor.start_child()`` to spawn a new instance of the `InfrastructureSupervisor`.
80+
```
81+
{:ok, pid} = DynamicSupervisor.start_child(
82+
MyProject.PipelineDynamicSupervisor,
83+
{MyProject.InfrastructureSupervisor, [input_url: "rtmp://127.0.0.1:1935/app/key"]}
84+
)
85+
```
86+
87+
### Batch Processing
88+
89+
Batch processing is applicable in a scenario where you are performing some "offline task". It's good to be sure that the task has finished sucessfully.
90+
Let's assume that we want to transcode and transmux H.264 in MP4 container into VP8 in Matrioska container.
91+
To do so, we need to build the pipeline like this:
92+
```
93+
defmodule TranscodePipeline do
94+
use Membrane.Pipeline
95+
96+
alias Membrane.File.{Source, Sink}
97+
alias Membrane.H264.FFmpeg.Parser
98+
alias Membrane.FFmpeg.SWScale.PixelFormatConverter
99+
100+
@impl true
101+
def handle_init(_ctx, opts) do
102+
structure = [
103+
child(:source, %Source{location: opts[:input_path]})
104+
|> child(:demuxer, Membrane.MP4.Demuxer.ISOM)
105+
|> via_out(:video)
106+
|> child(:parser, %Parser{generate_best_effort_timestamps: true})
107+
|> child(:decoder, Membrane.H264.FFmpeg.Decoder)
108+
|> child(:converter, %PixelFormatConverter{format: :I420})
109+
|> child(:encoder, Membrane.VP8.Encoder)
110+
|> child(:muxer, Membrane.Matroska.Muxer)
111+
|> child(:sink, %Sink{location: opts[:output_path]})
112+
]
113+
114+
{[spec: structure], %{}}
115+
end
116+
end
117+
```
118+
119+
Next let's create an Oban worker. Using it will ensure that the transcoding is restarted if it fails.
120+
```
121+
defmodule VideoTranscoderWorker do
122+
@timeout_sec :time
123+
124+
use Oban.Worker, queue: :media, unique: [period: 60], timeout: :timer.seconds(10)
125+
126+
@impl true
127+
def perform(%Oban.Job{args: %{"input_path" => input, "output_path" => output}}) do
128+
{:ok, _supervisor_pid, pipeline_pid} = Membrane.Pipeline.start_link(
129+
TranscodePipeline,
130+
[input_path: input, output_path: output]
131+
)
132+
133+
case Membrane.Pipeline.wait_for_termination(pipeline_pid, 100_000) do
134+
:ok -> :ok
135+
{:error, reason} -> {:error, reason}
136+
end
137+
end
138+
end
139+
```
140+
141+
Now we can run the Oban worker:
142+
```
143+
%{
144+
"input_path" => "uploads/raw_video.mp4",
145+
"output_path" => "processed/optimized_video.mkv"
146+
}
147+
|> VideoTranscoderWorker.new()
148+
|> Oban.insert()
149+
```

0 commit comments

Comments
 (0)