|
| 1 | +--- |
| 2 | +name: membrane-core |
| 3 | +description: Work with the Membrane multimedia streaming framework in Elixir. Use this skill whenever the user is building or debugging Membrane pipelines, writing custom Elements, Bins, or Filters, connecting pads, implementing callbacks, handling stream formats or EOS, or asking about Membrane architecture. Trigger on any mention of membrane_core, Membrane.Pipeline, Membrane.Element, Membrane.Bin, Membrane.Pad, or multimedia streaming in an Elixir context — even if the user doesn't say "Membrane" explicitly but is clearly working on this codebase. |
| 4 | +--- |
| 5 | + |
| 6 | +# Membrane Framework |
| 7 | + |
| 8 | +**Package**: `membrane_core` ~> 1.2 | **Docs**: https://hexdocs.pm/membrane_core/ | **Demos**: https://github.com/membraneframework/membrane_demo |
| 9 | + |
| 10 | +## How to Approach Tasks |
| 11 | + |
| 12 | +- **New component** — identify subtype (Source/Filter/Sink/Endpoint/Bin), define pads, implement required callbacks (`handle_buffer/4` for filters/sinks, `handle_demand/5` for manual-flow sources) |
| 13 | +- **Pipeline topology** — use the ChildrenSpec DSL (`child/2`, `get_child/1`, `via_in/2`, `via_out/2`); see Integration Patterns below or `references/integration.md` |
| 14 | +- **Dynamic tracks** (demuxers, variable inputs) — use the Dynamic Pads Pattern below |
| 15 | +- **Debugging** — check pad `accepted_format` compatibility, lifecycle ordering (`handle_setup` vs `handle_playing`), flow control mode mismatches |
| 16 | +- **Full callback reference** — `references/callbacks.md` |
| 17 | +- **Full actions reference** — `references/actions.md` |
| 18 | +- **Never modify code in `deps/`** |
| 19 | +- **Use `mix hex.info <plugin name>` when you need to check the newest version of a plugin** |
| 20 | +- **Search for appropriate plugins in `README.md`, in `all-packages` section** |
| 21 | +- **Check input and output pad definitions of elements in `deps/` (use `cat <filename> | grep def_input_pad` and `cat <filename> | grep def_output pad`) to make sure output pad's `accepted_stream_format` is compatible with `accepted_stream_format` of the input pad which it is linked to.** |
| 22 | +- **If the `accepted_stream_format` doesn't match, search for an element which can act as an adapter** |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Architecture |
| 27 | + |
| 28 | +``` |
| 29 | +Pipeline |
| 30 | +├── Element (Source/Filter/Sink/Endpoint) ← leaf, processes data |
| 31 | +├── Bin ← dual role: parent (has children) + child (has pads) |
| 32 | +│ ├── Element |
| 33 | +│ └── Bin ← bins nest arbitrarily deep |
| 34 | +└── ... |
| 35 | +``` |
| 36 | + |
| 37 | +| Type | Parent | Child | Has Pads | |
| 38 | +|------|--------|-------|----------| |
| 39 | +| **Pipeline** | yes | no | no | |
| 40 | +| **Bin** | yes | yes | yes | |
| 41 | +| **Element** | no | yes | yes | |
| 42 | + |
| 43 | +Element subtypes: **Source** (output only) · **Filter** (in + out, output is transformed input) · **Sink** (input only) · **Endpoint** (in + out, but output might be not related to input) |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## Pads |
| 48 | + |
| 49 | +Defined on Elements and Bins (not Pipelines): |
| 50 | + |
| 51 | +```elixir |
| 52 | +def_input_pad :input, accepted_format: _any |
| 53 | +def_output_pad :output, accepted_format: Membrane.RawAudio, flow_control: :auto |
| 54 | +``` |
| 55 | + |
| 56 | +- **Availability**: `:always` (static, one instance, referenced by atom) or `:on_request` (dynamic, reference via `Pad.ref(:name, id)`) |
| 57 | +- **Flow control**: `:auto` (framework manages demand — preferred), `:manual` (explicit via `:demand`/`:redemand`), `:push` (no demand, risk of overflow) |
| 58 | +- One input pad ↔ one output pad only; pads must have compatible `accepted_format` |
| 59 | +- Default pad names `:input`/`:output` allow omitting `via_in`/`via_out` in specs |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## Component Lifecycle |
| 64 | + |
| 65 | +``` |
| 66 | +handle_init/2 sync, blocks parent — parse opts, return initial spec |
| 67 | +handle_setup/2 async — heavy init (open files, connect services) |
| 68 | + return {[setup: :incomplete], state} to delay :playing |
| 69 | +handle_pad_added/3 fires for dynamic pads linked in the same spec |
| 70 | +handle_playing/2 component is ready — start producing/consuming data |
| 71 | +``` |
| 72 | + |
| 73 | +All components spawned in the same `:spec` action enter `:playing` together (slowest setup wins). Elements and Bins wait for their parent before `handle_playing/2`. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## Pipeline & Bin DSL (ChildrenSpec) |
| 78 | + |
| 79 | +```elixir |
| 80 | +# Linear chain — child/2 spawns a new named child |
| 81 | +child(:source, %Membrane.File.Source{location: "input.mp4"}) |
| 82 | +|> child(:filter, MyFilter) |
| 83 | +|> child(:sink, %Membrane.File.Sink{location: "out.raw"}) |
| 84 | + |
| 85 | +# Explicit pad names (required for non-default names or dynamic pads) |
| 86 | +get_child(:demuxer) |
| 87 | +|> via_out(Pad.ref(:output, track_id)) |
| 88 | +|> via_in(:video_input) |
| 89 | +|> child(:decoder, Membrane.H264.FFmpeg.Decoder) |
| 90 | + |
| 91 | +# Link to an already-existing child |
| 92 | +get_child(:existing_filter) |> child(:new_sink, MySink) |
| 93 | + |
| 94 | +# Inside a Bin |
| 95 | +bin_input(:input) |> child(:filter, MyFilter) |> bin_output(:output) |
| 96 | +``` |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Dynamic Pads Pattern |
| 101 | + |
| 102 | +The standard approach for variable-track streams (e.g. MP4 demuxers): |
| 103 | + |
| 104 | +```elixir |
| 105 | +# 1. Spawn source + demuxer; demuxer hasn't identified tracks yet |
| 106 | +def handle_init(_ctx, state) do |
| 107 | + {[spec: child(:source, Source) |> child(:demuxer, Demuxer)], state} |
| 108 | +end |
| 109 | + |
| 110 | +# 2. Demuxer notifies parent once tracks are known |
| 111 | +def handle_child_notification({:new_tracks, tracks}, :demuxer, _ctx, state) do |
| 112 | + spec = Enum.map(tracks, fn {id, _fmt} -> |
| 113 | + get_child(:demuxer) |
| 114 | + |> via_out(Pad.ref(:output, id)) |
| 115 | + |> child({:decoder, id}, Decoder) |
| 116 | + |> child({:sink, id}, Sink) |
| 117 | + end) |
| 118 | + {[spec: spec], state} |
| 119 | +end |
| 120 | +``` |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## Built-in Utility Elements |
| 125 | + |
| 126 | +| Module | Purpose | |
| 127 | +|--------|---------| |
| 128 | +| `Membrane.Funnel` | Multiple inputs → one output | |
| 129 | +| `Membrane.Tee` | One input → multiple outputs | |
| 130 | +| `Membrane.Connector` | Connect dynamic pads with internal buffering | |
| 131 | +| `Membrane.FilterAggregator` | Run multiple filters in a single process | |
| 132 | +| `Membrane.Testing.Source` | Inject buffers into a pipeline in tests | |
| 133 | +| `Membrane.Testing.Sink` | Capture and assert on buffers in tests | |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Testing |
| 138 | + |
| 139 | +```elixir |
| 140 | +import Membrane.ChildrenSpec |
| 141 | +import Membrane.Testing.Assertions |
| 142 | +alias Membrane.Testing |
| 143 | + |
| 144 | +pipeline = Testing.Pipeline.start_link_supervised!(spec: [ |
| 145 | + child(:source, %Testing.Source{output: [<<1, 2, 3>>, <<4, 5, 6>>]}) |
| 146 | + |> child(:sink, Testing.Sink) |
| 147 | +]) |
| 148 | + |
| 149 | +assert_sink_buffer(pipeline, :sink, %Membrane.Buffer{payload: <<1, 2, 3>>}) |
| 150 | +``` |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## Timing |
| 155 | + |
| 156 | +All timestamps are `Membrane.Time.t()` (integer nanoseconds). Helpers: `Membrane.Time.seconds/1`, `Membrane.Time.milliseconds/1`, `Membrane.Time.microseconds/1`, etc. Timers started with `:start_timer` action fire `handle_tick/3`. |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## Reference Files |
| 161 | + |
| 162 | +- `references/callbacks.md` — full callback tables for every component type |
| 163 | +- `references/actions.md` — all actions with signatures and usage notes |
| 164 | +- `references/integration.md` — integration patterns, demo examples, key source file locations |
0 commit comments