davinci-resolve-mcp 2.218.2 → 2.220.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,112 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.220.0 — native Resolve 21.1 output blanking, timeline and clip
6
+
7
+ Contributed by @legionsound (#213), live-validated on Studio 21.1.0.14.
8
+
9
+ ### Added
10
+
11
+ - **`timeline set_output_blanking`** and **`timeline_item set_output_blanking`**,
12
+ with granular twins `set_timeline_output_blanking` and
13
+ `set_timeline_item_output_blanking`, calling native 21.1 `SetOutputBlanking`.
14
+ `Top`/`Bottom`/`Left`/`Right` are **native pixel coordinates, not independent
15
+ margin widths** — the wrapper says so rather than letting the names imply
16
+ otherwise. Whole-valued native floats round-trip without conversion,
17
+ malformed values and unknown keys are refused, and no undocumented geometry
18
+ limits are invented.
19
+ - **`timeline_item set_use_timeline_for_output_blanking`**, with the granular
20
+ twin `set_timeline_item_use_timeline_for_output_blanking`, as an explicit
21
+ inheritance switch.
22
+ - **A clip override does not silently disable timeline inheritance.** Measured:
23
+ a clip write while inheritance was on returned false without changing
24
+ inheritance, and succeeded only after an explicit disable. The wrapper
25
+ preserves that native false instead of quietly flipping the switch on the
26
+ caller's behalf — a clip that stops inheriting is a change nobody asked for.
27
+ - All three actions carry method floors, entries in **both** write-risk tables,
28
+ destructive granular annotations and dry-run refusal tests. Tool count 370 →
29
+ 373.
30
+
31
+ ### Validation
32
+
33
+ - Full suite green: 3,457 passed, 1 skipped. All six mutating actions added
34
+ across #209, #211 and #213 probed directly against the classifier and the
35
+ destructive registry — every one MEDIUM / destructive / recognised in both.
36
+ - Live evidence is @legionsound's on Studio 21.1.0.14: both interfaces exported
37
+ synthetic red-clip PNGs with exact lit-pixel bounds — full `0/0/640/360`,
38
+ timeline `64/36/576/324`, clip `128/72/512/288`, restored inheritance
39
+ `64/36/576/324` — and a native partial write of `Top: 80.0` updated one
40
+ coordinate while preserving the others on readback. Recorded as sampled-frame
41
+ and readback results, **not** whole-movie or out-of-range behaviour claims.
42
+ Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor.
43
+
44
+ ### Changed
45
+
46
+ - Adapted on merge. The branch was cut before #211, so its tool-count bump
47
+ (368 → 371) and every generated agent-rule file conflicted with the 370 that
48
+ multicam had landed. Counts were resolved to **373** — confirmed
49
+ independently by the agent-rule generator rather than by arithmetic alone —
50
+ and the generated files were regenerated instead of hand-merged, which is
51
+ the only resolution that cannot silently disagree with its source. Both
52
+ documentation pointers were kept. No behaviour was changed in the
53
+ adaptation.
54
+
55
+ ## What's New in v2.219.0 — native Resolve 21.1 multicam creation and flattening
56
+
57
+ Contributed by @legionsound (#211), live-validated on Studio 21.1.0.14.
58
+
59
+ ### Added
60
+
61
+ - **`media_pool create_multicam_clip`**, with the granular twin
62
+ `create_multicam_clip`, calling 21.1's native `CreateMulticamClip`. It takes
63
+ a list of media-pool unique IDs and an options dictionary covering all
64
+ eleven documented `MulticamOptions` fields. Enum fields accept either a
65
+ documented Resolve constant name or an integral native value; omitted fields
66
+ stay omitted rather than being filled with invented defaults. It returns the
67
+ ids and names of the clips **actually created**, and an empty native result
68
+ stays `success: false`.
69
+ - **`timeline_item flatten_multicam`**, with the granular twin
70
+ `flatten_timeline_item_multicam`, calling native `FlattenMulticam` with
71
+ either documented grade constant. Flattening replaces the item, so the tool
72
+ documentation tells callers to re-query the track afterwards.
73
+ - Every clip ID is resolved **before** anything is written, and an unknown or
74
+ duplicated ID refuses with nothing created. That is all-or-nothing input
75
+ resolution, not a transaction — the module says so in its own docstring
76
+ rather than implying a guarantee the native call does not offer.
77
+
78
+ ### Changed
79
+
80
+ - Both actions are registered in **both** write tables — the
81
+ `destructive_hook` registry and the MEDIUM-risk set in
82
+ `execution_lifecycle` — so safe mode, the dry-run refusal, the audit log and
83
+ the operation log all treat them as the mutations they are. Verified by
84
+ probing the classifier directly rather than reading the diff. Tool count 368
85
+ → 370 across the docs and the generated agent-rule files.
86
+ - The existing stacked-timeline multicam workflow is unchanged and still
87
+ available; the native route is an addition, not a replacement.
88
+
89
+ ### Validation
90
+
91
+ - Full suite green: 3,453 passed, 1 skipped. Both write tables probed directly:
92
+ `media_pool.create_multicam_clip` and `timeline_item.flatten_multicam` each
93
+ classify MEDIUM / destructive / recognised.
94
+ - The return shape is right by documentation as well as by measurement — the
95
+ shipped 21.1 stub declares `CreateMulticamClip(clips, multicamOptions) ->
96
+ list[MediaPoolItem]`.
97
+ - Live evidence is @legionsound's, measured on Studio 21.1.0.14 with synthetic
98
+ media in a disposable project, and this was the family where a **rendered**
99
+ comparison rather than a readback was the bar it had to clear: both
100
+ interfaces created native multicam items, rendered, flattened with
101
+ `COPY_GRADE`, and rendered again — all four complete decoded RGB movies
102
+ identical at 144 frames, media type becoming Video, clip span unchanged.
103
+ Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor,
104
+ where both methods refuse with their version error.
105
+ - **What that evidence does not cover**, stated so it is not read as more: it
106
+ proves the natively-selected angle survives flattening in an ungraded
107
+ fixture. Angle ordering, alternate-angle selection, grade transfer, audio
108
+ routing and synchronisation are unverified. Smart Switch and
109
+ `AutoAlignClips` are deliberately not part of this change.
110
+
5
111
  ## What's New in v2.218.2 — the AddTransition null-duration boundary, measured
6
112
 
7
113
  Measured by @legionsound on Studio 21.1.0.14 (#209), recorded here; no behavior
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.218.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.220.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(368%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(373%20full)-blue.svg)](#server-modes)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#server-modes)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
133
133
  | Mode | Entry point | Tools | Best for |
134
134
  |------|-------------|-------|----------|
135
135
  | Compound | `src/server.py` | 36 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
136
- | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 368 | Power users who want one MCP tool per Resolve API method. |
136
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 373 | Power users who want one MCP tool per Resolve API method. |
137
137
 
138
138
  The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
139
139
 
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
365
365
 
366
366
  | Metric | Value |
367
367
  |--------|-------|
368
- | MCP Tools | **36** compound / **368** granular (live server) |
368
+ | MCP Tools | **36** compound / **373** granular (live server) |
369
369
  | Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
370
370
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
371
371
  | API Methods Covered | **361/361** (100%) |
package/README.zh-CN.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.218.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.220.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(368%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(373%20full)-blue.svg)](#服务器模式)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#服务器模式)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.218.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.220.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
88
88
  | 模式 | 入口 | 工具数 | 适合谁 |
89
89
  |------|------|--------|--------|
90
90
  | Compound(复合) | `src/server.py` | 36 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
91
- | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 368 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
91
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 373 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
92
92
 
93
93
  除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
94
94
 
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
226
226
 
227
227
  | 指标 | 数值 |
228
228
  |------|------|
229
- | MCP 工具 | **36** 复合 / **368** 细粒度(实时服务器) |
229
+ | MCP 工具 | **36** 复合 / **373** 细粒度(实时服务器) |
230
230
  | Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
231
231
  | 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
232
232
  | API 方法覆盖 | **361/361**(100%) |
package/docs/SKILL.md CHANGED
@@ -345,7 +345,7 @@ to the user as verified.
345
345
  | Mode | Entry point | Tool count | Use when |
346
346
  |---|---|---|---|
347
347
  | Compound (default) | `src/server.py` | 36 tools | Most workflows — keeps context lean |
348
- | Granular (full) | `src/server.py --full` | 368 tools | Power users needing one tool per API method |
348
+ | Granular (full) | `src/server.py --full` | 373 tools | Power users needing one tool per API method |
349
349
 
350
350
  Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
351
351
  for edition, presets, audio formats/codecs, normalization modes, speed, fades
@@ -2483,3 +2483,6 @@ The full API reference is in `docs/reference/resolve_scripting_api.txt`.
2483
2483
  Native Resolve 21.1 speed and fade setters: see [speed/fades](reference/resolve211-speed-fades.md) for options, version guards and contributor validation limits.
2484
2484
 
2485
2485
  Native 21.1 transition creation: see [transition controls](reference/resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
2486
+
2487
+ Native multicam creation and flattening: [21.1 controls](reference/resolve211-multicam.md), with contributor render evidence and remaining family coverage.
2488
+ Native timeline/clip output blanking: [21.1 controls](reference/resolve211-blanking.md), with explicit inheritance and pixel-bound evidence.
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
64
64
  ├── install.py # Universal installer (macOS/Windows/Linux)
65
65
  ├── src/
66
66
  │ ├── server.py # Compound MCP server — 36 tools (default)
67
- │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 368 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 373 tools
68
68
  │ ├── granular/ # Modular full-server implementation
69
69
  │ └── utils/ # Platform detection, Resolve connection helpers
70
70
  ├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
package/docs/install.md CHANGED
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
144
144
  | Mode | File | Tools | Best For |
145
145
  |------|------|-------|----------|
146
146
  | **Compound** (default) | `src/server.py` | 36 | Most users — fast, clean, low context usage |
147
- | **Full** | `src/resolve_mcp_server.py` | 368 | Power users who want one tool per API method |
147
+ | **Full** | `src/resolve_mcp_server.py` | 373 | Power users who want one tool per API method |
148
148
 
149
149
  The compound server's `timeline_item` tool includes dedicated actions for common workflows:
150
150
 
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
159
159
 
160
160
  The installer uses the compound server by default. To use the full server:
161
161
  ```bash
162
- python src/server.py --full # Launch full 368-tool server
162
+ python src/server.py --full # Launch full 373-tool server
163
163
  # Or point your MCP config directly at src/resolve_mcp_server.py
164
164
  ```
165
165
 
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
25
25
  does not claim complete coverage of the newer Resolve 21.1 typed API. The
26
26
  default compound server exposes **36 tools** that group related operations by
27
27
  action parameter, keeping LLM context windows lean. The full granular server
28
- provides **368 individual tools** for power users. The legacy coverage spans
28
+ provides **373 individual tools** for power users. The legacy coverage spans
29
29
  13 API object classes. MCP-level kernel actions are tracked separately in
30
30
  [Kernel Action Coverage](../kernels/README.md).
31
31
 
@@ -633,3 +633,6 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
633
633
  Native speed and fade setters in both interfaces are documented in [Resolve 21.1 speed/fades](resolve211-speed-fades.md), including sampled video validation and unverified cases.
634
634
 
635
635
  Native 21.1 transition creation: see [transition controls](resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
636
+
637
+ Native multicam creation and flattening: [21.1 controls](resolve211-multicam.md), with contributor render evidence and remaining family coverage.
638
+ Native timeline/clip output blanking: [21.1 controls](resolve211-blanking.md), with explicit inheritance and pixel-bound evidence.
@@ -2,8 +2,8 @@
2
2
 
3
3
  # Read/Write Symmetry Audit
4
4
 
5
- - write-style action occurrences scanned: **120**
6
- - write-style action occurrences with a matching read: **73**
5
+ - write-style action occurrences scanned: **124**
6
+ - write-style action occurrences with a matching read: **76**
7
7
  - distinct high-signal `set_` actions without a direct/known readback: **4**
8
8
 
9
9
  ## High-signal gaps — `set_` with no direct/known readback
@@ -13,6 +13,6 @@
13
13
  - `set_keyframe_interpolation`
14
14
  - `set_node_enabled`
15
15
 
16
- ## Low-signal (create/add/insert/apply/import — usually expected): 42 distinct names
16
+ ## Low-signal (create/add/insert/apply/import — usually expected): 43 distinct names
17
17
 
18
- `add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
18
+ `add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
@@ -0,0 +1,55 @@
1
+ # Resolve 21.1 output blanking
2
+
3
+ The native setters are exposed in both interfaces:
4
+
5
+ | Compound tool/action | Granular tool |
6
+ |---|---|
7
+ | timeline set_output_blanking | set_timeline_output_blanking |
8
+ | timeline_item set_output_blanking | set_timeline_item_output_blanking |
9
+ | timeline_item set_use_timeline_for_output_blanking | set_timeline_item_use_timeline_for_output_blanking |
10
+
11
+ Blanking setters take `options` containing one or more Top/Bottom/Left/Right
12
+ pixel coordinates. These are bounds, not four independent margin widths. For a
13
+ 640x360 frame, full picture is Top0/Bottom360/Left0/Right640. Whole-valued floats
14
+ from native getter results are accepted; fractional/non-finite values, booleans,
15
+ unknown keys and empty dictionaries are refused. No undocumented geometry bounds
16
+ are invented, and partial dictionaries are forwarded unchanged to Resolve.
17
+
18
+ The inheritance setter requires a strict `use_timeline` boolean. To set a clip
19
+ override, disable inheritance explicitly first. On the measured build, setting
20
+ clip blanking while inheritance was enabled returned false and changed nothing.
21
+ The wrapper preserves that failure and never silently changes inheritance.
22
+ Re-enabling inheritance returns the clip to the timeline's blanking. Native
23
+ getter payloads may be empty when the clip inherits timeline blanking.
24
+
25
+ All three actions have 21.1 method floors, registered destructive MEDIUM-risk
26
+ writes in both classifier tables, and destructive granular annotations. Explicit
27
+ compound dry-run requests are refused before writes. Native failure is preserved
28
+ as success:false. Clip locators use the existing 1-based tracks/0-based item
29
+ indexes.
30
+
31
+ ## Contributor validation
32
+
33
+ Contributor-validated on macOS Studio 21.1.0.14 using a generated solid red clip
34
+ in a disposable 640x360 project, through both actual community interfaces.
35
+ Resolve-exported PNGs showed exact lit-pixel bounds:
36
+
37
+ - Full picture: left0/top0/right640/bottom360.
38
+ - Timeline blanking: left64/top36/right576/bottom324.
39
+ - Clip override: left128/top72/right512/bottom288.
40
+ - Inheritance restored: timeline bounds64/36/576/324 again.
41
+
42
+ The override frame was visually inspected; both interfaces returned the same
43
+ bounds. Inherited clip-set refusal was also verified live. This is sampled-frame
44
+ and readback evidence, not whole-movie verification, or proof of
45
+ negative-coordinate, out-of-frame or audio-item behavior. A native-only partial
46
+ update with Top80.0 also succeeded and preserved Bottom/Left/Right in readback;
47
+ that extra case was not image-verified. Those cases are not
48
+ claimed tested.
49
+
50
+ `tests/live_resolve211_blanking.py OUTPUT_DIR` requires the disposable project
51
+ Codex Blanking Validation 20260909 with one synthetic red.mov video clip, at
52
+ 640x360/24 fps with timeline start00:00:00:00. Generate red.mov using the color
53
+ fixture in resolve211-native-transitions.md. The script invokes both wrappers,
54
+ exports comparison PNGs and restores full timeline blanking/inheritance before
55
+ saving the scratch project. Never substitute production media.
@@ -0,0 +1,59 @@
1
+ # Native Resolve 21.1 multicam creation and flattening
2
+
3
+ Compound `media_pool create_multicam_clip` accepts `clip_ids` (media-pool unique
4
+ IDs) and optional `options`. Granular `create_multicam_clip` takes the same
5
+ arguments. Every ID must resolve before the native call; missing/duplicate IDs
6
+ are errors, never silently dropped angles. Success returns the IDs and names of
7
+ all returned multicam clips. An empty native result is success:false. This does
8
+ not replace the existing stacked-timeline preparation workflow on older Resolve.
9
+
10
+ All eleven MulticamOptions fields are supported: name, startTimecode, frameRate,
11
+ angleSyncMode, channelConfig, multicamAudioMode, angleNameMode, splitAtGaps,
12
+ useFullClipExtents, createBinForSourceClips and detectSameCameraClipsMode.
13
+ Documented Resolve constant names (for example MULTICAM_ANGLE_SYNC_TIMECODE) are
14
+ resolved against the live Resolve object. Integral native numeric constant values
15
+ are also accepted; the wrapper does not invent their numbering. Unknown option
16
+ keys, invalid named constants, malformed booleans and non-positive/non-finite
17
+ frame rates are refused. Omitted options stay omitted. Resolve controls native
18
+ defaults, including createBinForSourceClips=true, which can reorganize source
19
+ clips in the media pool. No source files are modified by this wrapper.
20
+
21
+ Compound `timeline_item flatten_multicam` and granular
22
+ `flatten_timeline_item_multicam` use the normal 1-based track/0-based item
23
+ locators and optional `grade_option`, default FLATTEN_MULTICAM_COPY_GRADE.
24
+ FLATTEN_MULTICAM_RETAIN_GRADE_FROM_ANGLE is also accepted. This replaces the
25
+ multicam item with its active angle, so re-query the timeline afterward. Native
26
+ false stays success:false. Both methods require 21.1 and callable APIs; both
27
+ compound actions are registered as destructive MEDIUM-risk writes in the two
28
+ risk tables. Granular annotations also declare destructive writes. Explicit
29
+ compound dry-run requests are refused before mutation.
30
+
31
+ ## Contributor validation
32
+
33
+ Contributor-validated on macOS Studio 21.1.0.14 using generated red/blue media in
34
+ a disposable 640x360/24 fps project. Both interfaces created a native Multicam
35
+ media-pool item, appended it to a timeline, rendered it, flattened with COPY_GRADE,
36
+ and rendered again. All four complete decoded RGB movies were identical, each
37
+ 144 frames. Flattened media type changed from Multicam to Video; start0 and
38
+ 144-frame duration were preserved. This proves preservation of the native-selected angle video in
39
+ an ungraded fixture. It does not prove second-angle switching, grade-copy versus
40
+ retained-grade differences, audio routing, audio synchronization, gap splitting
41
+ or camera-detection behavior. These options are documented pass-through, not
42
+ claimed live verified.
43
+
44
+ `tests/live_resolve211_multicam.py OUTPUT_DIR` requires the named disposable
45
+ project Codex Multicam Validation 20260909 with synthetic red.mov and blue.mov
46
+ six-second clips. It creates timelines and render jobs and saves that project.
47
+ Use the synthetic FFmpeg fixture commands in resolve211-native-transitions.md;
48
+ never run the fixture against production media. Compare complete decoded frames
49
+ from compound-before/compound-flattened/granular-before/granular-flattened.mov.
50
+
51
+ Smart Switch and AutoAlignClips remain separate work. Native timecode alignment
52
+ has positive position-readback evidence, but rendered/waveform acceptance remains
53
+ open. Silent color-card Smart Switch returned false and is not a meaningful
54
+ positive speaking-camera test. This contribution does not claim the entire
55
+ multicam family completed.
56
+
57
+ Do not infer angle order from clip_ids order. The wrapper forwards the requested
58
+ source order, but this measurement establishes the selected angle and its
59
+ preservation, not a contract for Resolve's default angle selection.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.218.2"
40
+ VERSION = "2.220.0"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
1543
1543
 
1544
1544
  def print_banner():
1545
1545
  title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
1546
- subtitle = "36 compound · 368 full · 3 platforms"
1546
+ subtitle = "36 compound · 373 full · 3 platforms"
1547
1547
  print()
1548
1548
  print(bold(" ╔══════════════════════════════════════════════════════╗"))
1549
1549
  print(bold(f" ║{title:^54}║"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.218.2",
3
+ "version": "2.220.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.218.2"
90
+ VERSION = "2.220.0"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
@@ -1,8 +1,10 @@
1
1
  """Native Resolve 21.1 discovery and editing controls."""
2
+ from src.utils.resolve211_blanking import validate_blanking
3
+ from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
2
4
  from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
3
5
  from src.granular.common import (
4
6
  mcp, READ_ONLY_TOOL, WRITE_TOOL, DESTRUCTIVE_TOOL, get_resolve, get_current_project,
5
- _get_timeline, _get_timeline_item, _requires_method, has_method,
7
+ _get_timeline, _get_timeline_item, _find_clip_by_id, _requires_method, has_method,
6
8
  )
7
9
 
8
10
 
@@ -198,3 +200,81 @@ def add_timeline_item_transition(options: dict, track_type: str = "video", track
198
200
  if missing:
199
201
  return missing
200
202
  return transition_result(item.AddTransition(dict(options)))
203
+
204
+
205
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
206
+ def create_multicam_clip(clip_ids: list[str], options: dict | None = None) -> dict:
207
+ """Create native 21.1 multicam clips. Options follow MulticamOptions; named Resolve constants or numeric values accepted. Resolves every ID before writing."""
208
+ _, p = get_current_project()
209
+ if p is None:
210
+ return {"error": "No project currently open"}
211
+ mp = p.GetMediaPool()
212
+ missing = _requires_method(mp, "CreateMulticamClip", "21.1")
213
+ if missing:
214
+ return missing
215
+ return create_multicam(get_resolve(), mp, clip_ids, {} if options is None else options, _find_clip_by_id)
216
+
217
+
218
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
219
+ def flatten_timeline_item_multicam(grade_option: str = "FLATTEN_MULTICAM_COPY_GRADE", track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
220
+ """Flatten a native multicam item using COPY_GRADE or RETAIN_GRADE_FROM_ANGLE. Re-query items after replacement."""
221
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
222
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
223
+ item, error = _get_timeline_item(track_type, track_index, item_index)
224
+ if error:
225
+ return error
226
+ missing = _requires_method(item, "FlattenMulticam", "21.1")
227
+ if missing:
228
+ return missing
229
+ grade, error = resolve_constant(get_resolve(), grade_option, GRADES)
230
+ if error:
231
+ return {"error": error}
232
+ return {"success": bool(item.FlattenMulticam(grade))}
233
+
234
+
235
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
236
+ def set_timeline_output_blanking(options: dict) -> dict:
237
+ """Set native 21.1 timeline Top/Bottom/Left/Right pixel coordinates."""
238
+ error = validate_blanking(options)
239
+ if error:
240
+ return {"error": error}
241
+ _, tl, error = _get_timeline()
242
+ if error:
243
+ return error
244
+ missing = _requires_method(tl, "SetOutputBlanking", "21.1")
245
+ if missing:
246
+ return missing
247
+ return {"success": bool(tl.SetOutputBlanking(dict(options)))}
248
+
249
+
250
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
251
+ def set_timeline_item_output_blanking(options: dict, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
252
+ """Set native 21.1 clip pixel coordinates. Disable timeline blanking inheritance first; this call does not change inheritance."""
253
+ error = validate_blanking(options)
254
+ if error:
255
+ return {"error": error}
256
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
257
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
258
+ item, error = _get_timeline_item(track_type, track_index, item_index)
259
+ if error:
260
+ return error
261
+ missing = _requires_method(item, "SetOutputBlanking", "21.1")
262
+ if missing:
263
+ return missing
264
+ return {"success": bool(item.SetOutputBlanking(dict(options)))}
265
+
266
+
267
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
268
+ def set_timeline_item_use_timeline_for_output_blanking(use_timeline: bool, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
269
+ """Explicitly enable/disable native 21.1 timeline blanking inheritance for a clip."""
270
+ if type(use_timeline) is not bool:
271
+ return {"error": "use_timeline must be a boolean"}
272
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
273
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
274
+ item, error = _get_timeline_item(track_type, track_index, item_index)
275
+ if error:
276
+ return error
277
+ missing = _requires_method(item, "SetUseTimelineForOutputBlanking", "21.1")
278
+ if missing:
279
+ return missing
280
+ return {"success": bool(item.SetUseTimelineForOutputBlanking(use_timeline))}
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
34
34
  if __name__ == "__main__":
35
35
  try:
36
36
  start_background_update_check(VERSION, project_dir, logger)
37
- logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (368 granular tools)")
37
+ logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (373 granular tools)")
38
38
  run_fastmcp_stdio(mcp)
39
39
  except KeyboardInterrupt:
40
40
  logger.info("Server shutdown requested")
package/src/server.py CHANGED
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
8
8
 
9
9
  Usage:
10
10
  python src/server.py # Start the MCP server
11
- python src/server.py --full # Start the 368-tool granular server instead
11
+ python src/server.py --full # Start the 373-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.218.2"
14
+ VERSION = "2.220.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -42,6 +42,8 @@ for p in [current_dir, project_dir]:
42
42
  if p not in sys.path:
43
43
  sys.path.insert(0, p)
44
44
 
45
+ from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
46
+ from src.utils.resolve211_blanking import validate_blanking
45
47
  from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
46
48
 
47
49
  # Platform-specific Resolve paths
@@ -20826,6 +20828,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
20826
20828
  exist. Raw mutators below do not validate paths, support dry_run, or normalize errors.
20827
20829
 
20828
20830
  Actions:
20831
+ create_multicam_clip(clip_ids, options?) -> {success, clips} — native 21.1 multicam creation.
20829
20832
  get_root_folder() -> {name, id}
20830
20833
  get_current_folder() -> {name, id}
20831
20834
  set_current_folder(path) -> {success} — path like "Master/SubFolder"
@@ -21036,6 +21039,11 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21036
21039
  created_new=True,
21037
21040
  versioned_name=bool(existing and create_name != p.get("name")),
21038
21041
  ) if tl else _err("Failed to create timeline")
21042
+ elif action == "create_multicam_clip":
21043
+ missing = _requires_method(mp, "CreateMulticamClip", "21.1")
21044
+ if missing:
21045
+ return missing
21046
+ return create_multicam(get_resolve(), mp, p.get("clip_ids"), {} if p.get("options") is None else p["options"], _find_clip)
21039
21047
  elif action == "setup_multicam_timeline":
21040
21048
  return _setup_multicam_timeline(proj, mp, p)
21041
21049
  elif action == "import_timeline":
@@ -21298,7 +21306,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21298
21306
  return _copy_clip_annotations(root, p)
21299
21307
  elif action == "media_pool_boundary_report":
21300
21308
  return _media_pool_boundary_report(mp, p)
21301
- return _unknown(action, ["get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
21309
+ return _unknown(action, ["create_multicam_clip","get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
21302
21310
 
21303
21311
 
21304
21312
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -25045,7 +25053,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
25045
25053
 
25046
25054
 
25047
25055
  _TIMELINE_ACTIONS = [
25048
- "get_normalize_audio_modes", "get_output_blanking",
25056
+ "set_output_blanking", "get_normalize_audio_modes", "get_output_blanking",
25049
25057
  # Offline authoring — served without a Resolve connection, above the _check() gate.
25050
25058
  "author_offline", "offline_fallback_capabilities",
25051
25059
  "list", "get_current", "set_current", "get_name", "set_name", "get_start_frame",
@@ -25094,6 +25102,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25094
25102
  (resolve_control api_truth "GetSourceStartFrame").
25095
25103
 
25096
25104
  Actions:
25105
+ set_output_blanking(options) -> {success} — native 21.1 pixel coordinates.
25097
25106
  get_normalize_audio_modes() -> {modes} — documented on Resolve 21.1+.
25098
25107
  get_output_blanking() -> {blanking} — documented on Resolve 21.1+. Pixel coordinates; empty on a clip inheriting timeline blanking.
25099
25108
  list() -> {timelines}
@@ -25412,6 +25421,15 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25412
25421
  if not tl:
25413
25422
  return _err("No current timeline")
25414
25423
 
25424
+ if action == "set_output_blanking":
25425
+ error = validate_blanking(p.get("options"))
25426
+ if error:
25427
+ return _err(error)
25428
+ missing = _requires_method(tl, "SetOutputBlanking", "21.1")
25429
+ if missing:
25430
+ return missing
25431
+ return {"success": bool(tl.SetOutputBlanking(dict(p["options"])))}
25432
+
25415
25433
  if action == "get_normalize_audio_modes":
25416
25434
  missing = _requires_method(tl, "GetNormalizeAudioModes", "21.1")
25417
25435
  if missing:
@@ -26274,6 +26292,9 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26274
26292
  Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
26275
26293
 
26276
26294
  Actions:
26295
+ flatten_multicam(grade_option?, ...) -> {success} — native 21.1, replaces multicam with its current angle.
26296
+ set_output_blanking(options, ...) -> {success} — native 21.1; disable timeline inheritance first.
26297
+ set_use_timeline_for_output_blanking(use_timeline, ...) -> {success} — explicit inheritance switch.
26277
26298
  add_transition(options, ...) -> {success, transition?} — native 21.1 transition; reports actual span.
26278
26299
  set_speed(options, ...) -> {success} — native 21.1 speed options; RippleTimeline defaults false.
26279
26300
  set_fades(options, ...) -> {success} — native 21.1 FadeIn/FadeOut integer frames.
@@ -26331,6 +26352,32 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26331
26352
  if err:
26332
26353
  return err
26333
26354
 
26355
+ if action == "flatten_multicam":
26356
+ missing = _requires_method(item, "FlattenMulticam", "21.1")
26357
+ if missing:
26358
+ return missing
26359
+ grade, error = resolve_constant(get_resolve(), p.get("grade_option", "FLATTEN_MULTICAM_COPY_GRADE"), GRADES)
26360
+ if error:
26361
+ return _err(error)
26362
+ return {"success": bool(item.FlattenMulticam(grade))}
26363
+
26364
+ if action == "set_output_blanking":
26365
+ error = validate_blanking(p.get("options"))
26366
+ if error:
26367
+ return _err(error)
26368
+ missing = _requires_method(item, "SetOutputBlanking", "21.1")
26369
+ if missing:
26370
+ return missing
26371
+ return {"success": bool(item.SetOutputBlanking(dict(p["options"])))}
26372
+
26373
+ if action == "set_use_timeline_for_output_blanking":
26374
+ if type(p.get("use_timeline")) is not bool:
26375
+ return _err("use_timeline must be a boolean")
26376
+ missing = _requires_method(item, "SetUseTimelineForOutputBlanking", "21.1")
26377
+ if missing:
26378
+ return missing
26379
+ return {"success": bool(item.SetUseTimelineForOutputBlanking(p["use_timeline"]))}
26380
+
26334
26381
  if action == "add_transition":
26335
26382
  options = p.get("options")
26336
26383
  error = validate_transition_options(options)
@@ -26552,7 +26599,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26552
26599
  return _err(f"Invalid interpolation. Must be one of: {', '.join(valid)}")
26553
26600
  return {"success": bool(item.SetKeyframeInterpolation(p["property"], p["frame"], p["interpolation"]))}
26554
26601
 
26555
- return _unknown(action, ["add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
26602
+ return _unknown(action, ["set_output_blanking","set_use_timeline_for_output_blanking","flatten_multicam","add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
26556
26603
 
26557
26604
 
26558
26605
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -32674,9 +32721,9 @@ if __name__ == "__main__":
32674
32721
  start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
32675
32722
  _install_threaded_tool_dispatch(mcp)
32676
32723
 
32677
- # Support --full flag to run the 368-tool granular server instead
32724
+ # Support --full flag to run the 373-tool granular server instead
32678
32725
  if "--full" in sys.argv:
32679
- logger.info("Starting full 368-tool granular server...")
32726
+ logger.info("Starting full 373-tool granular server...")
32680
32727
  sys.argv = [arg for arg in sys.argv if arg != "--full"]
32681
32728
  from src.granular import mcp as granular_mcp
32682
32729
 
@@ -878,9 +878,19 @@ API_TRUTH: List[Dict[str, Any]] = [
878
878
  "21.1.0.14: each of those four resolves to a "
879
879
  "<BlackmagicFusion.PyFunctionCall object>, not None — the same "
880
880
  "discriminator that distinguishes a real method from an absent "
881
- "one elsewhere in this registry. THE ENTRY IS NOT PROOF THE "
882
- "METHODS WORK: none of them was invoked, because doing so mutates "
883
- "the user's project. HISTORICAL, still true of 21.0.x and "
881
+ "one elsewhere in this registry. That original probe did not "
882
+ "invoke the methods. UPDATE: contributor-validated by "
883
+ "@legionsound on Studio 21.1.0.14, macOS, 2026-09-09; not "
884
+ "reproduced by the maintainer on 19.1.3.7. Both community "
885
+ "interfaces created a Multicam from synthetic red/blue clips "
886
+ "and flattened it with COPY_GRADE. Four complete decoded "
887
+ "144-frame renders were identical before/after flattening "
888
+ "and across interfaces; media type changed Multicam to Video "
889
+ "without changing the selected clip span. This verifies the "
890
+ "native-selected angle in an ungraded fixture, not angle "
891
+ "ordering, grade transfer, audio sync or Smart Switch. See "
892
+ "resolve211-multicam.md for exact limits. HISTORICAL, still "
893
+ "true of 21.0.x and "
884
894
  "earlier: there was no method to create a native multicam clip "
885
895
  "from a set of angles; angles could be stacked onto tracks "
886
896
  "programmatically but the multicam-clip conversion was a UI-only "
@@ -74,6 +74,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
74
74
  "create_timeline_from_clips",
75
75
  "append_to_timeline",
76
76
  "setup_multicam_timeline",
77
+ "create_multicam_clip",
77
78
  "create_stereo_clip",
78
79
  "auto_sync_audio",
79
80
  "set_clip_marks",
@@ -86,6 +87,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
86
87
  "execute_swap",
87
88
  }),
88
89
  "timeline": frozenset({
90
+ "set_output_blanking",
89
91
  "delete_clips",
90
92
  "move_clips",
91
93
  "duplicate_clips",
@@ -137,6 +139,8 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
137
139
  "create_subtitles",
138
140
  }),
139
141
  "timeline_item": frozenset({
142
+ "set_output_blanking",
143
+ "set_use_timeline_for_output_blanking",
140
144
  "set_clip_enabled",
141
145
  "set_property",
142
146
  "set_name",
@@ -151,6 +155,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
151
155
  # classifier did not recognise them, so safe mode, the dry-run refusal,
152
156
  # the audit log and the operation log all skipped a call that rewrites a
153
157
  # clip's speed — and, with RippleTimeline true, moves every clip after it.
158
+ "flatten_multicam",
154
159
  "add_transition",
155
160
  "set_speed",
156
161
  "set_fades",
@@ -274,6 +274,11 @@ class RiskClassificationHook(LifecycleHook):
274
274
  # Native 21.1 setters (#208): `set_speed` changes duration and, with
275
275
  # RippleTimeline true, moves every clip after it; `set_fades` rewrites
276
276
  # how the clip's edges render. Existing content altered, not deleted.
277
+ ("media_pool", "create_multicam_clip"),
278
+ ("timeline_item", "flatten_multicam"),
279
+ ("timeline", "set_output_blanking"),
280
+ ("timeline_item", "set_output_blanking"),
281
+ ("timeline_item", "set_use_timeline_for_output_blanking"),
277
282
  ("timeline_item", "add_transition"),
278
283
  ("timeline_item", "set_speed"),
279
284
  ("timeline_item", "set_fades"),
@@ -0,0 +1,17 @@
1
+ """Preserve native pixel coordinates without inventing undocumented geometry limits."""
2
+ import math
3
+
4
+
5
+ def validate_blanking(options):
6
+ if not isinstance(options,dict) or not options:
7
+ return 'options must be a non-empty dictionary'
8
+ if set(options)-{'Top','Bottom','Left','Right'}:
9
+ return 'Only Top, Bottom, Left and Right are accepted'
10
+ for value in options.values():
11
+ try:
12
+ valid = type(value) in (int,float) and math.isfinite(value) and int(value)==value
13
+ except OverflowError:
14
+ valid = False
15
+ if not valid:
16
+ return 'Blanking values must be finite whole-pixel coordinates'
17
+ return None
@@ -0,0 +1,68 @@
1
+ """Native multicam options and all-or-nothing input resolution (not transactionality)."""
2
+ import math
3
+
4
+ ENUMS = {
5
+ 'angleSyncMode': tuple('MULTICAM_ANGLE_SYNC_'+v for v in ('IN','OUT','TIMECODE','AUDIO','MARKER')),
6
+ 'angleNameMode': tuple('MULTICAM_ANGLE_NAME_'+v for v in ('SEQUENTIAL','ANGLE','CAMERA','CLIP','FILE')),
7
+ 'multicamAudioMode': tuple('MULTICAM_AUDIO_'+v for v in ('ADAPTIVE','SOURCE','REFERENCE','ALL')),
8
+ 'detectSameCameraClipsMode': ('MULTICAM_DETECT_NONE',) + tuple('MULTICAM_DETECT_BY_'+v for v in ('CAMERA_NUMBER','ANGLE','REEL_NUMBER','REEL_NAME','ROLL_CARD')),
9
+ 'channelConfig': ('AUDIO_SYNC_CHANNEL_AUTOMATIC','AUDIO_SYNC_CHANNEL_MIX'),
10
+ }
11
+ BOOLS = {'splitAtGaps','useFullClipExtents','createBinForSourceClips'}
12
+ STRINGS = {'name','startTimecode'}
13
+ GRADES = ('FLATTEN_MULTICAM_COPY_GRADE','FLATTEN_MULTICAM_RETAIN_GRADE_FROM_ANGLE')
14
+
15
+
16
+ def resolve_constant(r, value, names):
17
+ if isinstance(value,str):
18
+ if value not in names:
19
+ return None, 'Unknown constant: '+value
20
+ native=getattr(r,value,None)
21
+ if type(native) not in (int,float) or not math.isfinite(native):
22
+ return None, 'Constant unavailable: '+value
23
+ return native,None
24
+ if type(value) in (int,float):
25
+ try:
26
+ if math.isfinite(value) and int(value)==value:
27
+ return value,None
28
+ except OverflowError:
29
+ pass
30
+ return None,'Use a documented constant name or integral native constant value'
31
+
32
+
33
+ def create_multicam(r, mp, clip_ids, options, find_clip):
34
+ if not isinstance(clip_ids,list) or not clip_ids or any(not isinstance(i,str) or not i for i in clip_ids):
35
+ return {'error':'clip_ids must be a non-empty list of media-pool unique IDs'}
36
+ if len(set(clip_ids))!=len(clip_ids):
37
+ return {'error':'clip_ids must not contain duplicates'}
38
+ if not isinstance(options,dict):
39
+ return {'error':'options must be a dictionary'}
40
+ if set(options)-(set(ENUMS)|BOOLS|STRINGS|{'frameRate'}):
41
+ return {'error':'Unknown multicam option'}
42
+ normalized=dict(options)
43
+ for key,value in options.items():
44
+ if key in BOOLS:
45
+ if type(value) is not bool:
46
+ return {'error':key+' must be a boolean'}
47
+ elif key in STRINGS:
48
+ if not isinstance(value,str) or not value.strip():
49
+ return {'error':key+' must be a non-empty string'}
50
+ elif key=='frameRate':
51
+ try:
52
+ valid=type(value) in (int,float) and math.isfinite(value) and value>0
53
+ except OverflowError:
54
+ valid=False
55
+ if not valid:
56
+ return {'error':'frameRate must be a positive finite number'}
57
+ elif key in ENUMS:
58
+ normalized[key],error=resolve_constant(r,value,ENUMS[key])
59
+ if error:
60
+ return {'error':key+': '+error}
61
+ root=mp.GetRootFolder()
62
+ clips=[find_clip(root,i) for i in clip_ids]
63
+ if any(c is None for c in clips):
64
+ return {'error':'One or more clip_ids were not found; no multicam was created'}
65
+ created=mp.CreateMulticamClip(clips,normalized)
66
+ if not created:
67
+ return {'success':False,'clips':[]}
68
+ return {'success':True,'clips':[{'id':c.GetUniqueId(),'name':c.GetName()} for c in created]}
@@ -225,6 +225,11 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
225
225
  # disagree, which is what keeps the two from drifting apart again.
226
226
 
227
227
  CODE_FLOORS: Dict[str, str] = {
228
+ "MediaPool.CreateMulticamClip": "21.1",
229
+ "TimelineItem.FlattenMulticam": "21.1",
230
+ "Timeline.SetOutputBlanking": "21.1",
231
+ "TimelineItem.SetOutputBlanking": "21.1",
232
+ "TimelineItem.SetUseTimelineForOutputBlanking": "21.1",
228
233
  "TimelineItem.AddTransition": "21.1",
229
234
  "TimelineItem.SetSpeed": "21.1",
230
235
  "TimelineItem.SetFades": "21.1",