-
Notifications
You must be signed in to change notification settings - Fork 46
Add skill.md describing how to use Membrane Core #1078
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
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ee7b392
Add skill.md
varsill 832cc90
Remove outdated spec syntax
varsill c06c516
Structurize the skill. Add plugin.json
varsill afb407e
Rename plugin.json into marketplace.json
varsill 6ecc681
Fix marketplace.json
varsill 2ac631a
Add plugin.json
varsill 5a59fec
Rename skill to membrane-core
varsill 2fbd3e8
Add missing comma
varsill 5c8ed23
Fix testing example. Add reference to plugins list. Disallow deps mod…
varsill ebaedb9
Suggest using grep
varsill 099aaec
Add CLAUDE.md. Move the skills to .claude
varsill 7a81669
Update Filter and Endpoint subtype description in SKILL.md
varsill File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| ] | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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**. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.