|
1 | 1 | # Everything about pads |
2 | 2 |
|
3 | | -When developing some intuition about the structure of pipelines the pads are |
| 3 | +When developing intuition about the structure of pipelines pads are |
4 | 4 | something that can't be ignored. If you think about elements and bins as some |
5 | | -sort of containers in which some processing happens, then pads are the parts |
6 | | -that with these tanks are connected with. A pad of one element can only connect |
7 | | -to a single pad of other element and only once two pads are connected the media |
8 | | -can flow through them. |
| 5 | +sort of containers or boxes in which processing happens, then pads are the parts |
| 6 | +that with these containers are connected with. There are some constraints |
| 7 | +regarding pads: |
9 | 8 |
|
10 | | -`t:Membrane.Pad.element_spec/0` |
| 9 | +* A pad of one element can only connect to a single pad of other element and |
| 10 | +only once two pads are connected communication through them can happen. |
| 11 | +* One pad needs to be an input pad, and the other an output pad. |
| 12 | +* The accepted stream formats of the pads need to match. |
| 13 | + |
| 14 | +When looking at the insides of elements, the pads are their main way to |
| 15 | +communicate with other elements in the pipeline. When an element receives |
| 16 | +something from another one (e.g. a buffer or stream format), it receives it |
| 17 | +from a pad. The reference to this pad is then also available as an argument |
| 18 | +of the callback that handles the received thing. For example an invocation |
| 19 | +of the [following callback](`c:Membrane.Element.WithInputPads.handle_buffer/4`) |
| 20 | +would mean that a buffer `buffer` has arrived on a pad `some_pad`: |
| 21 | + |
| 22 | +```elixir |
| 23 | +@impl true |
| 24 | +def handle_buffer(some_pad, buffer, context, state) do |
| 25 | + ... ^^^^^^^^ |
| 26 | +end |
| 27 | +``` |
| 28 | + |
| 29 | +When an element wants to send something to another element in the |
| 30 | +pipeline, most likely it should send it on a pad that's connected to it. It |
| 31 | +can do that by using the pad reference in actions that send things, for example |
| 32 | +returning the following [buffer action](`t:Membrane.Element.Action.buffer/0`) |
| 33 | +from a callback would mean that a buffer `buffer` will be sent on a pad `some_pad`: |
| 34 | + |
| 35 | +```elixir |
| 36 | +@impl true |
| 37 | +def some_callback(...) do |
| 38 | + ... |
| 39 | + {[buffer: {some_pad, buffer}], state} |
| 40 | +end ^^^^^^^^ |
| 41 | +``` |
| 42 | + |
| 43 | +## Defining pads |
| 44 | + |
| 45 | +To define what pads an element will have and how they'll behave we use |
| 46 | +[`def_input_pad/2`](`Membrane.Element.WithInputPads.def_input_pad/2`) |
| 47 | +and [`def_output_pad/2`](`Membrane.Element.WithOutputPads.def_output_pad/2`) macros. |
| 48 | +Input pads can only be defined for Sinks, Filters and Endpoints, and output |
| 49 | +pads can only be defined for Sources, Filters and Endpoints. |
| 50 | +The first argument for these macros is a name, which then will be used to |
| 51 | +identify the pads. The second argument is a `t:Membrane.Pad.element_spec/0` |
| 52 | +keyword list, which is used to define how this pad will work. An option we'll |
| 53 | +now focus on is [`availability`](`t:Membrane.Pad.availability/0`), which |
| 54 | +determines if the pad is _static_ or _dynamic_. |
| 55 | + |
| 56 | +### Static pads |
| 57 | + |
| 58 | +Static pads are pretty straightforward - when a static pad is defined there |
| 59 | +will always be exactly one instance of this pad and it's referenced by it's |
| 60 | +name. |
| 61 | + |
| 62 | +#### File Source Example |
| 63 | + |
| 64 | +Example of an element with only static pads is a [File Source](https://hexdocs.pm/membrane_file_plugin/Membrane.File.Source.html). |
| 65 | +This element reads contents of a file and sends them in batches through a static |
| 66 | +output pad. The content of the buffers sent by this element is unknown - the file |
| 67 | +that's being read can contain anything - so this pad has `:accepted_format` set to |
| 68 | +`%RemoteStream{type: :bytestream}`. That means that any stream format that |
| 69 | +matches on this struct can be sent on the output pad and this fact has to be |
| 70 | +accounted for when connecting an element after the source. |
| 71 | + |
| 72 | +A pipeline spec with a file source passing buffers to a MP4 demuxer could look |
| 73 | +like this: |
| 74 | + |
| 75 | +```elixir |
| 76 | +@impl true |
| 77 | +def handle_init(_context, state) do |
| 78 | + spec = |
| 79 | + child(:source, %Membrane.File.Source{location: "my_file.mp4"}) |
| 80 | + |> via_out(:output) |
| 81 | + |> via_in(:input) |
| 82 | + |> child(:mp4_demuxer, Membrane.MP4.ISOM.Demuxer) |
| 83 | + |
| 84 | + {[spec: spec], state} |
| 85 | +end |
| 86 | +``` |
| 87 | + |
| 88 | +This spec will connect a pad named `:output` of the source to a pad named |
| 89 | +`:input` of the demuxer. However this can be shortened - if an output pad is |
| 90 | +called `:output` or an input pad is called `:input`, their respective |
| 91 | +[`via_in/3`](`Membrane.ChildrenSpec.via_in/3`) and |
| 92 | +[`via_out/3`](`Membrane.ChildrenSpec.via_out/3`) calls can be omitted and |
| 93 | +Membrane will automatically recognize and connect them: |
| 94 | + |
| 95 | +```elixir |
| 96 | +@impl true |
| 97 | +def handle_init(_context, state) do |
| 98 | + spec = |
| 99 | + child(:source, %Membrane.File.Source{location: "my_file.mp4"}) |
| 100 | + |> child(:mp4_demuxer, Membrane.MP4.ISOM.Demuxer) |
| 101 | + |
| 102 | + {[spec: spec], state} |
| 103 | +end |
| 104 | +``` |
| 105 | + |
| 106 | +### Dynamic pads |
| 107 | + |
| 108 | +Dynamic pads are a bit more complex. They're used when the amount of pads of |
| 109 | +given type is variable - dependent on the processed stream or external factors. |
| 110 | +The creation of these pads is controlled by the parent of the element - if a |
| 111 | +[`:spec`](`t:Membrane.Pipeline.Action.spec/0`) action linking the dynamic pad is |
| 112 | +being executed, then the pad is created dynamically and the element needs to |
| 113 | +handle this, in most cases with |
| 114 | +[`handle_pad_added/3`](`c:Membrane.Element.Base.handle_pad_added/3`). |
| 115 | + |
| 116 | +Another thing that's different are the pad references. The pad's name can't just |
| 117 | +be used as the pad's reference, because it wouldn't be unique. Dynamic pads are |
| 118 | +identified by [`Pad.ref/2`](`Membrane.Pad.ref/2`), that takes the pad's |
| 119 | +name and some unique reference as arguments. The result is a unique pad reference |
| 120 | +that is also associated with a given pad's specification through it's name. |
| 121 | + |
| 122 | +#### MP4 Demuxer Example |
| 123 | + |
| 124 | +An example of an element using dynamic pads is an |
| 125 | +[MP4 Demuxer](https://hexdocs.pm/membrane_mp4_plugin/Membrane.MP4.Demuxer.ISOM.html). |
| 126 | +This element has a input pad, from which it receives contents of a MP4 |
| 127 | +container, and output pads, on which it'll send the different tracks |
| 128 | +that were in the container. The input pad can be static, however MP4 containers can |
| 129 | +have different numbers and kinds of tracks, so the output pad needs to be dynamic. |
| 130 | + |
| 131 | +We'll consider the case when we don't have any prior information about the |
| 132 | +tracks in this MP4 container. Because of this, the parent pipeline or bin of this |
| 133 | +demuxer won't initially know how many pads should be connected. To solve this |
| 134 | +problem the demuxer will identify the tracks in the incoming stream and send a |
| 135 | +message to it's parent in the form of |
| 136 | +[`{:new_tracks, [{track_id :: integer(), content :: struct()}]}`](https://hexdocs.pm/membrane_mp4_plugin/Membrane.MP4.Demuxer.ISOM.html#t:new_tracks_t/0). |
| 137 | +The list contains a list of tuples corresponding to tracks, where the first |
| 138 | +element is a track id and will be used to identify corresponding pad, |
| 139 | +and the second a stream format contained in the track. |
| 140 | + |
| 141 | +Initially the parent will only create the elements before and including the |
| 142 | +demuxer: |
| 143 | + |
| 144 | +```elixir |
| 145 | +@impl true |
| 146 | +def handle_init(_context, state) do |
| 147 | + spec = |
| 148 | + child(:source, %Membrane.File.Source{location: "my_file.mp4"}) |
| 149 | + |> child(:mp4_demuxer, Membrane.MP4.ISOM.Demuxer) |
| 150 | + |
| 151 | + {[spec: spec], state} |
| 152 | +end |
| 153 | +``` |
| 154 | + |
| 155 | +The source will start providing the demuxer the MP4 container content, |
| 156 | +from which the demuxer will identify tracks and notify it's parent |
| 157 | +about them. The parent now has to connect an output pad of the demuxer for each |
| 158 | +track received, which can look like this: |
| 159 | + |
| 160 | +```elixir |
| 161 | +@impl true |
| 162 | +def handle_child_notification({:new_tracks, tracks}, :mp4_demuxer, _context, state) do |
| 163 | + spec = |
| 164 | + Enum.map(tracks, fn {id, format} -> |
| 165 | + get_child(:mp4_demuxer) |
| 166 | + |> via_out(Pad.ref(:output, id)) |
| 167 | + |> ... |
| 168 | + end) |
| 169 | + |
| 170 | + {[spec: spec], state} |
| 171 | +end |
| 172 | +``` |
| 173 | + |
| 174 | +The elements that the output pads will be linked to should be based on what stream |
| 175 | +format is in `format` variable - different formats require different approaches. |
| 176 | + |
| 177 | +After this spec is returned, the demuxer will now have the |
| 178 | +[`handle_pad_added/3`](`c:Membrane.Element.Base.handle_pad_added/3`) callback |
| 179 | +called for each new connected pad with pad reference of |
| 180 | +`Pad.ref(:output, track_id)`. It will now know that these pads are connected and |
| 181 | +ready to pass buffers forward. |
0 commit comments