Skip to content
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

Basic tutorial v1.0 #88

Merged
merged 18 commits into from
Dec 22, 2023
65 changes: 34 additions & 31 deletions basic_pipeline/04_StreamFormat.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,29 @@ Module name defines the type of the format, however it is possible to pass some

2. We specify the pad of the element with the format we have just defined, using the `:accepted_format` option. For the purpose of an example, let it be the `:input` pad:

```elixir
def_input_pad :input,
demand_unit: :buffers,
accepted_format:
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 480, height: 300}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60
```
As you can see, the argument of that option is simply a match pattern. The incoming stream format is later confronted against that match pattern. If it does not match, an exception is thrown at the runtime.

To simplify the pattern definition, there is `any_of/1` helper function that allows to define a alternative of match patterns - the matching will succeed if the stream format received on the pad matches any of the patterns listed as `any_of/1` argument. Below you can see an example of defining alternative of match patterns:

```elixir
def_input_pad :input,
demand_unit: :buffers,
accepted_format:
any_of([
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 480, height: 300}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60,
%Format.Raw{pixel_format: pixel_format, framerate: range(30, 60), width: 720, height: 480}
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 720, height: 480}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60
])
```

As you can see, we pass a list of compatible formats, each described with the tuple, consisting of our module name, and the keywords list fulfilling the
structure defined in that module. For the format's options, we can use the `range/2` or `one_of/1` specifier, which will modify the way in which the comparison between the accepted specification and the actual format received by the element is performed.

3. Once the `:stream_format` event comes to the element's pad, the format description sent in that event is confronted with each of the formats in the specification list of the pad. If the event's format description matches even one of the formats present in the list it means that they are matching.

- We have used `framerate: range(30, 60)`, so will accept the framerate value in the given interval, between 30 and 60 FPS.
- We have also used `pixel_format: one_of([:I420, :I422]`, and that will accept formats, whose pixel format is either I420 or I422
- We have used a plain value to specify the `width` and the `height` of a picture - the format will match if that option will be equal to the value passed in the specification

4. As noted previously, one can specify the format as `:any`. Such a specification will match all the formats sent on the pad, however, it is not a recommended way to develop the element - formats are there for a reason!

Our journey with stream formats does not end here. We know how to describe their specification...but we also need to make our elements send the `:stream_format` events so that the following elements will be aware of what type of data our element is producing!

An element can send a stream format as one of the [actions](https://hexdocs.pm/membrane_core/Membrane.Element.Action.html) it can take - the [`:stream_format` action](https://hexdocs.pm/membrane_core/Membrane.Element.Action.html#t:stream_format/0).
Expand All @@ -89,10 +89,12 @@ Here is the definition of the source element:
defmodule Source do
def_output_pad(:output,
demand_unit: :buffers,
stream_format: [
{Format.Raw, pixel_format: one_of([:I420, :I422]), framerate: range(30, 60), width: 480, height: 300},
{Format.Raw, pixel_format: one_of([:I420, :I422]), framerate: range(30, 60), width: 720, height: 480}
]
stream_format: any_of([
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 480, height: 300}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60,
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 720, height: 480}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60
])
)
...
def handle_playing(_context, state) do
Expand All @@ -103,36 +105,37 @@ defmodule Source do

While returning from the `handle_playing/2` callback, the element will send the format described by the `Formats.Raw` structure, through the `:output` pad.
Will this format meet the accepted specification provided by us? Think about it!
In fact, it will. The format matches (both in the event being sent and in the accepted specification of the pad, we have `Format.Raw` module). When it comes to the options, we see, that `I420` is in the `one_of` list, acceptable by the specification format for `width` equal to 720 and `height` equal to 480, and the `framerate`, equal to 45, is in the `range` between 30 and 60, as defined in the specification.
It means that the format can be sent through the `:output` pad.
In fact, it will, as the `Formats.Raw` structure sent with `:stream_format` action matches the pattern - the value of `:pixel_format` field is one of `:I420` and `:I422`, and the `:framerate` is in the range between 30 and 60. In case the structure didn't match the pattern, a runtime exception would be thrown.

Below there is the draft of the filter implementation:

```elixir
# Filter

defmodule Filter do
def_input_pad(:input,
def_input_pad:input,
demand_unit: :buffers,
accepted_format: [
{Format.Raw, pixel_format: one_of([:I420, :I422]), framerate: range(30, 60), width: 480, height: 300},
{Format.Raw, pixel_format: one_of([:I420, :I422]), framerate: range(30, 60), width: 720, height: 480}
]
)
accepted_format: any_of([
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 480, height: 300}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60,
%Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 720, height: 480}
when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60
])

def_output_pad(:output,
demand_unit: :buffers,
accepted_format: {Format.Raw, pixel_format: one_of([:I420, :I422]), framerate: range(30, 60), width: 480, height: 300},
)
def_output_pad :output,
demand_unit: :buffers,
accepted_format: %Format.Raw{pixel_format: pixel_format, framerate: framerate, width: 480,height: 300} when pixel_format in [:I420, :I422] and framerate >= 30 and framerate <= 60

...

def handle_stream_format(_pad, _stream_format, _context, state) do
...
{ {:ok, [stream_format: {:output, %Formats.Raw{pixel_format: I420, framerate: 60, width: 480, height:300} }]}, state}
...
{ {[stream_format: {:output, %Formats.Raw{pixel_format: I420, framerate: 60, width: 480, height: 300} }]}, state}
end

end
```

When we receive the spec on the input pad, we do not propagate it to our `:output` pad - instead, we send a different format, with reduced quality (width and height options are lower).
When we receive the spec on the input pad, we do not propagate it to our `:output` pad - instead, we send a different format, with reduced quality (values of the `width` and `height` fields might be lower).

We hope by now you have a better understanding of what stream formats are. This knowledge will be helpful in the following chapters.
6 changes: 3 additions & 3 deletions basic_pipeline/07_Redemands.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ end
```

## In Filter elements
<!-- This needs jesus __jm__ -->
In the filter element, the situation is quite different.
Since the filter's responsibility is to process the data sent via the input pads and transmit it through the output pads, there is no 'side-channel' from which we could take data. That is why in normal circumstances you would transmit the buffer through the output pad in the `handle_buffer/4` callback (which means - once your element receives a buffer, you process it, and then you 'mark' it as ready to be output with the `:buffer` action). When it comes to the `handle_demand/5` action on the output pad, all you need to do is to demand the appropriate number of buffers on the element's input pad. The behavior which is easy to specify when we exactly know how many input buffers correspond to the one output buffer (recall the situation in the [Depayloader](../glossary/glossary.md#payloader-and-depayloader) of our pipeline, where we *a priori* knew, that each output buffer ([frame](../glossary/glossary.md#frame)) consists of a given number of input buffers ([packets](../glossary/glossary.md#packet))), becomes impossible to define if the output buffer might be a combination of a discretionary set number of input buffers. However, we have dealt with an unknown number of required buffers in the OrderingBuffer implementation, where we didn't know how many input buffers do we need to demand to fulfill the missing spaces between the packets ordered in the list. How did we manage to do it?
Since the filter's responsibility is to process the data sent via the input pads and transmit it through the output pads, there is no 'side-channel' from which we could take data. That is why in normal circumstances you would transmit the buffer through the output pad in the `handle_buffer/4` callback (which means - once your element receives a buffer, you process it, and then you 'mark' it as ready to be output with the `:buffer` action). When it comes to the `handle_demand/5` action on the output pad, all you need to do is to demand the appropriate number of buffers on the element's input pad. That behavior is easy to specify when we exactly know how many input buffers correspond to the one output buffer (recall the situation in the [Depayloader](../glossary/glossary.md#payloader-and-depayloader) of our pipeline, where we *a priori* knew, that each output buffer ([frame](../glossary/glossary.md#frame)) consists of a given number of input buffers ([packets](../glossary/glossary.md#packet))). However it becomes impossible to define if the output buffer might be a combination of a discretionary set number of input buffers. At the same time, we have dealt with an unknown number of required buffers in the OrderingBuffer implementation, where we didn't know how many input buffers do we need to demand to fulfill the missing spaces between the packets ordered in the list. How did we manage to do it?
kidq330 marked this conversation as resolved.
Show resolved Hide resolved
We simply used the `:redemand` action! In case there was a missing space between the packets, we returned the `:redemand` action, which immediately called the `handle_demand/5` callback (implemented in a way to request for a buffer on the input pad). The fact, that that callback invocation was immediate, which means - the callback was called synchronously, right after returning from the `handle_buffer/4` callback, before processing any other message from the element's mailbox - might be crucial in some situations, since it makes us sure, that the demand will be done before handling any other event.
kidq330 marked this conversation as resolved.
Show resolved Hide resolved
Recall the situation in the [Mixer](../glossary/glossary.md#mixer), <!--this might've changed after refactor--> where we were producing the output buffers right in the `handle_demand/5` callback. We needed to attempt to create the output buffer after:
Recall the situation in the [Mixer](../glossary/glossary.md#mixer), where we were producing the output buffers right in the `handle_demand/5` callback. We needed to attempt to create the output buffer after:

- updating the buffers' list in `handle_buffer/4`
- updating the status of the [track](../glossary/glossary.md#track) in `handle_end_of_stream/3`
Therefore, we were simply returning the `:redemand` action, and the `handle_demand/5` was called sequentially after on, trying to produce the output buffer.

As you can see, redemand mechanism in filters helps us deal with situations, where we do not know how many input buffers to demand in order to be able to produce an output buffer/buffers.
In case we don't provide enough buffers in the `handle_demand/5` callback (or we are not sure that we do provide), we should call `:redemand` somewhere else (usually in the `handle_buffer/4`) to make sure that the demand is not lost.

With that knowledge let's carry on with the next element in our pipeline - `Depayloader`.