davinci-resolve-mcp 2.116.2 → 2.118.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/docs/guides/native-drt-authoring.md +2 -0
- package/docs/reference/api-limitations.md +8 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +13 -5
- package/resolve-advanced/server/tools/drt.mjs +4 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +60 -4
- package/resolve-advanced/vendor/drp-format/marker-encoder.js +6 -0
- package/resolve-advanced/vendor/drp-format/timeline-markers-blob.js +159 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +22 -0
|
@@ -47,6 +47,8 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
47
47
|
| Constant retimes, reverse | `cuts[].reverse` | v2.114 |
|
|
48
48
|
| Audio placements, A1–A8 | `cuts[].audioOnly + track` | v2.115 |
|
|
49
49
|
| Built-in generators | `elements: [{type:'generator', generatorName}]` | v2.110 |
|
|
50
|
+
| Custom start timecode | `spec.startFrame` / `preserveStartTimecode` | v2.117 |
|
|
51
|
+
| Timeline markers | `spec.markers` (16 colors, notes, durations, customData) | v2.118 |
|
|
50
52
|
| Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
|
|
51
53
|
|
|
52
54
|
`assemble_from_interchange` drives the same engine from an EDL / OTIO /
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:**
|
|
15
|
+
**Totals:** 34 missing capabilities, 44 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -214,6 +214,13 @@ equivalent, blocking full automation.
|
|
|
214
214
|
- **Workaround / current handling:** Treat as irreversible within a session. server returns _ok() unconditionally because there is nothing to check.
|
|
215
215
|
- **Tags:** resolve-21, unreliable-return, irreversible, session-wide
|
|
216
216
|
|
|
217
|
+
### A timeline's start timecode lives in the pool clip's MediaExtents blob (patchable offline)
|
|
218
|
+
|
|
219
|
+
- **Object:** `Sm2MpTimelineClip.MediaExtents`
|
|
220
|
+
- **Behavior:** The start timecode of a timeline is stored in exactly one non-cosmetic place in a .drp/.drt: the media pool timeline clip's MediaExtents blob, a 16-byte pair of LE doubles [startSeconds, durationSeconds] (measured: 02:03:04:05 @24 appears only as 7384.2083 there and in a UI-state blob). Patching startSeconds offline and importing yields a timeline at the new start timecode with clips at their absolute frames, and it renders.
|
|
221
|
+
- **Workaround / current handling:** To author a non-default start TC offline, patch MediaExtents (drt.assemble spec.startFrame does this) and keep clip Start frames >= the new origin - clips before it are silently dropped on import. For conform, assemble_from_interchange preserveStartTimecode=true anchors at the turnover's real first record frame instead of 01:00:00:00.
|
|
222
|
+
- **Tags:** timecode, drt, import
|
|
223
|
+
|
|
217
224
|
### MediaPool.ImportMedia (current-folder destination only)
|
|
218
225
|
|
|
219
226
|
- **Object:** `MediaPool`
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.118.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
|
package/package.json
CHANGED
|
@@ -220,14 +220,14 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
220
220
|
* @returns {{spec: object, report: object}}
|
|
221
221
|
*/
|
|
222
222
|
export function eventsToAssembleSpec(events, opts = {}) {
|
|
223
|
-
const { sourceMap, timelineName } = opts;
|
|
223
|
+
const { sourceMap, timelineName, preserveStartTimecode = false } = opts;
|
|
224
224
|
if (!Array.isArray(events) || !events.length) {
|
|
225
225
|
throw new TypeError('eventsToAssembleSpec: events must be a non-empty array');
|
|
226
226
|
}
|
|
227
227
|
if (!sourceMap || typeof sourceMap !== 'object') {
|
|
228
228
|
throw new TypeError('eventsToAssembleSpec: sourceMap {reel: {mediaFilePath, spec}} is required');
|
|
229
229
|
}
|
|
230
|
-
const
|
|
230
|
+
const DEFAULT_ORIGIN = 86400;
|
|
231
231
|
const isAudio = (t) => /^A\d*$/.test(String(t || ''));
|
|
232
232
|
const vids = events.filter((e) => !isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
233
233
|
const auds = events.filter((e) => isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
@@ -252,7 +252,15 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
252
252
|
const perSource = new Map();
|
|
253
253
|
const placements = [];
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
// Default: anchor the earliest event at the template origin (86400).
|
|
256
|
+
// preserveStartTimecode keeps the interchange's ABSOLUTE record positions —
|
|
257
|
+
// the assembled timeline starts at the turnover's real first record frame
|
|
258
|
+
// (MediaExtents start-TC patch, measured: imports with the new start TC and
|
|
259
|
+
// renders). AAF conforms need this: build at THAT start, not 01:00:00:00.
|
|
260
|
+
const minRecRaw = Math.min(...vids.map((e) => toTl(e.recIn, e.fps)));
|
|
261
|
+
const minRec = preserveStartTimecode ? 0 : minRecRaw;
|
|
262
|
+
const ORIGIN = preserveStartTimecode ? 0 : DEFAULT_ORIGIN;
|
|
263
|
+
const startFrame = preserveStartTimecode ? minRecRaw : DEFAULT_ORIGIN;
|
|
256
264
|
for (const e of vids) {
|
|
257
265
|
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
258
266
|
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
@@ -419,7 +427,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
419
427
|
}
|
|
420
428
|
|
|
421
429
|
return {
|
|
422
|
-
spec: { timelineName, media, ...(transitions.length ? { transitions } : {}) },
|
|
430
|
+
spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(transitions.length ? { transitions } : {}) },
|
|
423
431
|
report: {
|
|
424
432
|
videoEvents: vids.length,
|
|
425
433
|
sources: media.length,
|
|
@@ -431,7 +439,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
431
439
|
authoredRetimes,
|
|
432
440
|
authoredTransitions: transitions,
|
|
433
441
|
droppedTransitions,
|
|
434
|
-
origin:
|
|
442
|
+
origin: startFrame,
|
|
435
443
|
},
|
|
436
444
|
};
|
|
437
445
|
}
|
|
@@ -41,12 +41,14 @@ const assembleFromInterchangeSchema = z.object({
|
|
|
41
41
|
outputPath: z.string().describe('Where the importable .drt is written'),
|
|
42
42
|
targetAppVersion: z.union([z.string(), z.number()]).optional()
|
|
43
43
|
.describe("Host Resolve version, e.g. '19.1' for pre-21"),
|
|
44
|
+
preserveStartTimecode: z.boolean().optional()
|
|
45
|
+
.describe('Keep the interchange\'s ABSOLUTE record start: the assembled timeline starts at the turnover\'s real first record frame instead of 01:00:00:00 (start-TC patch render-verified on 19). AAF conforms should pass true.'),
|
|
44
46
|
});
|
|
45
47
|
const assembleSchema = z.object({
|
|
46
48
|
spec: z
|
|
47
49
|
.object({})
|
|
48
50
|
.passthrough()
|
|
49
|
-
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' dissolve | 'audio' cross-fade, both render-verified on 19)}], elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
51
|
+
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' dissolve | 'audio' cross-fade, both render-verified on 19)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
50
52
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
51
53
|
targetAppVersion: z
|
|
52
54
|
.union([z.string(), z.number()])
|
|
@@ -220,6 +222,7 @@ export const drtTool = {
|
|
|
220
222
|
if (!events || !events.length) return { error: 'no events parsed from the interchange input' };
|
|
221
223
|
const { spec, report } = eventsToAssembleSpec(events, {
|
|
222
224
|
sourceMap: p.sourceMap, timelineName: p.timelineName,
|
|
225
|
+
preserveStartTimecode: p.preserveStartTimecode,
|
|
223
226
|
});
|
|
224
227
|
if (p.targetAppVersion !== undefined) {
|
|
225
228
|
spec.templateVersion = parseFloat(p.targetAppVersion) >= 21 ? 21 : 19;
|
|
@@ -26,6 +26,7 @@ const { loadMediaTemplate, transplantMediaElement, insertMediaElement } = requir
|
|
|
26
26
|
const JSZip = require('jszip');
|
|
27
27
|
const { cutSourceIntoClips } = require('./cut-media');
|
|
28
28
|
const { buildConstantSpeedTimemapKeyed } = require('./media-timemap');
|
|
29
|
+
const { encodeTimelineMarkersBlob } = require('./timeline-markers-blob');
|
|
29
30
|
const { randomUUID } = require('node:crypto');
|
|
30
31
|
const { placeFusionTitle } = require('./place-fusion-title');
|
|
31
32
|
const { placeGenerator } = require('./place-generator');
|
|
@@ -33,6 +34,13 @@ const { placeTransition } = require('./place-transition');
|
|
|
33
34
|
|
|
34
35
|
async function assembleTimeline(spec = {}) {
|
|
35
36
|
const { timelineName, elements = [], transitions = [], media, templateVersion } = spec;
|
|
37
|
+
// Timeline start (frames @24). The start timecode lives in ONE place — the
|
|
38
|
+
// pool timeline clip's MediaExtents [startSeconds, durationSeconds]
|
|
39
|
+
// double-LE pair (measured: patching it imports with the new start TC and
|
|
40
|
+
// renders; no other non-cosmetic copy exists). Clips are absolute frames,
|
|
41
|
+
// so a custom origin just moves the guard.
|
|
42
|
+
const originFrame = spec.startFrame ?? DEFAULT_START_FRAME;
|
|
43
|
+
if (!Number.isInteger(originFrame) || originFrame < 0) throw new TypeError('assembleTimeline: spec.startFrame must be a non-negative integer');
|
|
36
44
|
if (!Array.isArray(elements)) throw new TypeError('assembleTimeline: elements must be an array');
|
|
37
45
|
if (!Array.isArray(transitions)) throw new TypeError('assembleTimeline: transitions must be an array');
|
|
38
46
|
|
|
@@ -59,9 +67,9 @@ async function assembleTimeline(spec = {}) {
|
|
|
59
67
|
const validateCuts = (src, label) => {
|
|
60
68
|
const mediaSpec = src.spec;
|
|
61
69
|
(src.cuts || []).forEach((cut, i) => {
|
|
62
|
-
if (cut.startFrame <
|
|
70
|
+
if (cut.startFrame < originFrame) {
|
|
63
71
|
throw new RangeError(
|
|
64
|
-
`assembleTimeline: ${label}.cuts[${i}].startFrame ${cut.startFrame} is before the timeline origin ${
|
|
72
|
+
`assembleTimeline: ${label}.cuts[${i}].startFrame ${cut.startFrame} is before the timeline origin ${originFrame} — Resolve silently drops it on import`,
|
|
65
73
|
);
|
|
66
74
|
}
|
|
67
75
|
if (mediaSpec && Number.isFinite(mediaSpec.frameCount) && Number.isFinite(mediaSpec.fps)) {
|
|
@@ -81,7 +89,7 @@ async function assembleTimeline(spec = {}) {
|
|
|
81
89
|
base = await addMediaClip({
|
|
82
90
|
mediaFile: sources[0].mediaFilePath, spec: sources[0].spec, timelineName, templateVersion,
|
|
83
91
|
});
|
|
84
|
-
base.startFrame =
|
|
92
|
+
base.startFrame = originFrame;
|
|
85
93
|
|
|
86
94
|
// Transplant/insert native pool elements BEFORE cutting, so each cut can
|
|
87
95
|
// reference its source's MediaRef.
|
|
@@ -185,7 +193,55 @@ async function assembleTimeline(spec = {}) {
|
|
|
185
193
|
}));
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
|
|
196
|
+
if (Array.isArray(spec.markers) && spec.markers.length) {
|
|
197
|
+
// Timeline markers ride in project.xml as a Sm2SequenceLockableBlob whose
|
|
198
|
+
// BlobOwner is the timeline's Sm2Sequence DbId (the uuid every track's
|
|
199
|
+
// <Sequence> references). Encoder byte-exact vs a live 19.1.3.7 export.
|
|
200
|
+
// Marker frames here are TIMELINE-ABSOLUTE for consistency with cuts;
|
|
201
|
+
// the blob stores them start-relative.
|
|
202
|
+
const zipM = await JSZip.loadAsync(buffer);
|
|
203
|
+
const seqName = Object.keys(zipM.files).find((n) => !zipM.files[n].dir && /SeqContainer\/.+\.xml$/.test(n));
|
|
204
|
+
const seqXml2 = await zipM.file(seqName).async('string');
|
|
205
|
+
const seqIdM = (seqXml2.match(/<Sequence>([0-9a-f-]{36})<\/Sequence>/) || [])[1];
|
|
206
|
+
if (!seqIdM) throw new Error('assembleTimeline: cannot find the Sm2Sequence id for markers');
|
|
207
|
+
const rel = spec.markers.map((m) => {
|
|
208
|
+
if (!Number.isInteger(m.frame) || m.frame < originFrame) {
|
|
209
|
+
throw new RangeError(`assembleTimeline: marker frame ${m.frame} is before the timeline origin ${originFrame} (frames are timeline-absolute)`);
|
|
210
|
+
}
|
|
211
|
+
return { ...m, frame: m.frame - originFrame };
|
|
212
|
+
});
|
|
213
|
+
const blob = encodeTimelineMarkersBlob(rel);
|
|
214
|
+
let pjX = await zipM.file('project.xml').async('string');
|
|
215
|
+
const setM = pjX.match(/<LocableBlobSet>[\s\S]*?<\/LocableBlobSet>/);
|
|
216
|
+
if (!setM) throw new Error('assembleTimeline: project.xml has no LocableBlobSet to hold markers');
|
|
217
|
+
const el = `<Element>\n <Sm2SequenceLockableBlob DbId="${randomUUID()}">\n <FieldsBlob>${blob.toString('hex')}</FieldsBlob>\n <BlobOwner>${seqIdM}</BlobOwner>\n <DbSavedTime>0</DbSavedTime>\n </Sm2SequenceLockableBlob>\n </Element>\n `;
|
|
218
|
+
pjX = pjX.replace('</LocableBlobSet>', `${el}</LocableBlobSet>`);
|
|
219
|
+
zipM.file('project.xml', pjX);
|
|
220
|
+
buffer = await zipM.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (originFrame !== DEFAULT_START_FRAME) {
|
|
224
|
+
const zipF = await JSZip.loadAsync(buffer);
|
|
225
|
+
const mpP = 'MediaPool/Master/MpFolder.xml';
|
|
226
|
+
let mpF = await zipF.file(mpP).async('string');
|
|
227
|
+
const tlBlocks = mpF.match(/<Sm2MpTimelineClip[\s\S]*?<\/Sm2MpTimelineClip>/g) || [];
|
|
228
|
+
const tlBlock = tlBlocks.find((b) => b.includes(`<Name>${tlName}</Name>`)) || tlBlocks[0];
|
|
229
|
+
if (!tlBlock) throw new Error('assembleTimeline: timeline pool clip not found for startFrame patch');
|
|
230
|
+
const meM = tlBlock.match(/<MediaExtents>([0-9a-fA-F]*)<\/MediaExtents>/);
|
|
231
|
+
if (!meM) throw new Error('assembleTimeline: timeline pool clip has no MediaExtents to patch');
|
|
232
|
+
let maxEnd = originFrame;
|
|
233
|
+
const collect = (arr) => (arr || []).forEach((x) => { const e = (x.startFrame ?? 0) + (x.durationFrames ?? 0); if (e > maxEnd) maxEnd = e; });
|
|
234
|
+
(Array.isArray(media) ? media : media ? [media] : []).forEach((src) => collect(src.cuts));
|
|
235
|
+
collect(elements);
|
|
236
|
+
const me = Buffer.alloc(16);
|
|
237
|
+
me.writeDoubleLE(originFrame / 24, 0);
|
|
238
|
+
me.writeDoubleLE((maxEnd - originFrame) / 24, 8);
|
|
239
|
+
mpF = mpF.replace(tlBlock, tlBlock.replace(meM[0], `<MediaExtents>${me.toString('hex')}</MediaExtents>`));
|
|
240
|
+
zipF.file(mpP, mpF);
|
|
241
|
+
buffer = await zipF.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { buffer, timelineName: tlName, startFrame: originFrame, mediaDescriptor: mediaDescriptorState };
|
|
189
245
|
}
|
|
190
246
|
|
|
191
247
|
module.exports = { assembleTimeline };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DaVinci Resolve Timeline Marker Encoder/Decoder
|
|
3
3
|
*
|
|
4
|
+
* DEPRECATED for .drp/.drt authoring: this module's color map and emitted
|
|
5
|
+
* bytes never matched a real Resolve export (measured 2026-08-30 — Yellow is
|
|
6
|
+
* 16 not 8, Purple is 128, and the framing differs). Use
|
|
7
|
+
* timeline-markers-blob.js, whose encoder is byte-exact against a live
|
|
8
|
+
* 19.1.3.7 export and render/readback-verified through drt.assemble.
|
|
9
|
+
*
|
|
4
10
|
* Encodes and decodes timeline markers into DaVinci Resolve's
|
|
5
11
|
* compressed protobuf format stored in Sm2SequenceLockableBlob.FieldsBlob.
|
|
6
12
|
*
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timeline-markers-blob — encode/decode TIMELINE markers as the
|
|
3
|
+
* Sm2SequenceLockableBlob.FieldsBlob that lives in project.xml.
|
|
4
|
+
*
|
|
5
|
+
* Ground truth (harvested live from Studio 19.1.3.7, all 16 colors + custom
|
|
6
|
+
* data + durations, decoded round-trip against the API's own readback):
|
|
7
|
+
*
|
|
8
|
+
* FieldsBlob = keyed-dict { "BlobData": bytes }
|
|
9
|
+
* BlobData = [u32BE 10001][u32BE innerLen][0x81][zstd frame]
|
|
10
|
+
* zstd frame = magic 28b52ffd + single-segment header + RAW block(s)
|
|
11
|
+
* (Resolve itself emits raw-block zstd for small payloads —
|
|
12
|
+
* accepted on import; no real compressor needed)
|
|
13
|
+
* payload = protobuf: field2 { repeated field1 MarkerEntry }
|
|
14
|
+
* MarkerEntry= f1 varint frameRelative, f2 bytes {
|
|
15
|
+
* [u32BE 2][u32BE innerLen] f1 bytes {
|
|
16
|
+
* f1 varint colorBit,
|
|
17
|
+
* f3 string note, f3 string durationString, f3 string name,
|
|
18
|
+
* f6 string customData (present only when non-empty)
|
|
19
|
+
* } }
|
|
20
|
+
*
|
|
21
|
+
* Marker frames are RELATIVE to the timeline start (frame 0 = first frame),
|
|
22
|
+
* matching the scripting API's marker frame space. The blob attaches inside
|
|
23
|
+
* project.xml's <LocableBlobSet> (Resolve's own spelling) with
|
|
24
|
+
* <BlobOwner> = the timeline's Sm2Sequence DbId (the same uuid every track's
|
|
25
|
+
* <Sequence> references).
|
|
26
|
+
*
|
|
27
|
+
* Color bits (measured, one marker per color): sequential powers of two with
|
|
28
|
+
* 256 unassigned. This supersedes marker-encoder.js, whose map was wrong for
|
|
29
|
+
* Yellow/Purple/Lavender and whose emitted bytes never matched a real export.
|
|
30
|
+
*
|
|
31
|
+
* @module drp-format/timeline-markers-blob
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const { encodeKeyedDict, decodeKeyedDict } = require('./keyed-dict');
|
|
35
|
+
|
|
36
|
+
const MARKER_COLOR_BITS = {
|
|
37
|
+
Blue: 2, Cyan: 4, Green: 8, Yellow: 16, Red: 32, Pink: 64, Purple: 128,
|
|
38
|
+
Fuchsia: 512, Rose: 1024, Lavender: 2048, Sky: 4096, Mint: 8192,
|
|
39
|
+
Lemon: 16384, Sand: 32768, Cocoa: 65536, Cream: 131072,
|
|
40
|
+
};
|
|
41
|
+
const BITS_TO_COLOR = Object.fromEntries(Object.entries(MARKER_COLOR_BITS).map(([k, v]) => [v, k]));
|
|
42
|
+
|
|
43
|
+
function varint(n) {
|
|
44
|
+
const out = [];
|
|
45
|
+
let v = n >>> 0;
|
|
46
|
+
do { out.push((v & 0x7f) | (v > 0x7f ? 0x80 : 0)); v >>>= 7; } while (v);
|
|
47
|
+
return Buffer.from(out);
|
|
48
|
+
}
|
|
49
|
+
const lenDelim = (field, payload) => Buffer.concat([varint((field << 3) | 2), varint(payload.length), payload]);
|
|
50
|
+
const varField = (field, n) => Buffer.concat([varint(field << 3), varint(n)]);
|
|
51
|
+
|
|
52
|
+
function encodeMarkerEntry(m) {
|
|
53
|
+
const colorBit = MARKER_COLOR_BITS[m.color] ?? MARKER_COLOR_BITS.Blue;
|
|
54
|
+
const strs = [m.note ?? '', String(m.duration ?? 1), m.name ?? ''];
|
|
55
|
+
const body = Buffer.concat([
|
|
56
|
+
varField(1, colorBit),
|
|
57
|
+
...strs.map((s) => lenDelim(3, Buffer.from(s, 'utf8'))),
|
|
58
|
+
...(m.customData ? [lenDelim(6, Buffer.from(m.customData, 'utf8'))] : []),
|
|
59
|
+
]);
|
|
60
|
+
const inner = lenDelim(1, body);
|
|
61
|
+
const head = Buffer.alloc(8);
|
|
62
|
+
head.writeUInt32BE(2, 0);
|
|
63
|
+
head.writeUInt32BE(inner.length, 4);
|
|
64
|
+
const wrapped = Buffer.concat([head, inner]);
|
|
65
|
+
return lenDelim(1, Buffer.concat([varField(1, m.frame), lenDelim(2, wrapped)]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** zstd single-segment frame with one RAW block (no compression). */
|
|
69
|
+
function zstdRawFrame(payload) {
|
|
70
|
+
const magic = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
71
|
+
let header;
|
|
72
|
+
if (payload.length <= 255) {
|
|
73
|
+
header = Buffer.from([0x20, payload.length]); // single-segment, 1-byte FCS
|
|
74
|
+
} else {
|
|
75
|
+
header = Buffer.alloc(5);
|
|
76
|
+
header[0] = 0xa0; // single-segment, 4-byte FCS
|
|
77
|
+
header.writeUInt32LE(payload.length, 1);
|
|
78
|
+
}
|
|
79
|
+
const block = Buffer.alloc(3);
|
|
80
|
+
block.writeUIntLE((payload.length << 3) | 1, 0, 3); // last=1, type=raw
|
|
81
|
+
return Buffer.concat([magic, header, block, payload]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function zstdRawInflate(buf) {
|
|
85
|
+
if (buf.readUInt32LE(0) !== 0xfd2fb528) throw new Error('timeline-markers-blob: not a zstd frame');
|
|
86
|
+
const fhd = buf[4];
|
|
87
|
+
const single = (fhd >> 5) & 1;
|
|
88
|
+
const fcsCode = fhd >> 6;
|
|
89
|
+
let o = 5 + (single ? [1, 2, 4, 8][fcsCode] : [0, 2, 4, 8][fcsCode]);
|
|
90
|
+
if (fhd & 0x03) throw new Error('timeline-markers-blob: dictionary frames unsupported');
|
|
91
|
+
const out = [];
|
|
92
|
+
for (;;) {
|
|
93
|
+
const bh = buf.readUIntLE(o, 3); o += 3;
|
|
94
|
+
const last = bh & 1, type = (bh >> 1) & 3, size = bh >> 3;
|
|
95
|
+
if (type === 0) { out.push(buf.subarray(o, o + size)); o += size; }
|
|
96
|
+
else if (type === 1) { out.push(Buffer.alloc(size, buf[o])); o += 1; }
|
|
97
|
+
else throw new Error('timeline-markers-blob: compressed zstd block — use a real zstd decoder');
|
|
98
|
+
if (last) break;
|
|
99
|
+
}
|
|
100
|
+
return Buffer.concat(out);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Encode timeline markers → Sm2SequenceLockableBlob FieldsBlob buffer.
|
|
105
|
+
* @param {Array<{frame:number,color?:string,name?:string,note?:string,duration?:number,customData?:string}>} markers
|
|
106
|
+
* frame is timeline-RELATIVE (0 = first frame of the timeline).
|
|
107
|
+
*/
|
|
108
|
+
function encodeTimelineMarkersBlob(markers) {
|
|
109
|
+
for (const m of markers) {
|
|
110
|
+
if (!Number.isInteger(m.frame) || m.frame < 0) throw new TypeError('encodeTimelineMarkersBlob: marker.frame must be a non-negative integer (timeline-relative)');
|
|
111
|
+
if (m.color && !MARKER_COLOR_BITS[m.color]) {
|
|
112
|
+
throw new Error(`encodeTimelineMarkersBlob: unknown color "${m.color}" (known: ${Object.keys(MARKER_COLOR_BITS).join(', ')})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const entries = [...markers].sort((a, b) => b.frame - a.frame).map(encodeMarkerEntry);
|
|
116
|
+
const pb = lenDelim(2, Buffer.concat(entries));
|
|
117
|
+
const frame = zstdRawFrame(pb);
|
|
118
|
+
const head = Buffer.alloc(8);
|
|
119
|
+
head.writeUInt32BE(10001, 0);
|
|
120
|
+
head.writeUInt32BE(frame.length + 1, 4);
|
|
121
|
+
const blobData = Buffer.concat([head, Buffer.from([0x81]), frame]);
|
|
122
|
+
return encodeKeyedDict({ hdr: 1, entries: [
|
|
123
|
+
{ key: 'BlobData', type: 0x0c, subType: 0, value: blobData.toString('hex') },
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Decode a Sm2SequenceLockableBlob FieldsBlob → markers (raw/RLE zstd only). */
|
|
128
|
+
function decodeTimelineMarkersBlob(buf) {
|
|
129
|
+
const d = decodeKeyedDict(buf);
|
|
130
|
+
const bd = d.entries.find((e) => e.key === 'BlobData');
|
|
131
|
+
if (!bd) throw new Error('decodeTimelineMarkersBlob: no BlobData entry');
|
|
132
|
+
const val = Buffer.from(bd.value, 'hex');
|
|
133
|
+
if (val.readUInt32BE(0) !== 10001 || val[8] !== 0x81) throw new Error('decodeTimelineMarkersBlob: unexpected BlobData framing');
|
|
134
|
+
const pb = zstdRawInflate(val.subarray(9));
|
|
135
|
+
let o = 0;
|
|
136
|
+
const rv = () => { let v = 0, s = 0; for (;;) { const b = pb[o++]; v |= (b & 0x7f) << s; if (!(b & 0x80)) return v >>> 0; s += 7; } };
|
|
137
|
+
const markers = [];
|
|
138
|
+
if (pb[o] === 0x12) { o++; rv(); }
|
|
139
|
+
while (o < pb.length && pb[o] === 0x0a) {
|
|
140
|
+
o++; const el = rv(); const end = o + el;
|
|
141
|
+
const m = { frame: null, color: null, note: '', duration: 1, name: '', customData: '' };
|
|
142
|
+
if (pb[o] === 0x08) { o++; m.frame = rv(); }
|
|
143
|
+
if (pb[o] === 0x12) {
|
|
144
|
+
o++; rv(); o += 8;
|
|
145
|
+
if (pb[o] === 0x0a) { o++; rv(); }
|
|
146
|
+
if (pb[o] === 0x08) { o++; m.color = BITS_TO_COLOR[rv()] ?? null; }
|
|
147
|
+
const strs = [];
|
|
148
|
+
while (o < end && pb[o] === 0x1a) { o++; const sl = rv(); strs.push(pb.subarray(o, o + sl).toString('utf8')); o += sl; }
|
|
149
|
+
[m.note = '', , m.name = ''] = strs;
|
|
150
|
+
m.duration = parseInt(strs[1] ?? '1', 10) || 1;
|
|
151
|
+
if (o < end && pb[o] === 0x32) { o++; const sl = rv(); m.customData = pb.subarray(o, o + sl).toString('utf8'); o += sl; }
|
|
152
|
+
}
|
|
153
|
+
o = end;
|
|
154
|
+
markers.push(m);
|
|
155
|
+
}
|
|
156
|
+
return markers;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { encodeTimelineMarkersBlob, decodeTimelineMarkersBlob, MARKER_COLOR_BITS };
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.118.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()}")
|
package/src/server.py
CHANGED
package/src/utils/api_truth.py
CHANGED
|
@@ -1914,6 +1914,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1914
1914
|
"structural readback cannot see this class.",
|
|
1915
1915
|
"tags": ["media-pool", "import", "render", "silent-failure", "readback"],
|
|
1916
1916
|
},
|
|
1917
|
+
{
|
|
1918
|
+
"symbol": "A timeline's start timecode lives in the pool clip's MediaExtents blob (patchable offline)",
|
|
1919
|
+
"object": "Sm2MpTimelineClip.MediaExtents",
|
|
1920
|
+
"reality": "The start timecode of a timeline is stored in exactly "
|
|
1921
|
+
"one non-cosmetic place in a .drp/.drt: the media pool "
|
|
1922
|
+
"timeline clip's MediaExtents blob, a 16-byte pair of "
|
|
1923
|
+
"LE doubles [startSeconds, durationSeconds] (measured: "
|
|
1924
|
+
"02:03:04:05 @24 appears only as 7384.2083 there and in "
|
|
1925
|
+
"a UI-state blob). Patching startSeconds offline and "
|
|
1926
|
+
"importing yields a timeline at the new start timecode "
|
|
1927
|
+
"with clips at their absolute frames, and it renders.",
|
|
1928
|
+
"recommended": "To author a non-default start TC offline, patch "
|
|
1929
|
+
"MediaExtents (drt.assemble spec.startFrame does "
|
|
1930
|
+
"this) and keep clip Start frames >= the new origin "
|
|
1931
|
+
"- clips before it are silently dropped on import. "
|
|
1932
|
+
"For conform, assemble_from_interchange "
|
|
1933
|
+
"preserveStartTimecode=true anchors at the "
|
|
1934
|
+
"turnover's real first record frame instead of "
|
|
1935
|
+
"01:00:00:00.",
|
|
1936
|
+
"tags": ["timecode", "drt", "import"],
|
|
1937
|
+
"submit": "missing",
|
|
1938
|
+
},
|
|
1917
1939
|
{
|
|
1918
1940
|
"symbol": "Audio tracks cannot be grown in an imported timeline; Fairlight strips live in the pool Sm2Sequence.FieldsBlob",
|
|
1919
1941
|
"object": "Sm2TiTrack (audio) / FLStudioModelBA",
|