Skip to content
21 changes: 21 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "membrane-framework",
"description": "Membrane multimedia streaming framework skill for Elixir — provides architectural guidance, callback and action references, and code patterns for building pipelines, elements, and bins with membrane_core.",
"owner": {
"name": "Software Mansion",
"url": "https://github.com/membraneframework"
},
"plugins": [
{
"name": "membrane-core",
"description": "Work with the Membrane multimedia streaming framework in Elixir. Provides architectural guidance, callback/action references, and code patterns for pipelines, elements, and bins.",
"version": "1.0.0",
"source": "./",
"category": "development",
"homepage": "https://github.com/membraneframework/membrane_core",
"skills": [
"./skills/membrane-core"
]
}
]
}
32 changes: 32 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Claude Instructions

This is the **membrane_core** repository — the core of the Membrane multimedia streaming framework for Elixir.

Always use the `membrane-core` skill when working in this repository.

## Running Tests

```bash
mix test # run all tests
mix coveralls # with coverage
mix coveralls.html # coverage as HTML report
```

## Project Structure

| Directory | Purpose |
|-----------|---------|
| `lib/` | Main source code |
| `test/` | Tests |
| `test/support/` | Test helpers |
| `guides/` | Documentation and tutorials |
| `benchmark/` | Performance benchmarks |
| `config/` | Configuration |

## Conventions

- Elixir ~> 1.17
- Format code with `mix format`
- Type checking via Dialyzer (`mix dialyzer`)
- Linting via Credo (`mix credo`)
- Never modify code in `deps/`
164 changes: 164 additions & 0 deletions .claude/skills/membrane-core/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
name: membrane-core
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.
---

# Membrane Framework

**Package**: `membrane_core` ~> 1.2 | **Docs**: https://hexdocs.pm/membrane_core/ | **Demos**: https://github.com/membraneframework/membrane_demo

## How to Approach Tasks

- **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)
- **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`
- **Dynamic tracks** (demuxers, variable inputs) — use the Dynamic Pads Pattern below
- **Debugging** — check pad `accepted_format` compatibility, lifecycle ordering (`handle_setup` vs `handle_playing`), flow control mode mismatches
- **Full callback reference** — `references/callbacks.md`
- **Full actions reference** — `references/actions.md`
- **Never modify code in `deps/`**
- **Use `mix hex.info <plugin name>` when you need to check the newest version of a plugin**
- **Search for appropriate plugins in `README.md`, in `all-packages` section**
- **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.**
- **If the `accepted_stream_format` doesn't match, search for an element which can act as an adapter**

---

## Architecture

```
Pipeline
├── Element (Source/Filter/Sink/Endpoint) ← leaf, processes data
├── Bin ← dual role: parent (has children) + child (has pads)
│ ├── Element
│ └── Bin ← bins nest arbitrarily deep
└── ...
```

| Type | Parent | Child | Has Pads |
|------|--------|-------|----------|
| **Pipeline** | yes | no | no |
| **Bin** | yes | yes | yes |
| **Element** | no | yes | yes |

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)

---

## Pads

Defined on Elements and Bins (not Pipelines):

```elixir
def_input_pad :input, accepted_format: _any
def_output_pad :output, accepted_format: Membrane.RawAudio, flow_control: :auto
```

- **Availability**: `:always` (static, one instance, referenced by atom) or `:on_request` (dynamic, reference via `Pad.ref(:name, id)`)
- **Flow control**: `:auto` (framework manages demand — preferred), `:manual` (explicit via `:demand`/`:redemand`), `:push` (no demand, risk of overflow)
- One input pad ↔ one output pad only; pads must have compatible `accepted_format`
- Default pad names `:input`/`:output` allow omitting `via_in`/`via_out` in specs

---

## Component Lifecycle

```
handle_init/2 sync, blocks parent — parse opts, return initial spec
handle_setup/2 async — heavy init (open files, connect services)
return {[setup: :incomplete], state} to delay :playing
handle_pad_added/3 fires for dynamic pads linked in the same spec
handle_playing/2 component is ready — start producing/consuming data
```

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`.

---

## Pipeline & Bin DSL (ChildrenSpec)

```elixir
# Linear chain — child/2 spawns a new named child
child(:source, %Membrane.File.Source{location: "input.mp4"})
|> child(:filter, MyFilter)
|> child(:sink, %Membrane.File.Sink{location: "out.raw"})

# Explicit pad names (required for non-default names or dynamic pads)
get_child(:demuxer)
|> via_out(Pad.ref(:output, track_id))
|> via_in(:video_input)
|> child(:decoder, Membrane.H264.FFmpeg.Decoder)

# Link to an already-existing child
get_child(:existing_filter) |> child(:new_sink, MySink)

# Inside a Bin
bin_input(:input) |> child(:filter, MyFilter) |> bin_output(:output)
```

---

## Dynamic Pads Pattern

The standard approach for variable-track streams (e.g. MP4 demuxers):

```elixir
# 1. Spawn source + demuxer; demuxer hasn't identified tracks yet
def handle_init(_ctx, state) do
{[spec: child(:source, Source) |> child(:demuxer, Demuxer)], state}
end

# 2. Demuxer notifies parent once tracks are known
def handle_child_notification({:new_tracks, tracks}, :demuxer, _ctx, state) do
spec = Enum.map(tracks, fn {id, _fmt} ->
get_child(:demuxer)
|> via_out(Pad.ref(:output, id))
|> child({:decoder, id}, Decoder)
|> child({:sink, id}, Sink)
end)
{[spec: spec], state}
end
```

---

## Built-in Utility Elements

| Module | Purpose |
|--------|---------|
| `Membrane.Funnel` | Multiple inputs → one output |
| `Membrane.Tee` | One input → multiple outputs |
| `Membrane.Connector` | Connect dynamic pads with internal buffering |
| `Membrane.FilterAggregator` | Run multiple filters in a single process |
| `Membrane.Testing.Source` | Inject buffers into a pipeline in tests |
| `Membrane.Testing.Sink` | Capture and assert on buffers in tests |

---

## Testing

```elixir
import Membrane.ChildrenSpec
import Membrane.Testing.Assertions
alias Membrane.Testing
Comment thread
varsill marked this conversation as resolved.

pipeline = Testing.Pipeline.start_link_supervised!(spec: [
child(:source, %Testing.Source{output: [<<1, 2, 3>>, <<4, 5, 6>>]})
|> child(:sink, Testing.Sink)
])

assert_sink_buffer(pipeline, :sink, %Membrane.Buffer{payload: <<1, 2, 3>>})
```

---

## Timing

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`.

---

## Reference Files

- `references/callbacks.md` — full callback tables for every component type
- `references/actions.md` — all actions with signatures and usage notes
- `references/integration.md` — integration patterns, demo examples, key source file locations
91 changes: 91 additions & 0 deletions .claude/skills/membrane-core/references/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Membrane Framework — Actions Reference

Actions are returned from callbacks as `{[action_list], state}`.

---

## Data Actions (Elements Only)

```elixir
{:buffer, {pad_ref, %Membrane.Buffer{payload: binary, pts: pts, dts: dts, metadata: map}}}
# Send a buffer on an output pad

{:stream_format, {pad_ref, %SomeFormat{}}}
# Send stream format on an output pad — must be sent before the first buffer

{:event, {pad_ref, %SomeEvent{}}}
# Send an event on any pad — can go upstream or downstream

{:end_of_stream, pad_ref}
# Signal end of stream on an output pad
```

---

## Flow Control Actions

```elixir
# Manual input pads (flow_control: :manual) only:
{:demand, {pad_ref, size}} # request `size` more units from upstream
{:redemand, pad_ref} # re-evaluate demand on an output pad

# Auto input pads (flow_control: :auto) only:
{:pause_auto_demand, pad_ref} # temporarily stop auto-pulling from upstream
{:resume_auto_demand, pad_ref} # resume auto-pulling
```

---

## Topology Actions (Pipeline and Bin)

```elixir
{:spec, children_spec} # spawn and/or link children
{:remove_children, name_or_list} # stop and remove children by name
{:remove_link, {child_name, pad_ref}} # unlink a dynamic pad only
```

---

## Communication Actions

```elixir
{:notify_parent, any_term} # send notification to parent (Bin and Elements only)
# received in parent's handle_child_notification/4
```

---

## Component Control Actions

```elixir
{:setup, :complete | :incomplete}
# :incomplete — signals setup isn't done yet, delays :playing for the whole spec group
# :complete — signals setup is done (use after returning :incomplete earlier)

{:terminate, reason} # stop this component with the given reason
```

---

## Timer Actions

```elixir
{:start_timer, {name, interval}} # start a periodic timer → fires handle_tick/3
{:stop_timer, name} # stop a named timer
{:timer_interval, {name, new_interval}} # change interval of a running timer
```

`interval` is a `Membrane.Time.t()` value (integer nanoseconds). Use helpers like `Membrane.Time.milliseconds(100)`.

---

## Buffer Struct Reference

```elixir
%Membrane.Buffer{
payload: binary, # actual media data
pts: Membrane.Time.t() | nil, # presentation timestamp (nanoseconds)
dts: Membrane.Time.t() | nil, # decode timestamp (nanoseconds)
metadata: map | nil # arbitrary extra info
}
```
78 changes: 78 additions & 0 deletions .claude/skills/membrane-core/references/callbacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Membrane Framework — Callback Reference

## Every Component (Pipeline, Bin, Element)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_init/2` | Spawned (sync, blocks parent) | converts opts struct to map |
| `handle_setup/2` | After init (async) | no-op |
| `handle_playing/2` | Entering `:playing` | no-op |
| `handle_terminate_request/2` | Termination requested | `{[terminate: :normal], state}` |
| `handle_info/3` | Any non-Membrane Erlang message | logs warning |
| `handle_tick/3` | Timer tick (started with `:start_timer` action) | no-op |

---

## Parent Components Only (Pipeline and Bin)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_child_notification/4` | Child called `notify_parent` | no-op |
| `handle_child_setup_completed/3` | Child finished `handle_setup` | no-op |
| `handle_child_playing/3` | Child entered `:playing` | no-op |
| `handle_child_pad_removed/4` | Child removed a pad (child-initiated only) | no-op |
| `handle_child_terminated/3` | A child process terminated | no-op |
| `handle_crash_group_down/3` | All children in a crash group are down | no-op |
| `handle_element_start_of_stream/4` | A child element's pad started streaming | no-op |
| `handle_element_end_of_stream/4` | EOS received on a child element's input pad **or** sent on its output pad | no-op |

---

## Child Components Only (Bin and all Elements)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_parent_notification/3` | Parent sent a notification | no-op |
| `handle_pad_added/3` | Dynamic pad linked | no-op |
| `handle_pad_removed/3` | Dynamic pad unlinked | no-op |

**Lifecycle note for `handle_pad_added/3`:**
- Fires *before* `handle_playing/2` when the dynamic pad is linked in **the same spec** that spawns the component
- Fires *after* `handle_playing/2` when the pad is linked in a **later spec**

---

## Pipeline Only

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_call/3` | Synchronous call from an external process | no-op |

---

## Elements with Input Pads (Filter, Sink, Endpoint)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_stream_format/4` | Stream format received on input pad | Filter: forwards downstream |
| `handle_start_of_stream/3` | First buffer about to arrive on input pad | no-op |
| `handle_buffer/4` | Buffer received on input pad | — (no default, must implement) |
| `handle_end_of_stream/3` | EOS received on element's own input pad | Filter: forwards downstream |

---

## Elements with Output Pads (Source, Filter, Endpoint)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_demand/5` | Downstream requested data on a `:manual` output pad | — (no default, must implement if using `:manual` flow) |

---

## Events (All Elements)

| Callback | When called | Default |
|----------|-------------|---------|
| `handle_event/4` | Event received on any pad | Filter: forwards; others: no-op |

**Key property of events**: unlike buffers and stream formats, events can travel both **upstream and downstream**.
Loading