@motionharness/bridge-davinci 0.0.22 → 0.0.25
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/NOTICE +1 -1
- package/README.md +24 -14
- package/bridge-manifest.json +43 -28
- package/dist/inspector_runtime.py +4 -4
- package/dist/instance_runtime.py +1 -1
- package/dist/legacy_conversion.py +3 -5
- package/dist/resolve_rpc.py +109 -190
- package/dist/runner.mjs +699 -387
- package/package.json +2 -2
- package/resources/HotionHarness.ofx.bundle/Contents/Info.plist +1 -1
- package/resources/HotionHarness.ofx.bundle/Contents/MacOS/HotionHarness.ofx +0 -0
- package/resources/{HotionHarnessStatus.app → MotionHarnessBridge.app}/Contents/Info.plist +3 -3
- package/resources/MotionHarnessBridge.app/Contents/MacOS/MotionHarnessBridge +0 -0
- package/resources/MotionHarnessBridge.app/Contents/Resources/en.lproj/InfoPlist.strings +2 -0
- package/resources/MotionHarnessBridge.app/Contents/Resources/zh-Hans.lproj/InfoPlist.strings +2 -0
- package/resources/{HotionHarnessInstall → MotionHarnessInstall} +0 -0
- package/resources/legacy-forwarder.lua +2 -2
- package/resources/menu-entry.py +35 -21
- package/resources/sidebar-help/en.html +13 -0
- package/resources/sidebar-help/guide.css +14 -0
- package/resources/sidebar-help/index.html +10 -32
- package/resources/HotionHarnessStatus.app/Contents/MacOS/HotionHarnessStatus +0 -0
- package/resources/sidebar-help/resolve-sidebar.png +0 -0
package/dist/resolve_rpc.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""One JSON request over stdin. Only documented Resolve scripting APIs are used.
|
|
3
3
|
|
|
4
4
|
Only explicit targets and owned render IDs can be modified. Resolve's database
|
|
5
|
-
is never edited directly.
|
|
6
|
-
|
|
5
|
+
is never edited directly. Updating a bound media-pool item leaves its timeline
|
|
6
|
+
instances and effects under Resolve ownership.
|
|
7
7
|
"""
|
|
8
8
|
import hashlib
|
|
9
9
|
import importlib
|
|
@@ -29,8 +29,6 @@ import instance_runtime as instances
|
|
|
29
29
|
import legacy_conversion as legacy
|
|
30
30
|
from runtime_io import atomic_json, file_digest
|
|
31
31
|
|
|
32
|
-
# Frame boundaries were read back on this exact build; do not extrapolate to unknown builds.
|
|
33
|
-
VALIDATED_BUILD = "21.1.0.14"
|
|
34
32
|
SUPPORTED_INSPECTOR_EFFECTS = {
|
|
35
33
|
"Transform", "Source Transform", "Immersive Transform", "Cropping", "Source Cropping",
|
|
36
34
|
"Source Looks", "Dynamic Zoom", "Composite", "Lens Correction", "Retime and Scaling",
|
|
@@ -39,10 +37,6 @@ SUPPORTED_INSPECTOR_EFFECTS = {
|
|
|
39
37
|
ORIGINAL_PLAYBACK = {"perfProxyMediaMode": "0", "perfOptimisedMediaOn": "0", "perfRenderCacheMode": "none"}
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
def validated_host(resolve):
|
|
43
|
-
return resolve.GetVersionString() == VALIDATED_BUILD and sys.platform == "darwin" and platform.machine() == "arm64"
|
|
44
|
-
|
|
45
|
-
|
|
46
40
|
class BridgeFailure(Exception):
|
|
47
41
|
def __init__(self, code, message, **details):
|
|
48
42
|
super().__init__(message)
|
|
@@ -88,8 +82,7 @@ def project_for(resolve, requested=None, render_proof=None):
|
|
|
88
82
|
# GetCurrentDatabase returns None throughout some renders on 21.1.0.14.
|
|
89
83
|
# Monitoring/cancelling an already-started job may use its previously
|
|
90
84
|
# verified project + complete queue fingerprint; starting still requires DB.
|
|
91
|
-
if
|
|
92
|
-
and validated_host(resolve) and project.IsRenderingInProgress()):
|
|
85
|
+
if render_proof and requested and not manager.GetCurrentDatabase() and project.IsRenderingInProgress():
|
|
93
86
|
require(project.GetUniqueId() == requested["projectId"], "VERSION_CONFLICT", "运行中的任务已不属于原工程。")
|
|
94
87
|
job = render_job(project, render_proof["jobId"])
|
|
95
88
|
require(render_proof.get("expectedJobFingerprint") and fingerprint(job) == render_proof["expectedJobFingerprint"],
|
|
@@ -127,91 +120,36 @@ def media_for(project, media_id):
|
|
|
127
120
|
|
|
128
121
|
|
|
129
122
|
def ofx_source_target(resolve, params):
|
|
130
|
-
"""
|
|
123
|
+
"""Match the actual input file to its registered media-pool identity.
|
|
131
124
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
File-less timeline/compound entries are excluded only when GetTimeline()
|
|
135
|
-
returns an identifiable timeline. Other unclassifiable entries fail closed.
|
|
125
|
+
Read file paths once to reject ambiguous imports. Only the target's bridge
|
|
126
|
+
metadata matters; unrelated subtitles, generators and metadata are ignored.
|
|
136
127
|
"""
|
|
137
|
-
def identifier(value):
|
|
138
|
-
return isinstance(value, str) and bool(value.strip()) and "\0" not in value
|
|
139
|
-
|
|
140
|
-
def canonical_path(value, code):
|
|
141
|
-
require(identifier(value) and os.path.isabs(value), code, "侧栏源路径必须是非空绝对路径。")
|
|
142
|
-
return os.path.realpath(value)
|
|
143
|
-
|
|
144
128
|
requested = params.get("scope")
|
|
129
|
+
media_id, raw_path = params.get("mediaPoolItemId"), params.get("path")
|
|
130
|
+
valid = lambda value: isinstance(value, str) and bool(value.strip()) and "\0" not in value
|
|
145
131
|
require(isinstance(requested, dict) and set(requested) == {"databaseId", "projectId"}
|
|
146
|
-
and all(
|
|
147
|
-
"INVALID_ARGUMENT", "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
require(isinstance(metadata, dict) and all(isinstance(key, str) and isinstance(value, str)
|
|
168
|
-
for key, value in metadata.items()), "CAPABILITY_UNAVAILABLE", "无法完整读取素材关联元数据。")
|
|
169
|
-
# Copy values so a host response or test double cannot mutate our proof.
|
|
170
|
-
return identity, {"path": source_path, "metadata": dict(metadata)}
|
|
171
|
-
|
|
172
|
-
def scan(project):
|
|
173
|
-
pool = project.GetMediaPool()
|
|
174
|
-
root = pool.GetRootFolder() if pool else None
|
|
175
|
-
require(root is not None, "CAPABILITY_UNAVAILABLE", "无法读取素材池根目录。")
|
|
176
|
-
folders, media_states, objects = {}, {}, {}
|
|
177
|
-
pending = [root]
|
|
178
|
-
while pending:
|
|
179
|
-
folder = pending.pop()
|
|
180
|
-
folder_id = folder.GetUniqueId()
|
|
181
|
-
require(identifier(folder_id) and folder_id not in folders,
|
|
182
|
-
"CAPABILITY_UNAVAILABLE", "素材池目录 ID 缺失、重复或存在循环。")
|
|
183
|
-
clips, children = folder.GetClipList(), folder.GetSubFolderList()
|
|
184
|
-
require(isinstance(clips, list) and isinstance(children, list),
|
|
185
|
-
"CAPABILITY_UNAVAILABLE", "素材池目录读取不完整,不能证明侧栏源唯一。")
|
|
186
|
-
folders[folder_id] = []
|
|
187
|
-
for media in clips:
|
|
188
|
-
identity, state = read_media(media)
|
|
189
|
-
require(identity not in media_states, "CAPABILITY_UNAVAILABLE", "素材池对象 ID 重复,不能证明侧栏源唯一。")
|
|
190
|
-
media_states[identity], objects[identity] = state, media
|
|
191
|
-
folders[folder_id].append(identity)
|
|
192
|
-
folders[folder_id].sort()
|
|
193
|
-
pending.extend(children)
|
|
194
|
-
matches = [identity for identity, state in media_states.items() if state.get("path") == path]
|
|
195
|
-
require(len(matches) == 1 and matches[0] == media_id, "VERSION_CONFLICT",
|
|
196
|
-
"侧栏源路径没有对应唯一且匹配的素材池对象;请从固定菜单管理片段。")
|
|
197
|
-
return {"folders": folders, "media": media_states}, objects[media_id]
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
project, scope = project_for(resolve, requested)
|
|
201
|
-
first, _ = scan(project)
|
|
202
|
-
project_for(resolve, requested)
|
|
203
|
-
second, target = scan(project)
|
|
204
|
-
require(first == second, "VERSION_CONFLICT", "素材池在侧栏身份核对期间发生变化。")
|
|
205
|
-
identity, final_state = read_media(target)
|
|
206
|
-
require(identity == media_id and final_state == second["media"][media_id],
|
|
207
|
-
"VERSION_CONFLICT", "素材路径或关联元数据在侧栏身份核对期间发生变化。")
|
|
208
|
-
project_for(resolve, requested)
|
|
209
|
-
return {"scope": scope, "mediaPoolItemId": media_id, "path": path,
|
|
210
|
-
"metadata": final_state["metadata"], "unique": True}
|
|
211
|
-
except BridgeFailure:
|
|
212
|
-
raise
|
|
213
|
-
except Exception as error:
|
|
214
|
-
raise BridgeFailure("CAPABILITY_UNAVAILABLE", "无法完整核对侧栏素材身份。", diagnostic=str(error))
|
|
132
|
+
and all(valid(value) for value in requested.values()) and valid(media_id),
|
|
133
|
+
"INVALID_ARGUMENT", "侧栏入口需要明确的数据库、工程和素材池对象 ID。")
|
|
134
|
+
require(valid(raw_path) and os.path.isabs(raw_path), "INVALID_ARGUMENT", "侧栏源路径必须是非空绝对路径。")
|
|
135
|
+
path = os.path.realpath(raw_path)
|
|
136
|
+
project, scope = project_for(resolve, requested)
|
|
137
|
+
matches = []
|
|
138
|
+
for media in all_media(project.GetMediaPool().GetRootFolder()):
|
|
139
|
+
try:
|
|
140
|
+
properties = media.GetClipProperty() or {}
|
|
141
|
+
source_path = properties.get("File Path")
|
|
142
|
+
except Exception:
|
|
143
|
+
require(media.GetUniqueId() != media_id, "CAPABILITY_UNAVAILABLE", "无法读取目标素材的文件路径。")
|
|
144
|
+
continue
|
|
145
|
+
if valid(source_path) and os.path.isabs(source_path) and os.path.realpath(source_path) == path:
|
|
146
|
+
matches.append(media)
|
|
147
|
+
require(len(matches) == 1 and matches[0].GetUniqueId() == media_id,
|
|
148
|
+
"VERSION_CONFLICT", "侧栏源路径没有对应唯一且匹配的素材池对象;请从固定菜单管理片段。")
|
|
149
|
+
result = media_identity(project, scope, matches[0])
|
|
150
|
+
require(os.path.realpath(result["path"]) == path, "VERSION_CONFLICT", "目标素材已被重新链接。")
|
|
151
|
+
project_for(resolve, requested)
|
|
152
|
+
return {**result, "path": path, "unique": True}
|
|
215
153
|
|
|
216
154
|
|
|
217
155
|
def fps_value(value):
|
|
@@ -234,13 +172,13 @@ def sample_end_time(start, timeline_frames, source_fps, timeline_fps, source_fra
|
|
|
234
172
|
return min(source_frames / source_rate, start / source_rate + (timeline_frames - 1) / timeline_rate + 1 / source_rate)
|
|
235
173
|
|
|
236
174
|
|
|
237
|
-
def item_snapshot(item, timeline_id, track,
|
|
175
|
+
def item_snapshot(item, timeline_id, track, media_index=None, timeline_fps=None):
|
|
238
176
|
media = item.GetMediaPoolItem()
|
|
239
|
-
intrinsic_entry = intrinsic.read_entry(item, media_index or {}) if
|
|
177
|
+
intrinsic_entry = intrinsic.read_entry(item, media_index or {}) if not media else None
|
|
240
178
|
if intrinsic_entry:
|
|
241
179
|
media = media_index[intrinsic_entry['identity']['mediaPoolItemId']]
|
|
242
|
-
unsupported = []
|
|
243
|
-
kind = item.GetType()
|
|
180
|
+
unsupported = []
|
|
181
|
+
kind = item.GetType()
|
|
244
182
|
if not media and kind != "transition":
|
|
245
183
|
unsupported.append("no-plain-media-source")
|
|
246
184
|
fusion_count = item.GetFusionCompCount() if media else 0
|
|
@@ -254,7 +192,7 @@ def item_snapshot(item, timeline_id, track, validated=False, media_index=None, t
|
|
|
254
192
|
unsupported.append("take-selector-or-multicam")
|
|
255
193
|
props = media.GetClipProperty() if media else {}
|
|
256
194
|
speed, properties, fades = None, None, None
|
|
257
|
-
if
|
|
195
|
+
if media:
|
|
258
196
|
if media.GetTimeline():
|
|
259
197
|
unsupported.append("nested-or-compound-timeline")
|
|
260
198
|
if kind != "video":
|
|
@@ -313,12 +251,12 @@ def item_snapshot(item, timeline_id, track, validated=False, media_index=None, t
|
|
|
313
251
|
"sourceIn": source_in, "sourceOut": source_out,
|
|
314
252
|
"mediaPoolItemId": media.GetUniqueId() if media else None, "unsupported": unsupported,
|
|
315
253
|
"kind": kind, "speed": speed, "properties": properties, "fades": fades,
|
|
316
|
-
"enabled": item.GetClipEnabled() if
|
|
317
|
-
"sourceStartTime": item.GetSourceStartTime() if
|
|
318
|
-
"sourceEndTime": item.GetSourceEndTime() if
|
|
319
|
-
"leftOffset": item.GetLeftOffset() if
|
|
320
|
-
"rightOffset": item.GetRightOffset() if
|
|
321
|
-
"markers": item.GetMarkers() if
|
|
254
|
+
"enabled": item.GetClipEnabled() if media else None,
|
|
255
|
+
"sourceStartTime": item.GetSourceStartTime() if media else None,
|
|
256
|
+
"sourceEndTime": item.GetSourceEndTime() if media else None,
|
|
257
|
+
"leftOffset": item.GetLeftOffset() if media else None,
|
|
258
|
+
"rightOffset": item.GetRightOffset() if media else None,
|
|
259
|
+
"markers": item.GetMarkers() if media else None,
|
|
322
260
|
**({"inspectorEntry": entry} if entry else {}),
|
|
323
261
|
**({"intrinsicEntry": intrinsic_entry} if intrinsic_entry else {}),
|
|
324
262
|
}
|
|
@@ -575,9 +513,6 @@ def grade_evidence(resolve, timeline, native_items, intrinsic_ids=None):
|
|
|
575
513
|
|
|
576
514
|
@contextmanager
|
|
577
515
|
def timeline_context(resolve, timeline):
|
|
578
|
-
if resolve is None or not validated_host(resolve):
|
|
579
|
-
yield
|
|
580
|
-
return
|
|
581
516
|
project = resolve.GetProjectManager().GetCurrentProject()
|
|
582
517
|
previous = project.GetCurrentTimeline()
|
|
583
518
|
try:
|
|
@@ -593,42 +528,41 @@ def timeline_context(resolve, timeline):
|
|
|
593
528
|
await_timeline(project, previous, resolve=resolve)
|
|
594
529
|
|
|
595
530
|
|
|
596
|
-
def scan_timeline(timeline, resolve
|
|
531
|
+
def scan_timeline(timeline, resolve, grade_media_id=None):
|
|
597
532
|
with timeline_context(resolve, timeline):
|
|
598
533
|
return scan_current_timeline(timeline, resolve, grade_media_id)
|
|
599
534
|
|
|
600
535
|
|
|
601
|
-
def scan_current_timeline(timeline, resolve
|
|
602
|
-
validated = resolve is not None and validated_host(resolve)
|
|
536
|
+
def scan_current_timeline(timeline, resolve, grade_media_id=None):
|
|
603
537
|
items, native_items, paths = [], [], {}
|
|
604
538
|
media_index = None
|
|
605
539
|
for track in range(1, timeline.GetTrackCount("video") + 1):
|
|
606
540
|
native = timeline.GetItemListInTrack("video", track) or []
|
|
607
541
|
native_items.extend(native)
|
|
608
542
|
for item in native:
|
|
609
|
-
if
|
|
543
|
+
if not item.GetMediaPoolItem() and item.GetFusionCompCount() > 0 and media_index is None:
|
|
610
544
|
project = resolve.GetProjectManager().GetCurrentProject()
|
|
611
545
|
media_index = {m.GetUniqueId(): m for m in all_media(project.GetMediaPool().GetRootFolder())}
|
|
612
|
-
snapshot = item_snapshot(item, timeline.GetUniqueId(), track,
|
|
613
|
-
snapshot.update(trackEnabled=timeline.GetIsTrackEnabled("video", track)
|
|
614
|
-
trackLocked=timeline.GetIsTrackLocked("video", track)
|
|
546
|
+
snapshot = item_snapshot(item, timeline.GetUniqueId(), track, media_index, fps_value(timeline.GetSetting("timelineFrameRate")))
|
|
547
|
+
snapshot.update(trackEnabled=timeline.GetIsTrackEnabled("video", track),
|
|
548
|
+
trackLocked=timeline.GetIsTrackLocked("video", track))
|
|
615
549
|
items.append(snapshot)
|
|
616
|
-
if
|
|
550
|
+
if item.GetMediaPoolItem():
|
|
617
551
|
paths[snapshot["id"]] = item.GetMediaPoolItem().GetClipProperty("File Path") or ""
|
|
618
552
|
elif snapshot.get('intrinsicEntry'):
|
|
619
553
|
paths[snapshot['id']] = media_index[snapshot['mediaPoolItemId']].GetClipProperty('File Path') or ''
|
|
620
|
-
evidence = otio_evidence(resolve, timeline) if
|
|
621
|
-
if
|
|
554
|
+
evidence = otio_evidence(resolve, timeline) if items else {"complete": True, "reasons": [], "hash": None}
|
|
555
|
+
if items and evidence.get("clipCount") != sum(1 for item in items if item["kind"] == "video" and item["mediaPoolItemId"]):
|
|
622
556
|
evidence["reasons"] = sorted(set(evidence["reasons"] + ["otio-item-coverage-mismatch"]))
|
|
623
557
|
evidence["globalReasons"] = sorted(set(evidence.get("globalReasons", evidence["reasons"]) + ["otio-item-coverage-mismatch"]))
|
|
624
|
-
scoped_reasons = otio_item_reasons(evidence, items, paths, timeline.GetStartFrame(), fps_value(timeline.GetSetting("timelineFrameRate"))) if
|
|
558
|
+
scoped_reasons = otio_item_reasons(evidence, items, paths, timeline.GetStartFrame(), fps_value(timeline.GetSetting("timelineFrameRate"))) if items else {}
|
|
625
559
|
# A media readback retains grades only for that media's uses. Keep every
|
|
626
560
|
# native item and OTIO check for indirect references, but avoid exporting
|
|
627
561
|
# unrelated LUTs. Whole-timeline snapshots still inspect all grades.
|
|
628
562
|
grade_items = [native for native, item in zip(native_items, items)
|
|
629
563
|
if grade_media_id is None or item["mediaPoolItemId"] == grade_media_id]
|
|
630
|
-
grades = grade_evidence(resolve, timeline, grade_items, {i['id'] for i in items if i.get('intrinsicEntry')}) if
|
|
631
|
-
entry_scope = scope_for(resolve.GetProjectManager(), resolve.GetProjectManager().GetCurrentProject()) if
|
|
564
|
+
grades = grade_evidence(resolve, timeline, grade_items, {i['id'] for i in items if i.get('intrinsicEntry')}) if grade_items and any(i["kind"] == "video" for i in items) else {}
|
|
565
|
+
entry_scope = scope_for(resolve.GetProjectManager(), resolve.GetProjectManager().GetCurrentProject()) if any(i.get("inspectorEntry") or i.get('intrinsicEntry') for i in items) else None
|
|
632
566
|
for item in items:
|
|
633
567
|
item["otioHash"] = evidence["hash"]
|
|
634
568
|
item["unsupported"] = sorted(set(item["unsupported"] + scoped_reasons.get(item["id"], evidence["reasons"])))
|
|
@@ -640,10 +574,9 @@ def scan_current_timeline(timeline, resolve=None, grade_media_id=None):
|
|
|
640
574
|
owner = entry["identity"]
|
|
641
575
|
if any(owner[key] != value for key, value in entry_scope.items()):
|
|
642
576
|
item["unsupported"] = sorted(set(item["unsupported"] + ["inspector-identity-conflict"]))
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
for item in items: item["unsupported"] = sorted(set(item["unsupported"] + ["timeline-grade-unverified"]))
|
|
577
|
+
graph = timeline.GetNodeGraph()
|
|
578
|
+
if graph and any(graph.GetToolsInNode(index) for index in range(1, graph.GetNumNodes() + 1)):
|
|
579
|
+
for item in items: item["unsupported"] = sorted(set(item["unsupported"] + ["timeline-grade-unverified"]))
|
|
647
580
|
return items
|
|
648
581
|
|
|
649
582
|
|
|
@@ -685,8 +618,28 @@ def publish_playback(path, publication, before_path):
|
|
|
685
618
|
os.unlink(pending)
|
|
686
619
|
|
|
687
620
|
|
|
688
|
-
def
|
|
689
|
-
|
|
621
|
+
def media_identity(project, scope, media):
|
|
622
|
+
"""The original media owns the binding, independently of its timeline uses."""
|
|
623
|
+
try:
|
|
624
|
+
properties = media.GetClipProperty() or {}
|
|
625
|
+
metadata = media.GetThirdPartyMetadata() or {}
|
|
626
|
+
except Exception as error:
|
|
627
|
+
raise BridgeFailure("CAPABILITY_UNAVAILABLE", "无法读取目标原素材的身份或关联。", diagnostic=str(error))
|
|
628
|
+
require(isinstance(properties, dict) and isinstance(metadata, dict)
|
|
629
|
+
and all(isinstance(key, str) and isinstance(value, str) for key, value in metadata.items()),
|
|
630
|
+
"CAPABILITY_UNAVAILABLE", "无法读取目标素材的身份或关联。")
|
|
631
|
+
data = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media.GetUniqueId(),
|
|
632
|
+
"path": properties.get("File Path", ""), "metadata": metadata,
|
|
633
|
+
"uses": [], "usageScanComplete": False,
|
|
634
|
+
"clipProperties": {key: properties.get(key) for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Proxy Media Path", "Input Color Space", "Input Gamma", "Data Level", "Alpha mode", "PAR", "Field Dominance", "Clip Name")}}
|
|
635
|
+
data["fingerprint"] = fingerprint({"scope": scope, "mediaPoolItemId": data["mediaPoolItemId"], "path": data["path"],
|
|
636
|
+
"bridgeMetadata": metadata.get("motionharness.bridge"),
|
|
637
|
+
"properties": {key: value for key, value in data["clipProperties"].items() if key != "Clip Name"}})
|
|
638
|
+
return data
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def media_snapshot(project, scope, media, resolve):
|
|
642
|
+
data = media_identity(project, scope, media)
|
|
690
643
|
media_id = media.GetUniqueId()
|
|
691
644
|
uses = []
|
|
692
645
|
complete = True
|
|
@@ -697,23 +650,20 @@ def media_snapshot(project, scope, media, resolve=None):
|
|
|
697
650
|
items = scan_timeline(timeline, resolve, grade_media_id=media_id)
|
|
698
651
|
uses.extend(i for i in items if i["mediaPoolItemId"] == media_id)
|
|
699
652
|
# A nested timeline could reference this media indirectly.
|
|
700
|
-
if any(any(reason in i["unsupported"] for reason in ("nested-or-compound-timeline", "no-plain-media-source", "nested-or-unknown-otio-item", "nested-media-reference", "otio-inspection-unavailable", "otio-item-coverage-mismatch"
|
|
653
|
+
if any(any(reason in i["unsupported"] for reason in ("nested-or-compound-timeline", "no-plain-media-source", "nested-or-unknown-otio-item", "nested-media-reference", "otio-inspection-unavailable", "otio-item-coverage-mismatch")) for i in items):
|
|
701
654
|
complete = False
|
|
702
|
-
settings = project.GetSettings()
|
|
703
|
-
data
|
|
704
|
-
|
|
705
|
-
"clipProperties": {key: properties.get(key) for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Proxy Media Path", "Input Color Space", "Input Gamma", "Data Level", "Alpha mode", "PAR", "Field Dominance", "Clip Name")},
|
|
706
|
-
"playbackPolicy": {key: settings.get(key) for key in ("perfProxyMediaMode", "perfOptimisedMediaOn", "perfRenderCacheMode")}}
|
|
707
|
-
data["fingerprint"] = fingerprint({"properties": properties, **data})
|
|
655
|
+
settings = project.GetSettings()
|
|
656
|
+
data.update({"uses": uses, "usageScanComplete": complete,
|
|
657
|
+
"playbackPolicy": {key: settings.get(key) for key in ("perfProxyMediaMode", "perfOptimisedMediaOn", "perfRenderCacheMode")}})
|
|
708
658
|
return data
|
|
709
659
|
|
|
710
660
|
|
|
711
|
-
def timeline_snapshot(project, scope, timeline, resolve
|
|
661
|
+
def timeline_snapshot(project, scope, timeline, resolve):
|
|
712
662
|
with timeline_context(resolve, timeline):
|
|
713
663
|
return current_timeline_snapshot(project, scope, timeline, resolve)
|
|
714
664
|
|
|
715
665
|
|
|
716
|
-
def current_timeline_snapshot(project, scope, timeline, resolve
|
|
666
|
+
def current_timeline_snapshot(project, scope, timeline, resolve):
|
|
717
667
|
items = scan_timeline(timeline, resolve)
|
|
718
668
|
sources = []
|
|
719
669
|
# Rebuild once per snapshot, not once per source. Do not cache across calls:
|
|
@@ -729,8 +679,8 @@ def current_timeline_snapshot(project, scope, timeline, resolve=None):
|
|
|
729
679
|
props = media.GetClipProperty() or {}
|
|
730
680
|
sources.append({"mediaPoolItemId": media_id, "properties": {key: props.get(key) for key in ("File Path", "FPS", "Frames", "Resolution", "Input Color Space", "Input Gamma", "Data Level", "Proxy Media Path")},
|
|
731
681
|
"bridgeMetadata": (media.GetThirdPartyMetadata() or {}).get("motionharness.bridge")})
|
|
732
|
-
settings = project.GetSettings()
|
|
733
|
-
data = {"scope": scope, "timelineId": timeline.GetUniqueId(), "name": timeline.GetName(),
|
|
682
|
+
settings = project.GetSettings()
|
|
683
|
+
data = {"scope": scope, "timelineId": timeline.GetUniqueId(), "name": timeline.GetName(),
|
|
734
684
|
"fps": fps_value(timeline.GetSetting("timelineFrameRate")), "startFrame": timeline.GetStartFrame(),
|
|
735
685
|
"resolution": {"width": int(timeline.GetSetting("timelineResolutionWidth")), "height": int(timeline.GetSetting("timelineResolutionHeight"))},
|
|
736
686
|
"endFrame": timeline.GetEndFrame(), "items": items, "sources": sources,
|
|
@@ -828,7 +778,6 @@ def seek_timecode(project, timeline, requested, timeout=3.0, resolve=None):
|
|
|
828
778
|
|
|
829
779
|
|
|
830
780
|
def preview_frame(resolve, project, scope, params, before=None):
|
|
831
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该平台与构建的画面捕获尚未验证。")
|
|
832
781
|
require_active_project(resolve, project)
|
|
833
782
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
|
|
834
783
|
timeline = timeline_for(project, params["timelineId"])
|
|
@@ -885,7 +834,7 @@ def playback_policy(project):
|
|
|
885
834
|
|
|
886
835
|
|
|
887
836
|
def render_preset_evidence(resolve, project):
|
|
888
|
-
name = "
|
|
837
|
+
name = "MotionHarness Bridge delivery check " + str(uuid.uuid4())
|
|
889
838
|
require(project.SaveAsNewRenderPreset(name), "CAPABILITY_UNAVAILABLE", "无法核对导出预设的缓存策略。")
|
|
890
839
|
try:
|
|
891
840
|
with tempfile.TemporaryDirectory(prefix="mh-bridge-preset-") as directory:
|
|
@@ -899,7 +848,6 @@ def render_preset_evidence(resolve, project):
|
|
|
899
848
|
|
|
900
849
|
|
|
901
850
|
def create_render(resolve, project, scope, params):
|
|
902
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务导出。")
|
|
903
851
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 已有输出任务在运行。")
|
|
904
852
|
timeline = timeline_for(project, params["timelineId"])
|
|
905
853
|
expected = params["expectedTimelineFingerprint"]
|
|
@@ -910,7 +858,7 @@ def create_render(resolve, project, scope, params):
|
|
|
910
858
|
previous = project.GetCurrentTimeline()
|
|
911
859
|
before_jobs = project.GetRenderJobList() or []
|
|
912
860
|
require(not any(os.path.realpath(job.get("TargetDir", "")) == os.path.realpath(os.path.dirname(path)) and job.get("OutputFilename") == os.path.basename(path) for job in before_jobs), "TARGET_OCCUPIED", "输出位置已被其他队列项占用。")
|
|
913
|
-
backup = "
|
|
861
|
+
backup = "MotionHarness Bridge restore " + str(uuid.uuid4())
|
|
914
862
|
require(project.SaveAsNewRenderPreset(backup), "CAPABILITY_UNAVAILABLE", "无法保存原有导出配置,未修改设置。")
|
|
915
863
|
job_id = None
|
|
916
864
|
try:
|
|
@@ -958,7 +906,6 @@ def create_render(resolve, project, scope, params):
|
|
|
958
906
|
|
|
959
907
|
|
|
960
908
|
def start_render(resolve, project, scope, params):
|
|
961
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务导出。")
|
|
962
909
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在执行其他渲染;未启动队列。")
|
|
963
910
|
job = render_job(project, params["jobId"])
|
|
964
911
|
require(params.get("expectedJobFingerprint") and fingerprint(job) == params["expectedJobFingerprint"], "VERSION_CONFLICT", "队列任务的输出设置已改变或没有核对记录,未启动。")
|
|
@@ -985,7 +932,6 @@ def start_render(resolve, project, scope, params):
|
|
|
985
932
|
|
|
986
933
|
|
|
987
934
|
def finish_render(resolve, project, params):
|
|
988
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证输出恢复。")
|
|
989
935
|
render_job(project, params["jobId"])
|
|
990
936
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "仍有渲染运行,保留原始画面模式直至可核对的终态。")
|
|
991
937
|
expected, actual = params["playbackPolicy"], playback_policy(project)
|
|
@@ -997,7 +943,7 @@ def finish_render(resolve, project, params):
|
|
|
997
943
|
|
|
998
944
|
|
|
999
945
|
def export_references(resolve, project, scope, params):
|
|
1000
|
-
require(
|
|
946
|
+
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "输出后画面核对需要已完成渲染的目标工程。")
|
|
1001
947
|
timeline = timeline_for(project, params["timelineId"])
|
|
1002
948
|
before = timeline_snapshot(project, scope, timeline, resolve)
|
|
1003
949
|
require(before["fingerprint"] == params["expectedTimelineFingerprint"], "VERSION_CONFLICT", "输出后的时间线已改变,不能生成原回执的参考画面。")
|
|
@@ -1024,7 +970,6 @@ def export_references(resolve, project, scope, params):
|
|
|
1024
970
|
|
|
1025
971
|
|
|
1026
972
|
def preview_original(resolve, project, scope, params):
|
|
1027
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证原始媒体合成检查。")
|
|
1028
973
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "不能在导出期间改变合成画面检查策略。")
|
|
1029
974
|
timeline = timeline_for(project, params["timelineId"])
|
|
1030
975
|
before = timeline_snapshot(project, scope, timeline, resolve)
|
|
@@ -1046,8 +991,6 @@ def preview_original(resolve, project, scope, params):
|
|
|
1046
991
|
|
|
1047
992
|
|
|
1048
993
|
def selection_snapshot(resolve, plain_media_only=False):
|
|
1049
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证时间线选择读取。")
|
|
1050
|
-
|
|
1051
994
|
def plain_item(item):
|
|
1052
995
|
media = item.GetMediaPoolItem()
|
|
1053
996
|
require(media is not None and media is not False, "CAPABILITY_UNAVAILABLE", "请选择有直接视频源的普通片段;生成器入口暂不支持此操作。")
|
|
@@ -1131,8 +1074,7 @@ def entry_target(resolve, project, scope, params):
|
|
|
1131
1074
|
source = ofx_source_target(resolve, {"scope": scope, "mediaPoolItemId": selection.get("mediaPoolItemId"), "path": selection["path"]})
|
|
1132
1075
|
if not selection.get("timelineId") and not selection.get("itemId"):
|
|
1133
1076
|
require(source is not None, "INVALID_REQUEST", "素材级入口需要源视频路径。")
|
|
1134
|
-
|
|
1135
|
-
return {**source, "projectName": project.GetName(), "name": media.GetName()}
|
|
1077
|
+
return {**source, "name": source["clipProperties"].get("Clip Name") or ""}
|
|
1136
1078
|
timeline = timeline_for(project, selection.get("timelineId"))
|
|
1137
1079
|
found = [item for track in range(1, timeline.GetTrackCount("video") + 1)
|
|
1138
1080
|
for item in timeline.GetItemListInTrack("video", track) or []
|
|
@@ -1162,7 +1104,7 @@ def inspector_operation(resolve, params):
|
|
|
1162
1104
|
# A composition imported through the API is not an Edit-page Fusion effect.
|
|
1163
1105
|
# Keep the graph experiment separate from the product's Inspector entry.
|
|
1164
1106
|
require(params.get("remove") or params.get("compositionOnly") is True, "CAPABILITY_UNAVAILABLE",
|
|
1165
|
-
"ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用
|
|
1107
|
+
"ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用 MotionHarness Bridge 模板。")
|
|
1166
1108
|
identity = params["identity"]
|
|
1167
1109
|
require(identity.get("schemaVersion") == 1, "INVALID_REQUEST", "检查器入口身份版本无效。")
|
|
1168
1110
|
selected = selection_snapshot(resolve)
|
|
@@ -1197,7 +1139,7 @@ def inspector_operation(resolve, params):
|
|
|
1197
1139
|
before = timeline_snapshot(project, scope, timeline, resolve)
|
|
1198
1140
|
targets = [value for value in before["items"] if value["id"] == item.GetUniqueId()]
|
|
1199
1141
|
require(len(targets) == 1 and not targets[0]["unsupported"] and not targets[0].get("trackLocked"),
|
|
1200
|
-
"UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通
|
|
1142
|
+
"UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通 MotionHarness 片段。")
|
|
1201
1143
|
with open(params["compositionPath"], "rb") as stream:
|
|
1202
1144
|
data = stream.read()
|
|
1203
1145
|
require(hashlib.sha256(data).hexdigest() == params["compositionSha256"] and data.startswith(b"-- MH Resolve Bridge inspector v1\n"),
|
|
@@ -1238,6 +1180,8 @@ def inspector_operation(resolve, params):
|
|
|
1238
1180
|
|
|
1239
1181
|
|
|
1240
1182
|
def dispatch(resolve, operation, params):
|
|
1183
|
+
# Resolve version and process architecture are diagnostic information only.
|
|
1184
|
+
# Operations use the host APIs directly and report actual failures.
|
|
1241
1185
|
if operation == "ofx_source_target":
|
|
1242
1186
|
return ofx_source_target(resolve, params)
|
|
1243
1187
|
if operation == "selection":
|
|
@@ -1272,22 +1216,19 @@ def dispatch(resolve, operation, params):
|
|
|
1272
1216
|
"scope": scope_for(manager, project) if project else None,
|
|
1273
1217
|
"timelines": [{"id": t.GetUniqueId(), "name": t.GetName()} for t in timelines(project)] if project else [],
|
|
1274
1218
|
"adapter": "documented-python-scripting", "officialMcp": "not-tested-by-scripting",
|
|
1275
|
-
"replacement": "stable-playback-file-with-
|
|
1276
|
-
"frameConvention": "half-open"
|
|
1277
|
-
"profile": {"platform": sys.platform, "architecture": platform.machine()
|
|
1219
|
+
"replacement": "stable-playback-file-with-media-identity",
|
|
1220
|
+
"frameConvention": "half-open",
|
|
1221
|
+
"profile": {"platform": sys.platform, "architecture": platform.machine()},
|
|
1278
1222
|
"timelineReadback": "top-level-video; native speed/properties/fades and conservative OTIO structure check"}
|
|
1279
1223
|
proof = params if operation in ("render_status", "cancel_render") else None
|
|
1280
1224
|
project, scope = project_for(resolve, params.get("scope"), proof)
|
|
1281
1225
|
if operation == "entry_target":
|
|
1282
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证所选片段管理。")
|
|
1283
1226
|
return entry_target(resolve, project, scope, params)
|
|
1284
1227
|
if operation in ("legacy_preview", "legacy_convert", "legacy_check"):
|
|
1285
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证旧生成器结构转换。")
|
|
1286
1228
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
|
|
1287
1229
|
action = {"legacy_preview": legacy.preview, "legacy_convert": legacy.convert, "legacy_check": legacy.check}[operation]
|
|
1288
1230
|
return action(resolve, project, scope, params, globals())
|
|
1289
1231
|
if operation == "duplicate_instance":
|
|
1290
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证单实例独立副本。")
|
|
1291
1232
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
|
|
1292
1233
|
return instances.duplicate_instance(resolve, project, scope, params, globals())
|
|
1293
1234
|
if operation == "timeline":
|
|
@@ -1332,8 +1273,6 @@ def dispatch(resolve, operation, params):
|
|
|
1332
1273
|
require(len(found) <= 1, "RECOVERY_CONFLICT", "多个媒体对象拥有同一绑定身份,未自动选择。")
|
|
1333
1274
|
return media_snapshot(project, scope, found[0], resolve) if found else None
|
|
1334
1275
|
if operation in ("render_status", "cancel_render"):
|
|
1335
|
-
if operation == "cancel_render":
|
|
1336
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务取消。")
|
|
1337
1276
|
render_job(project, params["jobId"])
|
|
1338
1277
|
status = project.GetRenderJobStatus(params["jobId"])
|
|
1339
1278
|
if operation == "cancel_render" and status.get("JobStatus") == "Rendering":
|
|
@@ -1344,25 +1283,18 @@ def dispatch(resolve, operation, params):
|
|
|
1344
1283
|
return status
|
|
1345
1284
|
if operation == "media_identity":
|
|
1346
1285
|
media = media_for(project, params["mediaPoolItemId"])
|
|
1347
|
-
|
|
1348
|
-
result = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media.GetUniqueId(),
|
|
1349
|
-
"path": properties.get("File Path", ""), "metadata": media.GetThirdPartyMetadata() or {},
|
|
1350
|
-
"clipProperties": {key: properties.get(key) for key in ("Proxy Media Path", "Clip Name")},
|
|
1351
|
-
"validatedHost": validated_host(resolve)}
|
|
1286
|
+
result = media_identity(project, scope, media)
|
|
1352
1287
|
require_active_project(resolve, project)
|
|
1353
1288
|
return result
|
|
1354
1289
|
if operation in ("media", "bind", "replace"):
|
|
1355
1290
|
media = media_for(project, params["mediaPoolItemId"])
|
|
1356
|
-
before = media_snapshot(project, scope, media, resolve)
|
|
1357
1291
|
if operation == "media":
|
|
1358
|
-
return
|
|
1292
|
+
return media_snapshot(project, scope, media, resolve)
|
|
1293
|
+
before = media_identity(project, scope, media)
|
|
1359
1294
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出,桥接没有修改媒体。")
|
|
1360
|
-
require(
|
|
1361
|
-
require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 媒体或使用位置已变化。")
|
|
1295
|
+
require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 原素材的身份、关联或媒体规格已变化。")
|
|
1362
1296
|
if operation == "replace":
|
|
1363
1297
|
repair = params.get("offlineRepair")
|
|
1364
|
-
allowed = {"offline-media-reference"} if repair else set()
|
|
1365
|
-
require(before["usageScanComplete"] and not any(set(i["unsupported"]) - allowed for i in before["uses"]), "UNSUPPORTED_TIMELINE_ITEM", "已剪片段包含不能完整核对的处理,未重连新版本。")
|
|
1366
1298
|
path = os.path.realpath(params["filePath"])
|
|
1367
1299
|
publication = params.get("publication")
|
|
1368
1300
|
require(publication or os.path.isfile(path), "ARTIFACT_MISSING", "新产物不存在。")
|
|
@@ -1372,22 +1304,21 @@ def dispatch(resolve, operation, params):
|
|
|
1372
1304
|
"RECOVERY_CONFLICT", "离线搬迁修复只允许原绑定、原产物身份和已记录的旧路径。")
|
|
1373
1305
|
require(file_digest(publication["sourcePath"] if publication else path) == repair["sha256"],
|
|
1374
1306
|
"ARTIFACT_CHANGED", "搬迁媒体与记录摘要不同。")
|
|
1375
|
-
policy = before["playbackPolicy"]
|
|
1376
|
-
require(str(policy.get("perfProxyMediaMode")) in ("0", "1", "2") and str(policy.get("perfOptimisedMediaOn")) in ("0", "1")
|
|
1377
|
-
and policy.get("perfRenderCacheMode") in ("none", "smart", "user"),
|
|
1378
|
-
"PLAYBACK_POLICY_UNVERIFIED", "原生代理、优化媒体或缓存设置无法完整读回,未修改工程设置。", policy=policy)
|
|
1379
1307
|
require(not before["clipProperties"].get("Proxy Media Path"), "PROXY_UNSUPPORTED", "当前替换路径尚未验证已关联代理。")
|
|
1380
1308
|
require(os.path.basename(path) == os.path.basename(before["path"]), "BASENAME_CONFLICT", "安全重连要求版本目录中的文件名保持一致;未修改现有素材。")
|
|
1381
1309
|
if publication:
|
|
1382
1310
|
require_active_project(resolve, project)
|
|
1311
|
+
require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
|
|
1312
|
+
"VERSION_CONFLICT", "发布前原素材的身份或关联已改变。")
|
|
1383
1313
|
# Require the still-current metadata; no selection or effect-stored ID is consulted.
|
|
1384
1314
|
old_link = json.loads(before["metadata"].get("motionharness.bridge") or "{}")
|
|
1385
1315
|
new_link = json.loads(params["metadata"].get("motionharness.bridge") or "{}")
|
|
1386
1316
|
require(not old_link or all(old_link.get(k) == new_link.get(k) for k in ("schemaVersion", "bindingId", "sourceId")),
|
|
1387
1317
|
"BINDING_CONFLICT", "素材的关联身份已经改变。")
|
|
1388
|
-
if old_link and os.path.realpath(before["path"]) == path:
|
|
1389
|
-
ofx_source_target(resolve, {"scope": scope, "mediaPoolItemId": media.GetUniqueId(), "path": path})
|
|
1390
1318
|
publish_playback(path, publication, before["path"])
|
|
1319
|
+
require_active_project(resolve, project)
|
|
1320
|
+
require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
|
|
1321
|
+
"APPLY_OUTCOME_UNKNOWN" if publication else "VERSION_CONFLICT", "重连前原素材的身份或关联已改变。")
|
|
1391
1322
|
require(project.GetMediaPool().RelinkClips([media], os.path.dirname(path)), "APPLY_OUTCOME_UNKNOWN", "Resolve 未确认重连版本目录,需读回核对后处理。")
|
|
1392
1323
|
# Clip interpretation belongs to the existing cut. Relinking may
|
|
1393
1324
|
# re-read container defaults; preserve the user's native settings.
|
|
@@ -1400,27 +1331,15 @@ def dispatch(resolve, operation, params):
|
|
|
1400
1331
|
require(all(readback.get(key) == before["clipProperties"].get(key) for key in ("Alpha mode", "Input Color Space", "Input Gamma", "Data Level", "PAR", "Field Dominance", "Clip Name") if key in before["clipProperties"]),
|
|
1401
1332
|
"APPLY_OUTCOME_UNKNOWN", "媒体解释读回与替换前不同,需核对原生状态。")
|
|
1402
1333
|
set_metadata(media, {**before["metadata"], **params["metadata"]})
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
if not use.get('intrinsicEntry'):
|
|
1406
|
-
continue
|
|
1407
|
-
target = timeline_for(project, use['timelineId'])
|
|
1408
|
-
with timeline_context(resolve, target):
|
|
1409
|
-
matches = [i for i in target.GetItemListInTrack('video', use['track']) or [] if i.GetUniqueId() == use['id']]
|
|
1410
|
-
require(len(matches) == 1 and intrinsic.read_entry(matches[0], {media.GetUniqueId(): media}),
|
|
1411
|
-
'APPLY_OUTCOME_UNKNOWN', '素材已替换,但原生入口或画面引用在更新期间发生变化。')
|
|
1412
|
-
intrinsic.refresh_media(matches[0], media)
|
|
1413
|
-
after = media_snapshot(project, scope, media, resolve)
|
|
1334
|
+
after = media_identity(project, scope, media)
|
|
1335
|
+
require_active_project(resolve, project)
|
|
1414
1336
|
if operation == "replace":
|
|
1415
|
-
|
|
1416
|
-
return [{**item, "unsupported": [reason for reason in item["unsupported"] if not repair or reason != "offline-media-reference"]} for item in uses]
|
|
1417
|
-
require(os.path.realpath(after["path"]) == path and edits(before["uses"]) == edits(after["uses"]), "APPLY_OUTCOME_UNKNOWN", "替换后的路径或剪辑使用位置读回不一致。")
|
|
1337
|
+
require(os.path.realpath(after["path"]) == path, "APPLY_OUTCOME_UNKNOWN", "替换后的原素材路径读回不一致。")
|
|
1418
1338
|
for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Input Color Space", "Input Gamma", "Data Level"):
|
|
1419
1339
|
require(before["clipProperties"].get(key) == after["clipProperties"].get(key), "APPLY_OUTCOME_UNKNOWN", "替换后媒体规格或颜色解释改变。", property=key)
|
|
1420
1340
|
return after
|
|
1421
1341
|
if operation == "insert":
|
|
1422
1342
|
require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
|
|
1423
|
-
require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证插入的帧边界。")
|
|
1424
1343
|
timeline = timeline_for(project, params["timelineId"])
|
|
1425
1344
|
before = timeline_snapshot(project, scope, timeline, resolve)
|
|
1426
1345
|
require(before["fingerprint"] == params["expectedTimelineFingerprint"], "VERSION_CONFLICT", "时间线在插入前发生变化。")
|
|
@@ -1476,7 +1395,7 @@ def dispatch(resolve, operation, params):
|
|
|
1476
1395
|
"mediaType": 1, "trackIndex": track, "recordFrame": record}]) or []
|
|
1477
1396
|
require(len(created) == 1, "APPLY_OUTCOME_UNKNOWN", "Resolve 未返回唯一插入对象,需核对后恢复。")
|
|
1478
1397
|
save_insert_progress(params, before, "appended", media, created[0])
|
|
1479
|
-
item = item_snapshot(created[0], timeline.GetUniqueId(), track,
|
|
1398
|
+
item = item_snapshot(created[0], timeline.GetUniqueId(), track, {media.GetUniqueId(): media}, before["fps"])
|
|
1480
1399
|
sampling_matches = item["sourceIn"] == start and item["sourceOut"] == end
|
|
1481
1400
|
if spec["fps"] != before["fps"]:
|
|
1482
1401
|
expected_start = start * spec["fps"]["den"] / spec["fps"]["num"]
|