Files
DMF-Studio/DMF_STUDIO_PLAN.md
2026-05-25 22:21:24 +03:00

410 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DMF Studio — Implementation Plan
## Product Definition
**DMF Studio** is a node-based visual production platform for the Dynamic Media Facility architecture. It provides a canvas where users create, connect, and control containerized Media Functions that exchange live video via the MXL shared-memory layer.
## Technology Stack
| Component | Choice |
|---|---|
| Language | C++20 |
| Build | CMake 3.24+, vcpkg |
| MXL SDK | Git submodule (github.com/dmf-mxl/mxl) |
| WebSocket | libwebsockets |
| JSON | nlohmann/json |
| Logging | spdlog |
| GPU compute | Vulkan (headless render + compute shaders) |
| SDI I/O | Blackmagic DeckLink SDK |
| Color conversion | libyuv |
| JPEG encoding | libjpeg-turbo |
| Testing | Catch2 |
| Frontend (phase 4) | Vue 3 + Vue Flow + Vite + Pinia |
## Architecture Principles
1. **Separate processes per node** — crash isolation, true DMF model
2. **MXL is the ONLY data path between nodes** — zero-copy shared memory
3. **Engine owns the graph, nodes own the media** — clean separation
4. **Engine generates flow IDs, nodes create MXL readers/writers** — centralized routing
5. **WebSocket control per node** — NMOS IS-12 aligned, bidirectional
6. **All nodes use `libdmf-node` skeleton** — consistent lifecycle
7. **Node binary = 1 C++ class + 1 main.cpp** — minimal per-node code
8. **Vulkan for GPU, CPU fallback available** — GPU-accelerated but not GPU-required
9. **Video only for MVP** — audio deferred
10. **Backend first, frontend last** — validate with curl + minimal HTML before building UI
## Project Structure
```
dmf-studio/
├── CMakeLists.txt
├── vcpkg.json
├── vcpkg-configuration.json
├── extern/
│ └── mxl/ # git submodule
├── libs/
│ ├── dmf-node/ # Node skeleton library (static)
│ │ ├── CMakeLists.txt
│ │ ├── include/dmf-node/
│ │ │ ├── node.hpp # Abstract Node interface
│ │ │ ├── port.hpp # PortDef, PortDirection, MediaType
│ │ │ ├── node_runner.hpp # CLI, MXL init, control WS, main loop
│ │ │ ├── control_server.hpp # WS server per-node
│ │ │ └── types.hpp # NodeId, PortId, FlowId aliases
│ │ └── src/
│ │ ├── node_runner.cpp
│ │ └── control_server.cpp
│ │
│ └── dmf-engine/ # Engine library (static)
│ ├── CMakeLists.txt
│ ├── include/dmf-engine/
│ │ ├── graph.hpp # Graph model: nodes, edges
│ │ ├── flow_manager.hpp # Flow UUID gen, NMOS flow defs
│ │ ├── process_manager.hpp # Spawn/monitor/kill node processes
│ │ ├── api_server.hpp # REST + WS to browser
│ │ └── types.hpp # Graph-level types
│ └── src/
│ ├── graph.cpp
│ ├── flow_manager.cpp
│ ├── process_manager.cpp
│ └── api_server.cpp
├── nodes/
│ ├── passthrough/
│ │ ├── CMakeLists.txt
│ │ └── src/
│ │ ├── main.cpp
│ │ ├── passthrough_node.hpp
│ │ └── passthrough_node.cpp
│ │
│ ├── test-source/
│ │ ├── CMakeLists.txt
│ │ └── src/
│ │ ├── main.cpp
│ │ ├── test_source_node.hpp
│ │ ├── test_source_node.cpp
│ │ ├── vulkan_context.hpp
│ │ ├── vulkan_context.cpp
│ │ ├── vulkan_renderer.hpp
│ │ ├── vulkan_renderer.cpp
│ │ └── shaders/
│ │ ├── pattern.vert
│ │ ├── pattern_bars.frag
│ │ ├── pattern_ramp.frag
│ │ ├── pattern_solid.frag
│ │ └── rgb_to_v210.comp
│ │
│ ├── decklink-in/
│ │ ├── CMakeLists.txt
│ │ └── src/
│ │ ├── main.cpp
│ │ ├── decklink_in_node.hpp
│ │ └── decklink_in_node.cpp
│ │
│ ├── decklink-out/
│ │ ├── CMakeLists.txt
│ │ └── src/
│ │ ├── main.cpp
│ │ ├── decklink_out_node.hpp
│ │ └── decklink_out_node.cpp
│ │
│ ├── preview/
│ │ ├── CMakeLists.txt
│ │ └── src/
│ │ ├── main.cpp
│ │ ├── preview_node.hpp
│ │ └── preview_node.cpp
│ │
│ └── mixer/
│ ├── CMakeLists.txt
│ └── src/
│ ├── main.cpp
│ ├── mixer_node.hpp
│ └── mixer_node.cpp
├── engine/
│ ├── CMakeLists.txt
│ └── src/
│ └── main.cpp
├── web/ # Phase 4 — Vue 3 frontend
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ └── src/
│ ├── App.vue
│ ├── main.ts
│ ├── api/
│ │ ├── rest.ts
│ │ └── websocket.ts
│ ├── stores/
│ │ ├── graph.ts
│ │ └── connection.ts
│ ├── canvas/
│ │ ├── StudioCanvas.vue
│ │ ├── nodes/
│ │ │ ├── SourceNode.vue
│ │ │ ├── ProcessorNode.vue
│ │ │ ├── OutputNode.vue
│ │ │ └── NodeThumbnail.vue
│ │ └── edges/
│ │ └── FlowEdge.vue
│ ├── panels/
│ │ ├── NodeProperties.vue
│ │ ├── Toolbar.vue
│ │ └── StatusBar.vue
│ └── templates/
│ └── templateLoader.ts
├── templates/
│ ├── loopback.json
│ ├── sdi_monitor.json
│ ├── sdi_pass.json
│ └── simple_switcher.json
├── docker/
│ ├── Dockerfile.engine
│ ├── Dockerfile.node.test-source
│ ├── Dockerfile.node.decklink-in
│ ├── Dockerfile.node.decklink-out
│ ├── Dockerfile.node.preview
│ ├── Dockerfile.node.passthrough
│ └── docker-compose.yaml
└── tests/
├── CMakeLists.txt
├── test_graph.cpp
├── test_flow_manager.cpp
└── test_control_protocol.cpp
```
## Node Interface
```cpp
class Node {
public:
virtual ~Node() = default;
virtual std::string type() const = 0;
virtual std::vector<PortDef> input_ports() const = 0;
virtual std::vector<PortDef> output_ports() const = 0;
virtual void configure(const nlohmann::json& params) = 0;
virtual void on_add_writer(const std::string& port_id, mxlFlowWriter writer) = 0;
virtual void on_add_reader(const std::string& port_id, mxlFlowReader reader) = 0;
virtual void on_remove_writer(const std::string& port_id) = 0;
virtual void on_remove_reader(const std::string& port_id) = 0;
virtual void process() = 0;
virtual nlohmann::json status() const = 0;
};
```
## Control Protocol (Engine ↔ Node via WebSocket)
Engine → Node:
```json
{ "cmd": "add_writer", "flow_id": "uuid", "flow_def": { ... } }
{ "cmd": "add_reader", "flow_id": "uuid" }
{ "cmd": "remove_writer", "port_id": "out_0" }
{ "cmd": "remove_reader", "port_id": "in_0" }
{ "cmd": "configure", "params": { ... } }
{ "cmd": "shutdown" }
```
Node → Engine:
```json
{ "event": "ready", "type": "test_source", "ports": { ... } }
{ "event": "flow_status", "port_id": "out_0", "writing": true, "fps": 50 }
{ "event": "error", "message": "..." }
{ "event": "grain_stats", "port_id": "out_0", "grains_written": 1234, "latency_us": 120 }
```
## Flow Connection Sequence
```
User draws edge in UI (or REST API call):
1. Engine generates flow UUID + NMOS flow definition
2. Engine → Node A: { "cmd": "add_writer", "flow_id": ..., "flow_def": ... }
3. Node A creates mxlFlowWriter, starts writing grains
4. Node A → Engine: { "event": "flow_status", "writing": true }
5. Engine → Node B: { "cmd": "add_reader", "flow_id": ... }
6. Node B creates mxlFlowReader, starts reading grains
7. Node B → Engine: { "event": "flow_status", "reading": true }
User removes edge:
1. Engine → Node B: { "cmd": "remove_reader", "port_id": ... }
2. Node B destroys mxlFlowReader
3. Engine → Node A: { "cmd": "remove_writer", "port_id": ... }
4. Node A destroys mxlFlowWriter
```
## Engine REST API
```
GET /api/graph # Current node graph
POST /api/graph/nodes # Add node { type, id? }
DELETE /api/graph/nodes/:id # Remove node
POST /api/graph/edges # Connect { from_node:from_port, to_node:to_port }
DELETE /api/graph/edges/:id # Disconnect
PUT /api/graph/nodes/:id # Configure node
POST /api/graph/start # Start all node processes + create flows
POST /api/graph/stop # Stop all
GET /api/templates # List templates
POST /api/templates/:name/load # Load template as graph
GET /api/nodes/types # List available node types
```
## Node Designs
### Test Source (Vulkan)
- Headless Vulkan: render 2 fullscreen triangles with pattern fragment shader → RGBA8 image
- Compute shader `rgb_to_v210.comp`: RGBA8 → BT.709 YCbCr → 4:2:2 downsample → V210 packing
- vkMapMemory → memcpy to MXL grain → commit
- Patterns: SMPTE bars, luma ramp, solid color
- Frame pacing to target 50fps
- 1 output port: `video_out` (V210)
### Passthrough
- Read 1 MXL grain → memcpy → write 1 MXL grain
- 1 input: `video_in`, 1 output: `video_out`
- Hello-world node for validating the pipeline
### Preview
- Read V210 MXL grain → libyuv convert to RGBA → downsample to 480×270 → libjpeg-turbo → JPEG
- Push JPEG frames to subscribed WS clients (throttled to ~15fps)
- 1 input: `video_in`
### DeckLink In
- IDeckLinkInputCallback::VideoInputFrameArrived → GetBytes (V210) → memcpy to MXL grain → commit
- V210 native match = straight copy, no conversion
- 1 output: `video_out`
- Config: device index, connection type
### DeckLink Out
- IDeckLinkOutputCallback::ScheduledFrameCompleted → read MXL grain → memcpy to DeckLink buffer → schedule frame
- 1 input: `video_in`
- Config: device index, connection type
### Mixer
- 2 inputs: `video_a`, `video_b` (V210) → 1 output: `program` (V210)
- Cut: select active input
- Dissolve: blend A×(1-t) + B×t over N frames
- V210 blending: unpack 10-bit → linear blend → repack
- Config: active_input, transition_type, transition_duration_frames
## Implementation Phases
### Phase 1: Framework + Passthrough (Week 1-2)
**Goal: Two processes exchanging video via MXL, controlled by engine**
| Step | Task | Test |
|---|---|---|
| 1.1 | Repo init, CMake top-level, vcpkg, MXL submodule | Build succeeds |
| 1.2 | Verify MXL SDK: `mxl-gst-testsrc` + `mxl-gst-sink` work | Video displays |
| 1.3 | Build `libdmf-node`: node.hpp, port.hpp, control_server, node_runner | Unit tests pass |
| 1.4 | Build `passthrough` node binary | Starts, connects to MXL |
| 1.5 | Build `libdmf-engine`: graph, flow_manager, process_manager, api_server | Unit tests pass |
| 1.6 | Build engine binary with minimal REST | `curl` works |
| 1.7 | Integration: engine starts 2 passthrough processes, creates flow between them | Frames flow A→B |
**Testing method**: curl + mxl-gst-testsrc/sink as external I/O
### Phase 2: Test Source + Preview (Week 3-4)
**Goal: GPU-generated color bars visible in browser**
| Step | Task | Test |
|---|---|---|
| 2.1 | Vulkan headless context (VkDevice, queues, command pool) | Device created |
| 2.2 | Pattern fragment shaders (bars, ramp, solid) + fullscreen vert | Render to RGBA8 |
| 2.3 | `rgb_to_v210.comp` compute shader | V210 output verified |
| 2.4 | `test-source` node: render → compute → MXL write | MXL flow active |
| 2.5 | `preview` node: MXL read → libyuv → JPEG → WS | Browser sees frames |
| 2.6 | Engine integration: test-source → preview | curl starts graph, browser shows bars |
**Testing method**: curl to start graph, open browser to preview WS endpoint, see live color bars
### Phase 3: DeckLink I/O (Week 5-6)
**Goal: Real SDI signal flowing through DMF**
| Step | Task | Test |
|---|---|---|
| 3.1 | `decklink-in` node: DeckLink callback → MXL writer | SDI camera → MXL flow |
| 3.2 | `decklink-out` node: MXL reader → DeckLink playback | MXL flow → SDI monitor |
| 3.3 | Templates: sdi_monitor, sdi_loopback, test_output | Load and run via curl |
| 3.4 | Full pipeline: SDI in → preview → browser | Camera visible in browser |
**Testing method**: curl to start graph, real SDI camera + monitor
### Phase 4: Frontend (Week 7-8)
**Goal: Visual graph editor on DMF TV**
| Step | Task | Test |
|---|---|---|
| 4.1 | Vue 3 + Vite project setup, Vue Flow integration | Dev server runs |
| 4.2 | Pinia stores: graph model, WS connection | State management works |
| 4.3 | StudioCanvas: drag-add nodes, draw edges, delete | Graph editing works |
| 4.4 | Custom node components: type icon, name, status, live thumbnail | Nodes render |
| 4.5 | FlowEdge: color-coded by flow status | Edges render |
| 4.6 | NodeProperties panel: dynamic config from node schema | Config changes work |
| 4.7 | Toolbar: New/Load/Save/Run/Stop + template menu | Full workflow works |
| 4.8 | StatusBar: node count, flow count, running state | Status updates live |
| 4.9 | Test on DMF TV browser | Everything renders and works |
**Testing method**: Full end-to-end — build graph in UI, run, see live video
### Phase 5: Mixer Node (Week 9-10)
**Goal: DMF Studio is a production tool**
| Step | Task | Test |
|---|---|---|
| 5.1 | `mixer` node: 2-in, 1-out, cut + dissolve | Cut/dissolve work via curl |
| 5.2 | Mixer control panel in Vue: PGM/PVW buses, CUT, AUTO TRANS, T-bar | UI controls work |
| 5.3 | Template: Simple Switcher (2 sources → mixer → preview + SDI out) | Full production workflow |
| 5.4 | Polish, bug fixes, edge cases | Stable demo |
## vcpkg.json Dependencies
```json
{
"name": "dmf-studio",
"version": "0.1.0",
"dependencies": [
"libwebsockets",
"nlohmann-json",
"spdlog",
"fmt",
"libyuv",
"libjpeg-turbo",
"catch2"
]
}
```
Vulkan: `find_package(Vulkan)` — system install, not vcpkg.
DeckLink SDK: vendor headers linked manually, not in vcpkg.
## Key Risks and Mitigations
| Risk | Mitigation |
|---|---|
| MXL SDK API changes | Pin submodule to v1.0.1 tag, test before upgrading |
| V210 compute shader correctness | Validate against known reference frames, compare with libyuv output |
| DeckLink SDK availability on dev machine | Develop without hardware using test-source; DeckLink nodes only need testing on machines with cards |
| Single-process engine becomes bottleneck | Engine never touches media data; it only manages metadata and process lifecycle |
| WebSocket control latency | All control is localhost; sub-ms latency. Remote control is future work |
| Vulkan driver compatibility | Start with standard Vulkan 1.2; no extensions required for headless compute |