Skip to content

Commit ad9798b

Browse files
committed
Improve docs WiP
1 parent 4f30b98 commit ad9798b

5 files changed

Lines changed: 210 additions & 8 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Manual demands
2+
3+
Elements that use `:manual` flow control are responsible for explicitly
4+
requesting data from their input pads. This guide explains how that mechanism
5+
works and how to choose the right demand unit for your use case.
6+
7+
## How manual flow control works
8+
9+
When an input pad has `flow_control: :manual`, the element controls exactly
10+
how much data it wants to receive on that pad at any given time. Each such
11+
pad has an internal input queue that manages buffering and flow.
12+
13+
The general flow for a filter with a manual input and output looks like this:
14+
15+
1. **At startup**, the input queue issues an initial demand upstream to
16+
pre-fill itself to its target size. This happens automatically before the
17+
element returns its first `:demand` action, so that data is already
18+
available when the element first asks for it.
19+
20+
2. **When downstream demands data** (via `handle_demand/5`), the element
21+
requests some amount from its input queue using the
22+
[`:demand`](`t:Membrane.Element.Action.demand/0`) action.
23+
24+
3. **The queue delivers those buffers** synchronously from what it has
25+
buffered, invoking
26+
[`handle_buffer/4`](`c:Membrane.Element.WithInputPads.handle_buffer/4`).
27+
If the queue doesn't have enough, it delivers what it has and requests more
28+
from upstream.
29+
30+
4. **As the queue empties**, it automatically issues new demands upstream to
31+
stay close to its target size.
32+
33+
## Demand is overwritten, not accumulated
34+
35+
A critical property of `handle_demand/5`: the `demand_size` argument is the
36+
**total current demand** from downstream, not a delta. If the downstream
37+
element requested 5 buffers and then requested 16 more before the first
38+
request was fulfilled, `handle_demand` will be called once with `16` — not
39+
twice with `5` and `16`, and not once with `21`.
40+
41+
This means the element does not need to track previously received demands.
42+
It simply produces as many buffers as `handle_demand` says.
43+
44+
## Demand units
45+
46+
The `demand_unit` option in the pad spec controls how "how much data" is
47+
measured. It affects both the value passed to `handle_demand/5` on the
48+
upstream side and the size argument of the `:demand` action.
49+
50+
### `:buffers` and `:bytes`
51+
52+
These are the classic demand units.
53+
54+
With `:buffers`, the demand is a count of buffers. With `:bytes`, the demand
55+
is a number of payload bytes. When demanding by bytes, the input queue will
56+
split buffers at exact byte boundaries if needed — the resulting fragments
57+
share the same metadata and timestamps as the original buffer.
58+
59+
```elixir
60+
def_input_pad :input,
61+
flow_control: :manual,
62+
demand_unit: :buffers,
63+
accepted_format: _any
64+
65+
@impl true
66+
def handle_demand(:output, demand_size, :buffers, _ctx, state) do
67+
buffers = produce_buffers(demand_size, state)
68+
{[buffer: {:output, buffers}], state}
69+
end
70+
```
71+
72+
#### Deadlocks
73+
74+
The most common pitfall with `:buffers`/`:bytes` demands is a deadlock: the
75+
downstream element is waiting for buffers that will never arrive because the
76+
upstream element stopped producing.
77+
78+
This happens when a filter produces fewer buffers than demanded. For example,
79+
if downstream demanded 10 and the filter only sent 8, the downstream will wait
80+
forever for the remaining 2 — unless something triggers another `handle_demand`
81+
call.
82+
83+
There are two ways to avoid this:
84+
85+
1. **Track the shortfall explicitly** — if the filter knows it sent fewer
86+
buffers than demanded, it can issue an explicit `:demand` action on its
87+
input to trigger another round of processing.
88+
89+
2. **Use the `:redemand` action** — returning
90+
[`:redemand`](`t:Membrane.Element.Action.redemand/0`) causes Membrane to
91+
re-invoke `handle_demand` with the remaining unfulfilled demand. This is a
92+
safe way to signal "I may not be done yet, check again." If there is no
93+
outstanding demand on the output, `:redemand` has no effect.
94+
95+
> #### Deadlocks are intermittent {: .warning}
96+
>
97+
> Deadlocks caused by under-producing are especially tricky because they only
98+
> manifest sometimes: if downstream happens to issue a new demand while the
99+
> filter is still processing, the deadlock is avoided. This makes them easy to
100+
> miss in testing.
101+
102+
#### Sending slightly more than demanded
103+
104+
It is acceptable to send slightly more buffers than demanded. The extra buffers
105+
will be buffered in the downstream queue. This can be useful to avoid waiting
106+
for one more demand round-trip when a buffer is already ready.
107+
108+
However, the overshoot should always be a small, **bounded constant** — never
109+
proportional to the stream or unbounded. If the filter always sends `n+k`
110+
buffers when asked for `n`, the downstream queue will grow by `k` on every
111+
cycle and eventually overflow.
112+
113+
### Timestamp demand units
114+
115+
Timestamp demand units let an element request data in terms of _time duration_
116+
rather than buffer count or byte size. This is useful when the element needs to
117+
process a fixed time window of media regardless of how many buffers that window
118+
contains — for example, always consuming exactly 40 ms of audio.
119+
120+
Timestamp demand units are only applicable to **input pads**. Output pads do
121+
not support them. If an input pad uses a timestamp demand unit and the upstream
122+
element's linked output pad does not specify a `demand_unit`, that element will
123+
receive `handle_demand/5` with the demand expressed in `:buffers`.
124+
125+
The available variants are:
126+
127+
- `:timestamp` or `{:timestamp, :dts_or_pts}` — uses DTS if present on a
128+
buffer, falls back to PTS.
129+
- `{:timestamp, :pts}` — uses the PTS field of each buffer.
130+
- `{:timestamp, :dts}` — uses the DTS field of each buffer.
131+
132+
When using a timestamp unit, the demand size is a `t:Membrane.Time.t/0`
133+
value (nanoseconds). The queue delivers buffers until the elapsed timestamp
134+
span — measured as `last_consumed_buffer.ts - first_consumed_buffer.ts`
135+
reaches the demanded duration.
136+
137+
**Key difference from `:buffers`/`:bytes`**: timestamp demand does _not_
138+
decrement as buffers are consumed. The demanded duration is a fixed threshold,
139+
not a shrinking counter. Once the demanded window has been satisfied, the
140+
element must issue a new, _larger_ demand to make progress. Issuing the same
141+
demand again after the window has elapsed will produce a warning and deliver no
142+
buffers.
143+
144+
A typical pattern for time-based pacing:
145+
146+
```elixir
147+
def_input_pad :input,
148+
flow_control: :manual,
149+
demand_unit: {:timestamp, :dts},
150+
accepted_format: _any
151+
152+
@impl true
153+
def handle_init(_ctx, opts) do
154+
{[], %{window_end: Membrane.Time.milliseconds(40)}}
155+
end
156+
157+
@impl true
158+
def handle_demand(:output, _demand_size, :buffers, _ctx, state) do
159+
# Advance the window by 40 ms each time the downstream asks for more data
160+
{[demand: {:input, state.window_end}],
161+
%{state | window_end: state.window_end + Membrane.Time.milliseconds(40)}}
162+
end
163+
```
164+
165+
In this example, successive demands are `40ms`, `80ms`, `120ms`, … All are
166+
measured from the timestamp of the very first buffer ever consumed on the pad,
167+
so each demand naturally covers the next 40 ms slice of the stream.
168+
169+
> #### Buffers must carry timestamps {: .warning}
170+
>
171+
> When using a timestamp demand unit, every buffer in the stream must have
172+
> its relevant timestamp field (`:pts` or `:dts`) set to a non-`nil` value.
173+
> Missing timestamps will cause the queue to behave incorrectly.
174+
175+
> #### Timestamps should be monotonic {: .warning}
176+
>
177+
> The queue assumes that timestamps are non-decreasing. Non-monotonic
178+
> timestamps (e.g. those produced by B-frame reordering when using `:pts`)
179+
> will trigger a warning. Prefer `{:timestamp, :dts}` for streams where DTS
180+
> is available and monotonic.

guides/useful_concepts/pads.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,10 @@ sent to whatever component is connected to the bin's newly created pad.
340340
It's worth noting that pads of bins are only an abstraction. When a component
341341
links with a bin, it actually links directly to one of the components inside of
342342
it to avoid unnecessary forwarding of messages.
343+
344+
## Demand units
345+
346+
Input pads with `:manual` flow control use a _demand unit_ to determine how
347+
data is measured when an element requests buffers. For a detailed explanation
348+
of the available units — including timestamp-based demands — see the
349+
[Manual demands](manual_demands.md) guide.

lib/membrane/element/action.ex

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,31 @@ defmodule Membrane.Element.Action do
8181
`c:Membrane.WithInputPads.handle_buffer/4` callback. Invoked callback is
8282
guaranteed not to receive more data than demanded.
8383
84-
Demand size can be either a non-negative integer, that overrides existing demand,
85-
or a function that is passed current demand, and is to return the new demand. In case only pad
84+
## `:buffers` and `:bytes` demand units
85+
86+
When the pad's `demand_unit` is `:buffers` or `:bytes`, the demand size is a
87+
non-negative integer that overrides the existing demand, or a function that
88+
receives the current demand and returns the new demand. If only the pad ref
8689
is specified, the demand size defaults to 1.
8790
91+
## Timestamp demand units
92+
93+
When the pad's `demand_unit` is `:timestamp`, `{:timestamp, :pts}`,
94+
`{:timestamp, :dts}`, or `{:timestamp, :dts_or_pts}`, the demand size is a
95+
`t:Membrane.Time.t/0` duration (in nanoseconds). The queue will deliver
96+
buffers until the elapsed timestamp span (last consumed timestamp minus
97+
first consumed timestamp) reaches the demanded duration.
98+
99+
Unlike `:buffers`/`:bytes` demands, **timestamp demand does not decrement** as buffers
100+
are consumed. To make progress after the demanded window has elapsed, the
101+
element must re-issue a larger demand (e.g. advancing the window by the
102+
desired step). Issuing the same demand again after the window has been
103+
satisfied will result in a warning and no buffers being delivered.
104+
88105
Allowed only when playback is playing.
89106
"""
90107
@type demand :: {:demand, {Pad.ref(), demand_size} | Pad.ref()}
91-
@type demand_size :: pos_integer | (pos_integer() -> non_neg_integer())
108+
@type demand_size :: pos_integer | Membrane.Time.t() | (pos_integer() -> non_neg_integer())
92109

93110
@typedoc """
94111
Pauses auto-demanding on the specific pad.

lib/membrane/pad.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ defmodule Membrane.Pad do
130130

131131
@typedoc """
132132
Describes how a pad should be declared inside an element.
133+
134+
For details on `demand_unit` and how the different units affect demand
135+
behaviour, see the [Manual demands](manual_demands.md) guide.
133136
"""
134137
@type element_spec ::
135138
{name(),

test/membrane/core/element/input_queue_test.exs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,15 +359,10 @@ defmodule Membrane.Core.Element.InputQueueTest do
359359
queue = InputQueue.store(queue, buffers)
360360
assert queue.size == 5
361361

362-
# Demand 300ms: consume buffers until dts - first_dts >= 300ms
363-
# dts values: 0, 100, 250, 350ms → 350 - 0 = 350ms >= 300ms, stops there
364362
{out, queue} = InputQueue.take(queue, Membrane.Time.milliseconds(300))
365363
assert bufs_size(out, :buffers) == 4
366364
assert queue.size == 1
367365

368-
# The demand value does not decrement for timestamp metrics. Taking again with
369-
# the same demand (300ms) triggers a warning since 350ms already elapsed, and
370-
# returns no buffers — the element must demand a larger timestamp to proceed.
371366
{out, queue} = InputQueue.take(queue, Membrane.Time.milliseconds(300))
372367
assert bufs_size(out, :buffers) == 0
373368
assert queue.size == 1

0 commit comments

Comments
 (0)