davinci-resolve-mcp 2.97.2 → 2.97.4
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 +114 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/README.md +53 -9
- package/docs/reference/api-limitations.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +88 -17
- package/src/utils/api_truth.py +51 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,120 @@
|
|
|
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.97.4
|
|
6
|
+
|
|
7
|
+
**Drift is caught at the edit, not at publish time.** Tooling and docs only —
|
|
8
|
+
no server behavior changed, and no Resolve live run was required or performed.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Two opt-in `PostToolUse` hooks** in `.claude/hooks/`. Like the two existing
|
|
13
|
+
`PreToolUse` guards, they ship as scripts and are **not** wired up by default;
|
|
14
|
+
opt in via your own gitignored `.claude/settings.local.json` (the block is in
|
|
15
|
+
[docs/README.md](docs/README.md)).
|
|
16
|
+
- `agent_rules_drift_check.py` runs `node scripts/agent-rules/generate.mjs
|
|
17
|
+
--check` after an edit to anything the generator actually reads
|
|
18
|
+
(`docs/SKILL.md`, `docs/kernels/README.md`, `resolve-advanced/README.md`,
|
|
19
|
+
and `generate.mjs` itself, which carries the DOMAINS manifest inline) or to
|
|
20
|
+
`AGENTS.md`, which it writes. It separates the generator's two exit-1
|
|
21
|
+
paths: real staleness always prints `N agent-rule file(s) are stale`, a
|
|
22
|
+
throw never does — and telling a session to regenerate when the generator
|
|
23
|
+
is the thing that crashed sends it in a circle. Watching outputs but not
|
|
24
|
+
inputs was the original bug: bumping the compound tool count in
|
|
25
|
+
`docs/SKILL.md` left five generated files stale and the hook said nothing.
|
|
26
|
+
**A new generator input has to be added to `SOURCE_PATHS` or the hook goes
|
|
27
|
+
silent on exactly the edit it exists for.**
|
|
28
|
+
- `run_matching_test.py` runs the matching `tests/test_<module>.py` after an
|
|
29
|
+
edit under `src/`, resolving the project venv (`venv/bin/python`) before
|
|
30
|
+
`python3` so `pytest` is importable rather than reporting a false failure.
|
|
31
|
+
A fast partial net, not coverage: 72 of the 126 modules under `src/` follow
|
|
32
|
+
the convention, densely in `src/utils/` and not at all for `src/server.py`,
|
|
33
|
+
`src/granular/common.py`, or `src/control_panel.py`. Silence means "no
|
|
34
|
+
matching test file", not "this edit is fine".
|
|
35
|
+
- **`drift-guard-reviewer` subagent** (`.claude/agents/`) — runs the drift-guard
|
|
36
|
+
test family plus the adjacent checks `npm-publish.yml` runs before every
|
|
37
|
+
publish, on demand and in its own context. It reports what is stale and which
|
|
38
|
+
regeneration command fixes it; it does not fix anything.
|
|
39
|
+
- **`release-check` skill** (`/release-check`) — a thin wrapper that reads and
|
|
40
|
+
follows [docs/process/release-process.md](docs/process/release-process.md)
|
|
41
|
+
from disk. It deliberately does not restate the checklist: a second copy can
|
|
42
|
+
drift from the original, which `CLAUDE.md` prohibits.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `docs/README.md` documents all four hooks, all three subagents, and the new
|
|
47
|
+
skill in place, including the opt-in JSON block.
|
|
48
|
+
|
|
49
|
+
Contributed by [@Grimthereapper](https://github.com/Grimthereapper) in
|
|
50
|
+
[#149](https://github.com/samuelgursky/davinci-resolve-mcp/pull/149).
|
|
51
|
+
|
|
52
|
+
## What's New in v2.97.3
|
|
53
|
+
|
|
54
|
+
**`source_end` no longer has the start timecode baked into it.** Reported by
|
|
55
|
+
[@TheUnlockr](https://github.com/TheUnlockr) in
|
|
56
|
+
[#147](https://github.com/samuelgursky/davinci-resolve-mcp/issues/147) against
|
|
57
|
+
Studio 21.0.4.5, and confirmed live here on 19.1.3.7.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **The two source frame fields did not share an origin.** `source_start` comes
|
|
62
|
+
from `GetSourceStartFrame`, which is file-relative. `source_end` came from
|
|
63
|
+
`round(GetSourceEndTime x fps)` — and both second-readers answer in the media's
|
|
64
|
+
TIMECODE space, so on any clip with a non-zero start TC the whole start
|
|
65
|
+
timecode landed in `source_end`. On the reporter's Canon MP4 starting at
|
|
66
|
+
04:18:37;25 that produced `source_end` 468800 on a clip 10650 frames long. The
|
|
67
|
+
two fields could not be differenced, and anything sizing a pull from them —
|
|
68
|
+
`extract_source_frame_ranges`, `create_variant_from_ranges`, conform and
|
|
69
|
+
consolidation — was working from a number ~44x the clip's length.
|
|
70
|
+
- `source_end` is now the SPAN between the two second-readers, anchored on
|
|
71
|
+
`source_start`. The offset cancels in the difference, so the result is
|
|
72
|
+
file-relative under either convention and needs no timecode parsing, no `Start
|
|
73
|
+
TC` property read, and no drop-frame arithmetic. On media starting at
|
|
74
|
+
00:00:00:00 the correction is exactly zero, so nothing moves there.
|
|
75
|
+
- **The `_seconds` pair is rebased to match.** `source_start_seconds` used to
|
|
76
|
+
disagree with `source_start / source_fps` on any camera clip, despite being
|
|
77
|
+
documented as seconds into the source file. All four `source_*` fields are now
|
|
78
|
+
file-relative and say so.
|
|
79
|
+
|
|
80
|
+
### Why the existing suite could not catch it
|
|
81
|
+
|
|
82
|
+
The v2.93.0 work that introduced this was live-validated — but with synthetic
|
|
83
|
+
media, which starts at 00:00:00:00. That makes the offset exactly zero, so a
|
|
84
|
+
timecode-absolute reader is indistinguishable from a file-relative one in
|
|
85
|
+
precisely that setup. The new harness closes the gap by generating synthetic
|
|
86
|
+
media that *carries* a timecode.
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
|
|
90
|
+
- `tests/live_source_timecode_validation.py` — a controlled pair: two clips from
|
|
91
|
+
the same generator, identical except that one carries `-timecode 04:18:37;25`,
|
|
92
|
+
both cut into a timeline at a different rate. Any difference in the reported
|
|
93
|
+
fields is attributable to the timecode and nothing else. 17/17 checks pass on
|
|
94
|
+
Studio 19.1.3.7.
|
|
95
|
+
- Six unit tests in `tests/test_source_frame_rate.py` pinning the reporter's
|
|
96
|
+
measured numbers, including that zero-TC media is untouched.
|
|
97
|
+
|
|
98
|
+
### Documentation
|
|
99
|
+
|
|
100
|
+
- The `GetSourceStartFrame` api_truth entry claimed the v2.93.0 product was "a
|
|
101
|
+
SOURCE frame by construction". That holds only for media starting at
|
|
102
|
+
00:00:00:00; the entry now says so, records the live confirmation, and carries
|
|
103
|
+
the rate-conversion caveat below.
|
|
104
|
+
|
|
105
|
+
### Validation
|
|
106
|
+
|
|
107
|
+
- Live on **DaVinci Resolve Studio 19.1.3.7** (not the 21.0.4.5 of the report):
|
|
108
|
+
on the timecoded copy `GetSourceStartTime` read 15527.812 s where the
|
|
109
|
+
file-relative answer is 10.010 s while `GetSourceStartFrame` read 300 — the two
|
|
110
|
+
readers in different spaces on the same edit point. The pre-fix formula gave
|
|
111
|
+
465461 on a 1799-frame clip. After the fix the timecoded copy reports exactly
|
|
112
|
+
what the zero-TC control reports, for every range tried.
|
|
113
|
+
- One caveat the harness deliberately does *not* assert: `source_end` is not the
|
|
114
|
+
`endFrame` you sent when the rates differ. The record duration quantizes to
|
|
115
|
+
whole timeline frames, so a 93-source-frame request at 29.97 into a 24 fps
|
|
116
|
+
timeline consumes ~92.4 and *both* copies report 392. That is rate conversion,
|
|
117
|
+
not a timecode error.
|
|
118
|
+
|
|
5
119
|
## What's New in v2.97.2
|
|
6
120
|
|
|
7
121
|
**The panel's inventory snapshot survives a large project.** Follow-up to the
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.97.
|
|
15
|
+
> 本翻译对应 v2.97.4 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/README.md
CHANGED
|
@@ -69,23 +69,29 @@ skill routes, these two walk a whole job:
|
|
|
69
69
|
- `resolve-tighten-recording` (`.claude/skills/resolve-tighten-recording/SKILL.md`)
|
|
70
70
|
— **subtractive**: remove dead air from one long single-take recording.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Three more sit outside the domain routing:
|
|
73
73
|
|
|
74
74
|
- `house-style` (`.claude/skills/house-style/SKILL.md`) — accumulated editorial
|
|
75
75
|
corrections, so the same note is not given twice. Claude-only; append to it
|
|
76
76
|
when an editorial decision is corrected.
|
|
77
77
|
- `resolve-session` (`.claude/skills/resolve-session/SKILL.md`) — `/resolve-session`
|
|
78
78
|
connects, confirms edition and bridge, and reports project/timeline/pool state.
|
|
79
|
+
- `release-check` (`.claude/skills/release-check/SKILL.md`) — `/release-check`
|
|
80
|
+
walks a version bump using [docs/process/release-process.md](process/release-process.md)
|
|
81
|
+
as the sole source; the skill wraps that doc, it does not duplicate it.
|
|
79
82
|
|
|
80
83
|
The offline half of every one is the advanced server; see
|
|
81
84
|
[Advanced Server](../resolve-advanced/README.md).
|
|
82
85
|
|
|
83
86
|
## Claude Code Hooks and Subagents
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
Four hooks live in `.claude/hooks/` — two `PreToolUse` guards enforcing rules
|
|
89
|
+
`AGENTS.md` states in prose, and two `PostToolUse` checks that surface
|
|
90
|
+
engineering drift right after the edit that caused it instead of at the next
|
|
91
|
+
test run. All four ship as scripts but are **not** wired up by default — the
|
|
92
|
+
repository does not enable hooks on your behalf. Opt in by adding the block
|
|
93
|
+
below to your own `.claude/settings.local.json` (gitignored, so it stays
|
|
94
|
+
yours):
|
|
89
95
|
|
|
90
96
|
```json
|
|
91
97
|
{
|
|
@@ -111,13 +117,30 @@ hooks on your behalf. Opt in by adding the block below to your own
|
|
|
111
117
|
}
|
|
112
118
|
]
|
|
113
119
|
}
|
|
120
|
+
],
|
|
121
|
+
"PostToolUse": [
|
|
122
|
+
{
|
|
123
|
+
"matcher": "Write|Edit",
|
|
124
|
+
"hooks": [
|
|
125
|
+
{
|
|
126
|
+
"type": "command",
|
|
127
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/agent_rules_drift_check.py",
|
|
128
|
+
"timeout": 30
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "command",
|
|
132
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run_matching_test.py",
|
|
133
|
+
"timeout": 90
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
114
137
|
]
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
```
|
|
118
141
|
|
|
119
142
|
Hooks are read at session start, so restart Claude Code after adding them. The
|
|
120
|
-
|
|
143
|
+
four are:
|
|
121
144
|
|
|
122
145
|
- `.claude/hooks/frame_verification_guard.py` — denies grade-applying actions on
|
|
123
146
|
`timeline_item_color` until the session has actually looked at a
|
|
@@ -127,15 +150,36 @@ two guards are:
|
|
|
127
150
|
- `.claude/hooks/source_media_guard.py` — denies shell commands that write,
|
|
128
151
|
move, or delete source media outside a scratch root. Reads (`ffprobe`, and
|
|
129
152
|
`ffmpeg` writing into scratch) pass.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
- `.claude/hooks/agent_rules_drift_check.py` — after an edit to anything
|
|
154
|
+
`generate.mjs` reads (`docs/SKILL.md`, `docs/kernels/README.md`,
|
|
155
|
+
`resolve-advanced/README.md`, and `generate.mjs` itself, which carries the
|
|
156
|
+
DOMAINS manifest inline) or to `AGENTS.md`, which it writes, runs
|
|
157
|
+
`node scripts/agent-rules/generate.mjs --check` and surfaces the result. It
|
|
158
|
+
distinguishes real drift from a generator that threw before it could look —
|
|
159
|
+
regenerating fixes the first and not the second. Informational only; never
|
|
160
|
+
blocks. Skips quietly if `node` isn't on `PATH`. **If `generate.mjs` grows a
|
|
161
|
+
new input, add it to `SOURCE_PATHS` in the hook** — an unwatched input is a
|
|
162
|
+
silent hook on exactly the edit it exists to catch.
|
|
163
|
+
- `.claude/hooks/run_matching_test.py` — after an edit to `src/<module>.py`,
|
|
164
|
+
runs the matching `tests/test_<module>.py` if one exists, using the project
|
|
165
|
+
venv (`venv/bin/python`) so `pytest` is actually importable. Informational
|
|
166
|
+
only; skips quietly if there's no matching test file or no working `pytest`.
|
|
167
|
+
A fast partial net, not coverage: 72 of the 126 modules under `src/` have a
|
|
168
|
+
matching test under this convention, densely in `src/utils/` and not at all
|
|
169
|
+
for `src/server.py` or `src/granular/common.py`. Silence means "no matching
|
|
170
|
+
test file", not "this edit is fine".
|
|
171
|
+
|
|
172
|
+
Three review subagents in `.claude/agents/` run in their own context so bulky
|
|
173
|
+
output (frame images, full test transcripts) stays out of the main session:
|
|
133
174
|
|
|
134
175
|
- `cut-reviewer` — screens an assembled timeline from its frames and reports on
|
|
135
176
|
pacing, shot order, continuity, and coverage gaps.
|
|
136
177
|
- `grade-match-verifier` — measures shot match numerically from rendered frames
|
|
137
178
|
against the project's R−B tolerance, and reports mask pixel counts so an empty
|
|
138
179
|
skin mask cannot pass as a match.
|
|
180
|
+
- `drift-guard-reviewer` — runs the doc/generated-file drift-guard test family
|
|
181
|
+
(the same checks `npm-publish.yml` runs before every release) and reports
|
|
182
|
+
which files are stale relative to their source, without fixing them.
|
|
139
183
|
|
|
140
184
|
## Authoring References
|
|
141
185
|
|
|
@@ -402,8 +402,8 @@ values, or automation-hostile modal prompts.
|
|
|
402
402
|
|
|
403
403
|
- **Object:** `TimelineItem`
|
|
404
404
|
- **Signature:** `() -> int # source frame, counted in the MEDIA's frame rate`
|
|
405
|
-
- **Behavior:** The value is counted in the source MEDIA's own frame rate, not the timeline's. CORRECTION (2026-08-10, Studio 19.1.3.7): an earlier version of this entry said a WAV 'carries no frame rate, so Resolve falls back to 24 fps'. That is WRONG, and 24 is not a constant to rely on. A WAV takes the PROJECT's timelineFrameRate AT IMPORT and freezes it. Measured with one 400.000 s 48 kHz WAV imported into three project states: project at 24 -> clip FPS 24.0, Duration 00:06:40:00 (9600 frames = 400 s); project at 29.97 -> clip FPS 29.97, Duration 00:06:39:18 (11988 frames = 400 s); and changing the project rate to 29.97 AFTER import left the clip reading 24.0 (SetSetting returned True and the project did move). So the mismatch is not 'audio is always 24' but 'the clip kept the rate the project had when it was imported, and the project moved afterwards' — which also means a WAV imported into a 29.97 project behaves exactly like video, with no trap at all. ALWAYS read the clip's FPS property; never assume 24. The original 21.0.3.7 report below is consistent with this: that project was at 24 when the WAV was imported. Reading the frames at the timeline rate lands minutes away from the real position in the file. Verified live on Studio 21.0.3.7 (2026-08-09, 29.97 fps timeline): a ZOOM0028.WAV item reported source_start 56871, which is 56871 / 24 = 2369.6 s into the file, NOT the 1897.6 s a 29.97 fps reading gives — a 471.9 s (7 min 52 s) error. Nothing looks wrong, because timeline probe_timeline_structure derives source_end as source_start + timeline_duration: the start/end pair stays internally consistent whatever rate you assume. VIDEO items are NOT affected — two 29.97 fps items (KR020007.MOV, IMG_0001.mov) on the same timeline reported source frames in their own, matching rate, confirmed against ffprobe durations and span arithmetic. This is the read-side twin of the AppendToTimeline mixed-fps entry below: that one is about writing source frames whose rate differs from the timeline's, this one about reading them back and not knowing which rate they are in. The rate was pinned by regression, not assumed: across 12 items of the same WAV, GetSourceStartFrame advances at 24.000 fps against the item's own GetSourceStartTime (24.0000/24.0007/23.9995 over spans up to 22 minutes). The same measurement exposed a second unit trap: on an AUDIO item GetLeftOffset advances at 29.970 — the TIMELINE rate — so the two readers describe the same edit point in DIFFERENT frame spaces (60687 vs 75784 for one item). On video they share the source space. Caveat on the absolute zero: Resolve's model of this file is 133003 frames (Duration 01:32:21:19 at 24 fps = 5541.79 s) while its true PCM length is 266264768 samples / 48 kHz = 5547.18 s, a 0.097% difference we have not explained — so frames/24 is exact in Resolve's source-time space, which is the space every other Resolve call uses, but may sit ~2 s off the byte position in a 40-minute-deep offset. Re-confirmed on Studio 19.1.3.7 (2026-08-10) with synthetic media, so this is not a 21.x regression: a 300 s 48 kHz WAV reports FPS 24, and appending source frames 4800-5235 of it to a 29.97 fps timeline yields a timeline duration of 543 (= 435 x 29.97/24), which is the conversion happening in the open. The same run measured the cost of the derived end: source_end came back 5343 (4800 + 543) where the true source end is 5235, so source_end / 24 reports 222.625 s against a real 218.133 s from GetSourceEndTime — 4.49 s out, on a clip only 18.1 s long. GetSourceStartTime read exactly 200.0 s (= 4800/24) on the same item. The matching VIDEO item (29.97 source in a 29.97 timeline) was unaffected in both: 24.524 s read against 24.525 s derived. Both second-readers exist on 19.1.3.7, so the GetSourceEndFrame fallback below is for builds older still. GetSourceEndFrame ITSELF changes convention between the two regimes and cannot be used raw: measured over 12 items on 19.1.3.7, it is EXCLUSIVE (equals the endFrame sent) when the source rate equals the timeline rate, and INCLUSIVE (one less) when they differ — off by one in exactly the case a caller reaches for it. It is not a media-type split: the same WAV imported at 29.97 into a 29.97 timeline read exclusive, like video, and only the rate MISMATCH flipped it. What IS stable across both regimes is GetSourceEndTime x media_fps, which was exact on 12 of 12 valid items (30.633 s x 24 = 735.19 -> 735; 24.524 s x 29.97 = 734.98 -> 735) — seconds carry no frame-rate assumption, so the product is in source space
|
|
406
|
-
- **Workaround / current handling:** Convert an audio item's source frames with the MEDIA's rate, never the timeline's: seconds = source_start / media_fps. READ media_fps from the media-pool item's 'FPS' clip property (or ffprobe) every time — do not infer it from the timeline, and do NOT hard-code 24 for a WAV: that number is whatever the project rate was when the clip was imported, so it is 24 only for a project that was at 24, and a WAV imported at 29.97 has no mismatch at all. Feed the frames back to timeline create_variant_from_ranges in the same media-rate space you read them in; it converts on placement and reports the conversion in items[].duration_delta. The separate GetSourceStartFrame entry above (off-by-one vs GetLeftOffset) applies on top of this — the rate question is which unit the number is in, not whether it is exact. Mitigated in-process: _timeline_item_summary now emits source_fps and source_start_seconds/source_end_seconds beside the frames, so the number always arrives with its unit; on the GetLeftOffset fallback for an audio item it reports the rate as unknown rather than converting a timeline-frame value at the media rate. source_end is no longer source_start + TIMELINE duration: as of v2.93.0 it is round(GetSourceEndTime x media_fps)
|
|
405
|
+
- **Behavior:** The value is counted in the source MEDIA's own frame rate, not the timeline's. CORRECTION (2026-08-10, Studio 19.1.3.7): an earlier version of this entry said a WAV 'carries no frame rate, so Resolve falls back to 24 fps'. That is WRONG, and 24 is not a constant to rely on. A WAV takes the PROJECT's timelineFrameRate AT IMPORT and freezes it. Measured with one 400.000 s 48 kHz WAV imported into three project states: project at 24 -> clip FPS 24.0, Duration 00:06:40:00 (9600 frames = 400 s); project at 29.97 -> clip FPS 29.97, Duration 00:06:39:18 (11988 frames = 400 s); and changing the project rate to 29.97 AFTER import left the clip reading 24.0 (SetSetting returned True and the project did move). So the mismatch is not 'audio is always 24' but 'the clip kept the rate the project had when it was imported, and the project moved afterwards' — which also means a WAV imported into a 29.97 project behaves exactly like video, with no trap at all. ALWAYS read the clip's FPS property; never assume 24. The original 21.0.3.7 report below is consistent with this: that project was at 24 when the WAV was imported. Reading the frames at the timeline rate lands minutes away from the real position in the file. Verified live on Studio 21.0.3.7 (2026-08-09, 29.97 fps timeline): a ZOOM0028.WAV item reported source_start 56871, which is 56871 / 24 = 2369.6 s into the file, NOT the 1897.6 s a 29.97 fps reading gives — a 471.9 s (7 min 52 s) error. Nothing looks wrong, because timeline probe_timeline_structure derives source_end as source_start + timeline_duration: the start/end pair stays internally consistent whatever rate you assume. VIDEO items are NOT affected — two 29.97 fps items (KR020007.MOV, IMG_0001.mov) on the same timeline reported source frames in their own, matching rate, confirmed against ffprobe durations and span arithmetic. This is the read-side twin of the AppendToTimeline mixed-fps entry below: that one is about writing source frames whose rate differs from the timeline's, this one about reading them back and not knowing which rate they are in. The rate was pinned by regression, not assumed: across 12 items of the same WAV, GetSourceStartFrame advances at 24.000 fps against the item's own GetSourceStartTime (24.0000/24.0007/23.9995 over spans up to 22 minutes). The same measurement exposed a second unit trap: on an AUDIO item GetLeftOffset advances at 29.970 — the TIMELINE rate — so the two readers describe the same edit point in DIFFERENT frame spaces (60687 vs 75784 for one item). On video they share the source space. Caveat on the absolute zero: Resolve's model of this file is 133003 frames (Duration 01:32:21:19 at 24 fps = 5541.79 s) while its true PCM length is 266264768 samples / 48 kHz = 5547.18 s, a 0.097% difference we have not explained — so frames/24 is exact in Resolve's source-time space, which is the space every other Resolve call uses, but may sit ~2 s off the byte position in a 40-minute-deep offset. Re-confirmed on Studio 19.1.3.7 (2026-08-10) with synthetic media, so this is not a 21.x regression: a 300 s 48 kHz WAV reports FPS 24, and appending source frames 4800-5235 of it to a 29.97 fps timeline yields a timeline duration of 543 (= 435 x 29.97/24), which is the conversion happening in the open. The same run measured the cost of the derived end: source_end came back 5343 (4800 + 543) where the true source end is 5235, so source_end / 24 reports 222.625 s against a real 218.133 s from GetSourceEndTime — 4.49 s out, on a clip only 18.1 s long. GetSourceStartTime read exactly 200.0 s (= 4800/24) on the same item. The matching VIDEO item (29.97 source in a 29.97 timeline) was unaffected in both: 24.524 s read against 24.525 s derived. Both second-readers exist on 19.1.3.7, so the GetSourceEndFrame fallback below is for builds older still. GetSourceEndFrame ITSELF changes convention between the two regimes and cannot be used raw: measured over 12 items on 19.1.3.7, it is EXCLUSIVE (equals the endFrame sent) when the source rate equals the timeline rate, and INCLUSIVE (one less) when they differ — off by one in exactly the case a caller reaches for it. It is not a media-type split: the same WAV imported at 29.97 into a 29.97 timeline read exclusive, like video, and only the rate MISMATCH flipped it. What IS stable across both regimes is GetSourceEndTime x media_fps, which was exact on 12 of 12 valid items (30.633 s x 24 = 735.19 -> 735; 24.524 s x 29.97 = 734.98 -> 735) — seconds carry no frame-rate assumption, so the product is in source space. But NOT necessarily at the source's ORIGIN: all 12 of those items were synthetic media starting at 00:00:00:00. The second-readers are TIMECODE-absolute, so on media with a non-zero start TC the product is a source-RATE frame counted from the camera's timecode zero, not from the head of the file (issue #147, corrected in v2.97.3 — see the handling note below). Use the SPAN between the two second-readers, never either one alone: the offset cancels in the difference. CONFIRMED LIVE here on Studio 19.1.3.7 (2026-08-17), independently of the 21.0.4.5 report, with a CONTROLLED PAIR: two ffmpeg-generated 29.97 clips identical except that one carries -timecode 04:18:37;25, both cut into a 24 fps timeline. On the timecoded copy GetSourceStartTime read 15527.812 s where the file-relative answer is 10.010 s, while GetSourceStartFrame read 300 — the two readers in different spaces on the same edit point. round(GetSourceEndTime x fps) gave 465461 on a 1799-frame clip. After the fix the timecoded copy reports exactly what the zero-TC control reports for every range tried (300..393, 0..100, 1000..1500). Harness: tests/live_source_timecode_validation.py. NOTE the harness does NOT assert source_end == the endFrame sent: when the rates differ the record duration quantizes to whole TIMELINE frames, so a 93-source-frame request consumes ~92.4 and BOTH copies report 392. That is rate conversion, not a timecode error.
|
|
406
|
+
- **Workaround / current handling:** Convert an audio item's source frames with the MEDIA's rate, never the timeline's: seconds = source_start / media_fps. READ media_fps from the media-pool item's 'FPS' clip property (or ffprobe) every time — do not infer it from the timeline, and do NOT hard-code 24 for a WAV: that number is whatever the project rate was when the clip was imported, so it is 24 only for a project that was at 24, and a WAV imported at 29.97 has no mismatch at all. Feed the frames back to timeline create_variant_from_ranges in the same media-rate space you read them in; it converts on placement and reports the conversion in items[].duration_delta. The separate GetSourceStartFrame entry above (off-by-one vs GetLeftOffset) applies on top of this — the rate question is which unit the number is in, not whether it is exact. Mitigated in-process: _timeline_item_summary now emits source_fps and source_start_seconds/source_end_seconds beside the frames, so the number always arrives with its unit; on the GetLeftOffset fallback for an audio item it reports the rate as unknown rather than converting a timeline-frame value at the media rate. source_end is no longer source_start + TIMELINE duration: as of v2.93.0 it is read from GetSourceEndTime and stays EXCLUSIVE as every caller already assumed. CORRECTION (v2.97.3, issue #147): that release computed it as round(GetSourceEndTime x media_fps) and this entry called the result 'a SOURCE frame by construction'. That holds ONLY for media starting at 00:00:00:00. Both second-readers answer in the media's TIMECODE space, so on a camera clip carrying time-of-day or continuous TC the product has the whole start timecode baked into it: reported on a Canon MP4 starting at 04:18:37;25, source_end came back 468800 on a clip only 10650 frames long, ~44x its length, while source_start (GetSourceStartFrame) stayed file-relative — so the two fields no longer shared an origin and could not be differenced. The v2.93.0 live validation used synthetic media, which starts at 00:00:00:00, so the offset was zero and the error was invisible in exactly that setup. It is now the seconds SPAN between the two readers, anchored on source_start: the offset cancels in the difference, so the result is file-relative under either convention and needs no timecode parsing. All four source_* fields are file-relative, the _seconds pair included. Measured live on 19.1.3.7 over 8 items in both regimes, it equals the endFrame actually sent every time, and it reproduces the old value exactly wherever the old value was already right — so matched-rate media does not move. The old arithmetic overshot by +24/+26/+108/+149 frames on the mismatched WAV, and timeline extract_source_frame_ranges built pull ranges out of it: widths of 543 and 749 for clips that consume 435 and 600 source frames. It falls back to the old sum only when GetSourceEndTime or the rate is unreadable.
|
|
407
407
|
- **Tags:** timeline, audio, wav, frame-rate, mixed-fps, silent-failure, readback
|
|
408
408
|
|
|
409
409
|
### Studio-gated calls on the free edition raise a modal that blocks LATER calls
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.97.
|
|
40
|
+
VERSION = "2.97.4"
|
|
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
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.97.
|
|
90
|
+
VERSION = "2.97.4"
|
|
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
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.97.
|
|
14
|
+
VERSION = "2.97.4"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -3138,7 +3138,36 @@ def _timeline_item_source_end_frame(item):
|
|
|
3138
3138
|
return None
|
|
3139
3139
|
|
|
3140
3140
|
|
|
3141
|
-
def
|
|
3141
|
+
def _timeline_item_source_time_offset(item, source_start, source_fps, source_start_origin):
|
|
3142
|
+
"""Seconds between the second-readers' origin and the start of the file.
|
|
3143
|
+
|
|
3144
|
+
`GetSourceStartTime`/`GetSourceEndTime` answer in the media's TIMECODE space,
|
|
3145
|
+
so on any clip with a non-zero start TC they carry that offset — a Canon MP4
|
|
3146
|
+
starting at 04:18:37;25 reads 15639.187 s at source frame 3637, not the
|
|
3147
|
+
121.355 s that frame is into the file. `GetSourceStartFrame`, meanwhile, is
|
|
3148
|
+
file-relative. Mixing the two spaces is what made `source_end` exceed the
|
|
3149
|
+
clip's own length by the whole start timecode (#147).
|
|
3150
|
+
|
|
3151
|
+
The offset is recoverable from the item alone, with no timecode parsing and
|
|
3152
|
+
no drop-frame arithmetic: the same edit point read both ways differs by
|
|
3153
|
+
exactly the offset. Returns None when it cannot be established, and 0.0 on
|
|
3154
|
+
media starting at 00:00:00:00 — where every caller below is then a no-op,
|
|
3155
|
+
which is why zero-TC behavior does not move.
|
|
3156
|
+
"""
|
|
3157
|
+
if source_start is None or not source_fps:
|
|
3158
|
+
return None
|
|
3159
|
+
# GetLeftOffset counts an audio item in TIMELINE frames, so it is not the
|
|
3160
|
+
# same space as the second-readers and cannot calibrate them.
|
|
3161
|
+
if source_start_origin != "GetSourceStartFrame":
|
|
3162
|
+
return None
|
|
3163
|
+
start_seconds = _timeline_item_source_time(item, "GetSourceStartTime")
|
|
3164
|
+
if start_seconds is None:
|
|
3165
|
+
return None
|
|
3166
|
+
return start_seconds - (source_start / source_fps)
|
|
3167
|
+
|
|
3168
|
+
|
|
3169
|
+
def _timeline_item_source_end_exclusive(item, source_start, duration, source_fps,
|
|
3170
|
+
source_start_origin=None):
|
|
3142
3171
|
"""The EXCLUSIVE source-space end frame — the contract every caller assumes.
|
|
3143
3172
|
|
|
3144
3173
|
`source_end` has always been exclusive (`src_end_excl` at the append site,
|
|
@@ -3161,23 +3190,47 @@ def _timeline_item_source_end_exclusive(item, source_start, duration, source_fps
|
|
|
3161
3190
|
the two regimes (see above), so building on it would mean branching on a
|
|
3162
3191
|
rate comparison this function would first have to reconstruct.
|
|
3163
3192
|
|
|
3193
|
+
That product is only a FILE-relative frame when the media starts at
|
|
3194
|
+
00:00:00:00, though. `GetSourceEndTime` answers in the media's timecode
|
|
3195
|
+
space, so on camera footage carrying time-of-day or continuous TC it bakes
|
|
3196
|
+
the start timecode into the number: measured on a Canon MP4 starting at
|
|
3197
|
+
04:18:37;25, it produced source_end 468800 on a clip only 10650 frames long
|
|
3198
|
+
(#147). `source_start` is file-relative, so the two fields stopped sharing an
|
|
3199
|
+
origin and could no longer be differenced.
|
|
3200
|
+
|
|
3201
|
+
The span between the two second-readers is offset-free whichever space they
|
|
3202
|
+
are in, so anchoring that span on the file-relative `source_start` is correct
|
|
3203
|
+
under both conventions and needs no timecode parsing. On zero-TC media it
|
|
3204
|
+
reproduces the plain product exactly, so nothing moves there.
|
|
3205
|
+
|
|
3164
3206
|
Falls back to the historical `source_start + duration` when the second-reader
|
|
3165
3207
|
or the rate is unreadable — same value as before, so an older build loses the
|
|
3166
3208
|
correction rather than the field.
|
|
3167
3209
|
"""
|
|
3168
3210
|
end_seconds = _timeline_item_source_time(item, "GetSourceEndTime")
|
|
3169
3211
|
if end_seconds is not None and source_fps:
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3212
|
+
offset = _timeline_item_source_time_offset(
|
|
3213
|
+
item, source_start, source_fps, source_start_origin)
|
|
3214
|
+
if offset is not None:
|
|
3215
|
+
span = int(round((end_seconds - offset) * source_fps)) - source_start
|
|
3216
|
+
# A non-positive span means the readers disagree about this item;
|
|
3217
|
+
# the derived value is safer than an inverted range.
|
|
3218
|
+
if span > 0:
|
|
3219
|
+
return source_start + span
|
|
3220
|
+
else:
|
|
3221
|
+
# No way to calibrate the origin: pre-#147 behavior, which is exact
|
|
3222
|
+
# on media starting at 00:00:00:00 and on every build old enough to
|
|
3223
|
+
# lack GetSourceStartTime that we have measured.
|
|
3224
|
+
end_frame = int(round(end_seconds * source_fps))
|
|
3225
|
+
if source_start is None or end_frame > source_start:
|
|
3226
|
+
return end_frame
|
|
3175
3227
|
if source_start is not None and duration is not None:
|
|
3176
3228
|
return source_start + duration
|
|
3177
3229
|
return None
|
|
3178
3230
|
|
|
3179
3231
|
|
|
3180
|
-
def _timeline_item_source_seconds(item, source_start, source_end, source_fps
|
|
3232
|
+
def _timeline_item_source_seconds(item, source_start, source_end, source_fps,
|
|
3233
|
+
source_start_origin=None):
|
|
3181
3234
|
"""(start_seconds, end_seconds) into the source file, or None each.
|
|
3182
3235
|
|
|
3183
3236
|
Prefers Resolve's second-readers, then a source-space frame divided by the
|
|
@@ -3185,14 +3238,26 @@ def _timeline_item_source_seconds(item, source_start, source_end, source_fps):
|
|
|
3185
3238
|
is ``source_start + timeline_duration``, so on a 24 fps WAV in a 29.97 fps
|
|
3186
3239
|
timeline it overstates the clip's span by 25% (18.1 s reported for a
|
|
3187
3240
|
14.5 s clip). An unknown end reads as unknown.
|
|
3241
|
+
|
|
3242
|
+
Both readers answer in the media's TIMECODE space, so they are rebased onto
|
|
3243
|
+
the file-relative origin the frame fields use (#147) — otherwise
|
|
3244
|
+
``source_start_seconds`` disagrees with ``source_start / source_fps`` on any
|
|
3245
|
+
camera clip, and "seconds into the source file" is not what the field holds.
|
|
3246
|
+
The correction is exactly zero on media starting at 00:00:00:00.
|
|
3188
3247
|
"""
|
|
3248
|
+
offset = _timeline_item_source_time_offset(
|
|
3249
|
+
item, source_start, source_fps, source_start_origin)
|
|
3189
3250
|
start_seconds = _timeline_item_source_time(item, "GetSourceStartTime")
|
|
3190
3251
|
if start_seconds is None:
|
|
3191
3252
|
start_seconds = _source_frames_to_seconds(source_start, source_fps)
|
|
3253
|
+
elif offset is not None:
|
|
3254
|
+
start_seconds = round(start_seconds - offset, 3)
|
|
3192
3255
|
end_seconds = _timeline_item_source_time(item, "GetSourceEndTime")
|
|
3193
3256
|
if end_seconds is None:
|
|
3194
3257
|
end_seconds = _source_frames_to_seconds(
|
|
3195
3258
|
_timeline_item_source_end_frame(item), source_fps)
|
|
3259
|
+
elif offset is not None:
|
|
3260
|
+
end_seconds = round(end_seconds - offset, 3)
|
|
3196
3261
|
return start_seconds, end_seconds
|
|
3197
3262
|
|
|
3198
3263
|
|
|
@@ -3221,7 +3286,7 @@ def _timeline_item_summary(item, track_info=None, *, media_pool_item=None,
|
|
|
3221
3286
|
# of whichever reader produced source_start, so the audio caveat that blanks
|
|
3222
3287
|
# source_fps must not blank the end as well.
|
|
3223
3288
|
source_end = _timeline_item_source_end_exclusive(
|
|
3224
|
-
item, source_start, duration, media_fps)
|
|
3289
|
+
item, source_start, duration, media_fps, source_start_origin)
|
|
3225
3290
|
source_fps = media_fps
|
|
3226
3291
|
if source_start_origin == "GetLeftOffset" and (track_info or (None,))[0] == "audio":
|
|
3227
3292
|
# GetLeftOffset counts an audio item in TIMELINE frames, so pairing it
|
|
@@ -3229,7 +3294,7 @@ def _timeline_item_summary(item, track_info=None, *, media_pool_item=None,
|
|
|
3229
3294
|
# the frame and leave the rate unknown rather than convert it wrong.
|
|
3230
3295
|
source_fps = None
|
|
3231
3296
|
source_start_seconds, source_end_seconds = _timeline_item_source_seconds(
|
|
3232
|
-
item, source_start, source_end, source_fps)
|
|
3297
|
+
item, source_start, source_end, source_fps, source_start_origin)
|
|
3233
3298
|
summary = {
|
|
3234
3299
|
"timeline_item_id": _safe_timeline_item_id(item),
|
|
3235
3300
|
"name": _safe_timeline_item_name(item),
|
|
@@ -3238,10 +3303,13 @@ def _timeline_item_summary(item, track_info=None, *, media_pool_item=None,
|
|
|
3238
3303
|
"start": start,
|
|
3239
3304
|
"end": end,
|
|
3240
3305
|
"duration": duration,
|
|
3306
|
+
# All four source_* fields below are FILE-relative: frame 0 / 0.0 s is the
|
|
3307
|
+
# first frame of the media, whatever timecode the camera stamped on it.
|
|
3241
3308
|
"source_start": source_start,
|
|
3242
|
-
# EXCLUSIVE source frame.
|
|
3243
|
-
#
|
|
3244
|
-
#
|
|
3309
|
+
# EXCLUSIVE source frame. The span between Resolve's second-readers,
|
|
3310
|
+
# anchored on source_start so a non-zero start timecode cannot leak in;
|
|
3311
|
+
# falls back to source_start + TIMELINE duration only when those readers
|
|
3312
|
+
# or the media rate are unavailable, which is the old unit-mixed value.
|
|
3245
3313
|
"source_end": source_end,
|
|
3246
3314
|
"source_fps": source_fps,
|
|
3247
3315
|
"source_start_seconds": source_start_seconds,
|
|
@@ -22275,10 +22343,13 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
22275
22343
|
freezes the PROJECT's rate at import, so it differs from the timeline whenever
|
|
22276
22344
|
the project moved afterwards, and dividing by the timeline rate is then wrong by
|
|
22277
22345
|
minutes. source_fps is null when the rate could not be read; treat the frames as
|
|
22278
|
-
unitless then, do not assume the timeline's.
|
|
22279
|
-
|
|
22280
|
-
|
|
22281
|
-
|
|
22346
|
+
unitless then, do not assume the timeline's. All four source_* fields are
|
|
22347
|
+
FILE-relative — frame 0 is the head of the media, whatever timecode the camera
|
|
22348
|
+
stamped on it. source_end is the span between Resolve's source second-readers
|
|
22349
|
+
anchored on source_start, so it is a source frame even when the rates differ and
|
|
22350
|
+
even on media with a non-zero start TC (those readers are timecode-absolute; the
|
|
22351
|
+
offset cancels in the span). It falls back to source_start + TIMELINE duration
|
|
22352
|
+
(unit-mixed) only when those readers or the rate are unavailable.
|
|
22282
22353
|
detect_gaps_overlaps(track_types?, min_gap?) -> {gaps, overlaps}
|
|
22283
22354
|
source_range_report(handles?, merge?) -> {ranges, occurrences}
|
|
22284
22355
|
export_timeline_checked(path, format?|type?, subtype?, require_temp_path?, dry_run?, background?) -> {success, path, size | job_id}
|
package/src/utils/api_truth.py
CHANGED
|
@@ -913,8 +913,33 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
913
913
|
"regimes is GetSourceEndTime x media_fps, which was exact "
|
|
914
914
|
"on 12 of 12 valid items (30.633 s x 24 = 735.19 -> 735; "
|
|
915
915
|
"24.524 s x 29.97 = 734.98 -> 735) — seconds carry no "
|
|
916
|
-
"frame-rate assumption, so the product is in source space "
|
|
917
|
-
"
|
|
916
|
+
"frame-rate assumption, so the product is in source space. "
|
|
917
|
+
"But NOT necessarily at the source's ORIGIN: all 12 of those "
|
|
918
|
+
"items were synthetic media starting at 00:00:00:00. The "
|
|
919
|
+
"second-readers are TIMECODE-absolute, so on media with a "
|
|
920
|
+
"non-zero start TC the product is a source-RATE frame "
|
|
921
|
+
"counted from the camera's timecode zero, not from the head "
|
|
922
|
+
"of the file (issue #147, corrected in v2.97.3 — see the "
|
|
923
|
+
"handling note below). Use the SPAN between the two "
|
|
924
|
+
"second-readers, never either one alone: the offset cancels "
|
|
925
|
+
"in the difference. CONFIRMED LIVE here on Studio 19.1.3.7 "
|
|
926
|
+
"(2026-08-17), independently of the 21.0.4.5 report, with a "
|
|
927
|
+
"CONTROLLED PAIR: two ffmpeg-generated 29.97 clips identical "
|
|
928
|
+
"except that one carries -timecode 04:18:37;25, both cut "
|
|
929
|
+
"into a 24 fps timeline. On the timecoded copy "
|
|
930
|
+
"GetSourceStartTime read 15527.812 s where the file-relative "
|
|
931
|
+
"answer is 10.010 s, while GetSourceStartFrame read 300 — "
|
|
932
|
+
"the two readers in different spaces on the same edit point. "
|
|
933
|
+
"round(GetSourceEndTime x fps) gave 465461 on a 1799-frame "
|
|
934
|
+
"clip. After the fix the timecoded copy reports exactly what "
|
|
935
|
+
"the zero-TC control reports for every range tried "
|
|
936
|
+
"(300..393, 0..100, 1000..1500). Harness: "
|
|
937
|
+
"tests/live_source_timecode_validation.py. NOTE the harness "
|
|
938
|
+
"does NOT assert source_end == the endFrame sent: when the "
|
|
939
|
+
"rates differ the record duration quantizes to whole "
|
|
940
|
+
"TIMELINE frames, so a 93-source-frame request consumes "
|
|
941
|
+
"~92.4 and BOTH copies report 392. That is rate conversion, "
|
|
942
|
+
"not a timecode error.",
|
|
918
943
|
"recommended": "Convert an audio item's source frames with the MEDIA's rate, "
|
|
919
944
|
"never the timeline's: seconds = source_start / media_fps. "
|
|
920
945
|
"READ media_fps from the media-pool item's 'FPS' clip property "
|
|
@@ -936,9 +961,29 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
936
961
|
"reports the rate as unknown rather than converting a "
|
|
937
962
|
"timeline-frame value at the media rate. source_end is "
|
|
938
963
|
"no longer source_start + TIMELINE duration: as of "
|
|
939
|
-
"v2.93.0 it is
|
|
940
|
-
"
|
|
941
|
-
"
|
|
964
|
+
"v2.93.0 it is read from GetSourceEndTime and stays "
|
|
965
|
+
"EXCLUSIVE as every caller already assumed. CORRECTION "
|
|
966
|
+
"(v2.97.3, issue #147): that release computed it as "
|
|
967
|
+
"round(GetSourceEndTime x media_fps) and this entry "
|
|
968
|
+
"called the result 'a SOURCE frame by construction'. "
|
|
969
|
+
"That holds ONLY for media starting at 00:00:00:00. "
|
|
970
|
+
"Both second-readers answer in the media's TIMECODE "
|
|
971
|
+
"space, so on a camera clip carrying time-of-day or "
|
|
972
|
+
"continuous TC the product has the whole start "
|
|
973
|
+
"timecode baked into it: reported on a Canon MP4 "
|
|
974
|
+
"starting at 04:18:37;25, source_end came back 468800 "
|
|
975
|
+
"on a clip only 10650 frames long, ~44x its length, "
|
|
976
|
+
"while source_start (GetSourceStartFrame) stayed "
|
|
977
|
+
"file-relative — so the two fields no longer shared an "
|
|
978
|
+
"origin and could not be differenced. The v2.93.0 live "
|
|
979
|
+
"validation used synthetic media, which starts at "
|
|
980
|
+
"00:00:00:00, so the offset was zero and the error was "
|
|
981
|
+
"invisible in exactly that setup. It is now the seconds "
|
|
982
|
+
"SPAN between the two readers, anchored on source_start: "
|
|
983
|
+
"the offset cancels in the difference, so the result is "
|
|
984
|
+
"file-relative under either convention and needs no "
|
|
985
|
+
"timecode parsing. All four source_* fields are "
|
|
986
|
+
"file-relative, the _seconds pair included. Measured "
|
|
942
987
|
"live on 19.1.3.7 over 8 items in both regimes, it "
|
|
943
988
|
"equals the endFrame actually sent every time, and it "
|
|
944
989
|
"reproduces the old value exactly wherever the old value "
|
|
@@ -954,6 +999,7 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
954
999
|
"submit": "bug",
|
|
955
1000
|
"mitigation": ["_media_item_source_fps", "_source_frames_to_seconds",
|
|
956
1001
|
"_timeline_item_source_seconds",
|
|
1002
|
+
"_timeline_item_source_time_offset",
|
|
957
1003
|
"_timeline_item_source_end_exclusive"],
|
|
958
1004
|
},
|
|
959
1005
|
{
|