01 / BEFORE THE CODE
Set the conditions before you run.
Environment and reference
Use Python 3.9+ with a running Resolve Studio instance. Load the Blackmagic module from your own installation. S05 additionally checks the published 21.1 DaVinciResolveScript.pyi mirror; inspect the file shipped with your exact build rather than relying only on an older README.txt.
An exact target, not the first clip
Obtain project and timeline IDs from the earlier guides and the item ID from the S03 timeline map. Names help visual inspection but never select the mutation target. Track indices are 1-based and may change after structural edits.
Keep the experiment bounded
Use a disposable test copy and pause concurrent editing. The default is a plan; a write requires --apply, --test-copy and the reviewed --expect-plan token. Snapshots allow up to 128 tracks per type and 5,000 total items: tutorial bounds, not Resolve limits.
Current Studio edition guidance and the installed scripting documentation take precedence over older console or Free-edition tutorials.[1][4][3]
02 / API CONTRACT
Which object. Which method. Which result.
GetTrackCount(trackType)- Input
- video / audio / subtitle.
- Returns
- Track count.
- Check
- None is not zero.
GetIsTrackLocked(trackType, index)- Input
- An existing 1-based track.
- Returns
- Boolean lock state.
- Check
- A missing value is a read failure.
GetIsTrackEnabled(trackType, index)- Input
- The same track.
- Returns
- Boolean enabled state.
- Check
- Read only; unlocking must not change it.
SetTrackLock(trackType, index, False)- Input
- One explicitly selected track.
- Returns
- Boolean response.
- Check
- Check the return and read the structure again.
Resolve method contracts are documented by Blackmagic; Python and author helpers are labelled separately. The defensive checks are our example design. A non-empty object is not proof of full API compatibility.[3]
03 / UNDERSTAND
Lock and enable are independent
A track lock limits editing; enabled state controls whether the track is enabled. The old example changed both for every video and audio track. That expanded the operation beyond editing access and could overwrite intentionally different track states.
The new example changes only one track’s locked flag. A disabled track stays disabled after unlocking. Enabling belongs in a separate task with its own review and verification, not in an automatic preparation step for deletion.[14]
04 / UNDERSTAND
Why V2 alone is not an identity
An index is scoped to a track type and timeline: video/2 and audio/2 are distinct targets. Adding or removing tracks can shift numbers. A track name helps a person inspect the target but duplicate names are not an identity guarantee.
The plan binds project and timeline IDs, track composition, names, flags and item occurrences. An observed structural change changes the fingerprint. This reduces stale-index errors without inventing a persistent track ID or locking out concurrent editing.[14]
05 / UNDERSTAND
Already unlocked means no mutation
If GetIsTrackLocked returns False, the desired state is already present. The plan records write_required: false. Even an approved apply skips the setter and returns already_satisfied rather than repeating an unnecessary mutation.
A missing response is different: None does not mean unlocked. A failed read stops preflight. A helper returning without an exception is insufficient evidence, especially when a generic safe_call wrapper swallowed the underlying failure.[14]
06 / UNDERSTAND
What the readback compares
The expected state is a copy of the observation with a single locked flag set to False. Types, indices, names, enabled flags and recorded item intervals remain the same. New reads follow the single SetTrackLock call.
A True response plus matching fields produces verified_observed_scope. Grades, plug-ins, audio routing and source contents are outside this snapshot. Also inspect Resolve to confirm the intended lock was cleared, rather than merely noticing that some track became editable.[14]
PYTHON / COMPLETE EXAMPLE
Read the code. Then run it deliberately.
Download unlock_track.py, resolve_api_edit.py and resolve_api_connection.py into one folder. The bundle contains seven examples, two helpers, the standalone audit_edit_contract.py and RU/EN instructions. The local document checker needs only Python and a .pyi file; it never connects to Resolve.
Download all examples + instructions (.zip) ↓"""Unlock ONE identified track. Never enables it. See resolve_api_edit.py for the complete implementation."""
import argparse
from resolve_api_connection import connect
from resolve_api_edit import AUDIO_FORMATS, cli_target, cli_write, print_result, run_action
def main():
p = argparse.ArgumentParser(description=__doc__)
cli_target(p)
cli_write(p)
p.add_argument("--track-type", choices=("video", "audio", "subtitle"), required=True)
p.add_argument("--index", type=int, required=True)
a = p.parse_args()
return print_result(lambda: run_action(
connect(), a.project_id, a.timeline_id, "unlock",
apply=a.apply, expect_plan=a.expect_plan, test_copy=a.test_copy,
track_type=a.track_type, index=a.index))
if __name__ == "__main__":
raise SystemExit(main())
Edit helper — exact targets, plan checks and one mutation
Author tutorial code, not the SDK. Only unlock, one non-ripple deletion and one appended track can mutate Resolve. Split and trim stay read-only. Matching structural reads do not certify effects, routing or the entire project; no retries or automatic rollback are performed.
"""Bounded Resolve timeline tutorials, S05. Author code, NOT the Blackmagic SDK.
Only unlock, delete-one (non-ripple) and append-one-track can write to Resolve.
Plan fingerprints bind observed fields, not every project property. Reads are
not atomic, there is no transaction or native timeout, and no automatic retry,
rollback, save, UI automation or file deletion is performed here.
"""
from __future__ import annotations
import copy
import hashlib
import json
import math
import re
from fractions import Fraction
TRACK_TYPES = ("video", "audio", "subtitle")
AUDIO_FORMATS = ("mono", "stereo", "lrc", "lcr", "lrcs", "lcrs", "quad",
"5.0", "5.0film", "5.1", "5.1film", "7.0", "7.0film", "7.1", "7.1film") + tuple(
"adaptive%d" % n for n in range(1, 37))
MAX_TRACKS = 128 # Tutorial bounds, NOT Resolve product limits.
MAX_ITEMS = 5000
class PreflightError(RuntimeError):
"""No write is issued by this helper when preflight fails."""
def invoke(obj, method, *args):
fn = getattr(obj, method, None)
if not callable(fn):
raise PreflightError("Required method unavailable: " + method)
try:
return fn(*args)
except Exception as exc:
raise PreflightError("Read failed: " + method + ": " + type(exc).__name__) from exc
def ident(value, label):
if not isinstance(value, str) or not value.strip() or len(value) > 1024:
raise PreflightError("Missing or invalid " + label)
return value
def number(value, label):
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise PreflightError(label + " must be a finite API number (not None/bool/text)")
if isinstance(value, float) and not math.isfinite(value):
raise PreflightError(label + " is not finite")
return Fraction(str(value))
def count(value, label, maximum=MAX_TRACKS):
n = number(value, label)
if n.denominator != 1 or not 0 <= n <= maximum:
raise PreflightError(label + " must be an integer within this tutorial's bound")
return int(n)
def boolean(value, label):
if type(value) is not bool:
raise PreflightError(label + " must be an explicit boolean")
return value
def text(value, label):
if not isinstance(value, str):
raise PreflightError(label + " must be a string")
return value
def fingerprint(value):
raw = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def get_target(resolve, project_id=None, timeline_id=None):
manager = invoke(resolve, "GetProjectManager")
if not manager:
raise PreflightError("Project manager unavailable")
project = invoke(manager, "GetCurrentProject")
if not project:
raise PreflightError("No open project")
timeline = invoke(project, "GetCurrentTimeline")
if not timeline:
raise PreflightError("No current timeline")
pid = ident(invoke(project, "GetUniqueId"), "project ID")
tid = ident(invoke(timeline, "GetUniqueId"), "timeline ID")
if project_id is not None and pid != project_id:
raise PreflightError("Active project does not match --project-id")
if timeline_id is not None and tid != timeline_id:
raise PreflightError("Active timeline does not match --timeline-id")
return project, timeline, pid, tid
def capture(resolve, project_id=None, timeline_id=None):
"""All top-level tracks/items, no recursive flattening and no source-file I/O."""
project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
state = {"project_id": pid, "timeline_id": tid,
"project_name": text(invoke(project, "GetName"), "project name"),
"timeline_name": text(invoke(timeline, "GetName"), "timeline name"),
"base_frame": str(number(invoke(timeline, "GetStartFrame"), "base frame")),
"start_timecode": text(invoke(timeline, "GetStartTimecode"), "start timecode"),
"tracks": []}
objects = {}
for kind in TRACK_TYPES:
n = count(invoke(timeline, "GetTrackCount", kind), "track count")
for index in range(1, n + 1):
track = {"type": kind, "index": index,
"name": text(invoke(timeline, "GetTrackName", kind, index), "track name"),
"locked": boolean(invoke(timeline, "GetIsTrackLocked", kind, index), "track lock"),
"enabled": boolean(invoke(timeline, "GetIsTrackEnabled", kind, index), "track enable"),
"items": []}
if kind == "audio":
track["format"] = ident(invoke(timeline, "GetTrackSubType", kind, index), "audio format")
items = invoke(timeline, "GetItemListInTrack", kind, index)
if not isinstance(items, list):
raise PreflightError("Item list unavailable; None is not an empty track")
if len(items) + len(objects) > MAX_ITEMS:
raise PreflightError("Snapshot exceeds tutorial item bound")
for item in items:
uid = ident(invoke(item, "GetUniqueId"), "timeline item ID")
if uid in objects:
raise PreflightError("Ambiguous duplicate timeline item ID")
start = number(invoke(item, "GetStart", True), "item start")
end = number(invoke(item, "GetEnd", True), "item end")
duration = number(invoke(item, "GetDuration", True), "item duration")
if end < start or duration < 0:
raise PreflightError("Inconsistent item geometry")
record = {"id": uid, "name": text(invoke(item, "GetName"), "item name"),
"start": str(start), "end_raw": str(end), "duration": str(duration),
"enabled": boolean(invoke(item, "GetClipEnabled"), "clip enabled")}
track["items"].append(record)
objects[uid] = item
track["items"].sort(key=lambda x: x["id"])
state["tracks"].append(track)
get_target(resolve, pid, tid) # Do not certify data after an observed target switch.
return state, timeline, objects
def stable_snapshot(resolve, project_id=None, timeline_id=None):
first, _, _ = capture(resolve, project_id, timeline_id)
second, timeline, objects = capture(resolve, first["project_id"], first["timeline_id"])
if first != second:
raise PreflightError("Observed structure changed during reads; stop and inspect")
return second, timeline, objects
def locate(state, item_id):
matches = [(track, item) for track in state["tracks"] for item in track["items"] if item["id"] == item_id]
if len(matches) != 1:
raise PreflightError("Exactly one item with --item-id is required; no name/range fallback")
return matches[0]
def linked_ids(item):
linked = invoke(item, "GetLinkedItems")
if not isinstance(linked, list):
raise PreflightError("Linked-item list unknown; no permission to delete")
own = ident(invoke(item, "GetUniqueId"), "item ID")
return sorted(set(ident(invoke(x, "GetUniqueId"), "linked item ID") for x in linked) - {own})
def checked_index(value):
if type(value) is not int or value < 1 or value > MAX_TRACKS:
raise PreflightError("Track index must be 1-based and within the tutorial bound")
return value
def build_plan(state, objects, operation, **options):
"""Pure policy except explicit read-only link/type checks for deletion."""
expected = copy.deepcopy(state)
plan = {"schema": "videomontazher.resolve.edit-plan.v1", "operation": operation,
"before": state, "options": options, "write_required": True,
"scope": "Top-level structural fields only; not an atomic project snapshot"}
if operation == "unlock":
kind, index = options.get("track_type"), checked_index(options.get("index"))
if kind not in TRACK_TYPES:
raise PreflightError("Unknown track type")
matches = [t for t in expected["tracks"] if t["type"] == kind and t["index"] == index]
if not matches:
raise PreflightError("Target track does not exist")
plan["write_required"] = matches[0]["locked"]
matches[0]["locked"] = False
plan["call"] = "Timeline.SetTrackLock(type, index, False)"
elif operation == "delete":
item_id = ident(options.get("item_id"), "target item ID")
track, record = locate(state, item_id)
if track["locked"]:
raise PreflightError("Target track is locked; unlocking is a separate reviewed task")
kind = invoke(objects[item_id], "GetType")
if kind not in ("video", "audio"):
raise PreflightError("This example deletes only an identified video/audio item")
links = linked_ids(objects[item_id])
if links:
raise PreflightError("Linked items exist; this single-item example will not unlink or expand scope")
plan["target_type"] = kind
plan["linked_ids"] = links
for t in expected["tracks"]:
t["items"] = [i for i in t["items"] if i["id"] != item_id]
plan["call"] = "Timeline.DeleteClips([exact_item], False)"
elif operation in ("ensure_audio", "ensure_video"):
kind = "audio" if operation == "ensure_audio" else "video"
index = checked_index(options.get("index"))
tracks = [t for t in state["tracks"] if t["type"] == kind]
fmt = options.get("audio_format")
if kind == "audio" and fmt not in AUDIO_FORMATS:
raise PreflightError("Explicit supported audio format required")
if index <= len(tracks):
if kind == "audio" and tracks[index - 1]["format"] != fmt:
raise PreflightError("Existing track format differs; not converting or replacing it")
plan["write_required"] = False
elif index != len(tracks) + 1:
raise PreflightError("Only the next missing track can be added in one plan; no fill loop")
plan["track_type"] = kind
plan["call"] = "Timeline.AddTrack('audio', explicit_format)" if kind == "audio" else "Timeline.AddTrack('video')"
expected = None # New track name/initial flags are read, not predicted.
else:
raise PreflightError("Unknown operation")
plan["expected_structure"] = expected
plan["token"] = fingerprint(plan)
return plan
def verify_added(before, after, kind, index, fmt):
old = before["tracks"]
new = [t for t in after["tracks"] if t["type"] == kind and t["index"] == index]
if len(new) != 1 or new[0]["items"] or (kind == "audio" and new[0]["format"] != fmt):
return False
stripped = copy.deepcopy(after)
stripped["tracks"] = [t for t in after["tracks"] if not (t["type"] == kind and t["index"] == index)]
return len(after["tracks"]) == len(old) + 1 and stripped == before
def run_action(resolve, project_id, timeline_id, operation, apply=False, expect_plan=None, test_copy=False, **options):
ident(project_id, "explicit --project-id")
ident(timeline_id, "explicit --timeline-id")
before, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
plan = build_plan(before, objects, operation, **options)
if not apply:
return {"status": "plan_only", "plan": plan}
if not test_copy or not isinstance(expect_plan, str) or expect_plan != plan["token"]:
raise PreflightError("Apply requires --test-copy and the exact reviewed --expect-plan token")
# Re-read immediately before dispatch, including links/type for deletion.
fresh, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
fresh_plan = build_plan(fresh, objects, operation, **options)
if fresh_plan["token"] != expect_plan:
raise PreflightError("Plan became stale before dispatch; no write issued")
if not fresh_plan["write_required"]:
return {"status": "already_satisfied", "write_issued": False, "plan": fresh_plan}
if operation == "unlock":
method, args = "SetTrackLock", (options["track_type"], options["index"], False)
elif operation == "delete":
method, args = "DeleteClips", ([objects[options["item_id"]]], False)
elif operation == "ensure_audio":
method, args = "AddTrack", ("audio", options["audio_format"])
else:
method, args = "AddTrack", ("video",)
fn = getattr(timeline, method, None)
if not callable(fn):
raise PreflightError("Documented write method unavailable; no alternative is attempted")
# From this point even False / an exception may accompany a side effect.
returned, call_error = None, None
try:
returned = fn(*args) # ONE native write. Never retry automatically.
except Exception as exc:
call_error = type(exc).__name__
result = {"status": "outcome_unconfirmed", "write_issued": True,
"method": method, "returned_true": returned is True,
"call_error": call_error, "plan_token": expect_plan,
"structural_match": False,
"next": "Read actual state and inspect Resolve. Do not blindly retry or roll back."}
try:
after, _, _ = stable_snapshot(resolve, project_id, timeline_id)
result["after"] = after
if operation.startswith("ensure_"):
matched = verify_added(before, after, fresh_plan["track_type"], options["index"], options.get("audio_format"))
else:
matched = after == fresh_plan["expected_structure"]
result["structural_match"] = matched
if matched and returned is True and call_error is None:
result["status"] = "verified_observed_scope"
result["next"] = "Confirm the edit in Resolve. Unobserved properties/effects are not certified."
except Exception as exc:
result["readback_error"] = type(exc).__name__ + ": " + str(exc)
return result
def input_frame(value, label):
if isinstance(value, bool) or not isinstance(value, (str, int)):
raise PreflightError(label + " must be integer frame text")
value = str(value)
if not value or len(value) > 18 or value.strip() != value or re.fullmatch(r"-?[0-9]+", value) is None:
raise PreflightError(label + " must be integer frame text")
n = int(value)
if abs(n) > 10**12:
raise PreflightError(label + " exceeds tutorial bound")
return n
def boundary_math(start, end, mode, at=None, new_start=None, new_end=None):
"""Half-open whole-frame mathematical plan, no source-range conversion."""
start, end = input_frame(start, "start"), input_frame(end, "end")
if end <= start:
raise PreflightError("A positive [start, end) interval is required")
if mode == "split":
at = input_frame(at, "cut")
if not start < at < end:
raise PreflightError("Cut must be strictly inside the item, not on an edge")
return {"mode": mode, "before": [start, end], "segments": [[start, at], [at, end]],
"durations": [at - start, end - at], "duration_preserved": True}
if mode == "trim":
new_start, new_end = input_frame(new_start, "new start"), input_frame(new_end, "new end")
if not start <= new_start < new_end <= end:
raise PreflightError("Trim must keep a nonempty subinterval; extending handles is outside this plan")
return {"mode": mode, "before": [start, end], "keep": [new_start, new_end],
"remove_head": new_start - start, "remove_tail": end - new_end,
"duration": new_end - new_start, "unchanged": [new_start, new_end] == [start, end]}
raise PreflightError("Only split/trim mathematical plans are supported")
def inspect_boundary(resolve, project_id, timeline_id, item_id, mode, **values):
state, _, objects = stable_snapshot(resolve, project_id, timeline_id)
track, item = locate(state, ident(item_id, "item ID"))
start, end, duration = (Fraction(item[k]) for k in ("start", "end_raw", "duration"))
if any(x.denominator != 1 for x in (start, end, duration)) or end - start != duration:
raise PreflightError("This whole-frame model requires end-start == duration. No rounding or boundary guessing.")
geometry = boundary_math(int(start), int(end), mode, **values)
report = {"schema": "videomontazher.resolve.boundary-plan.v1", "status": "read_only_plan_no_executor",
"project_id": state["project_id"], "timeline_id": state["timeline_id"],
"track": {"type": track["type"], "index": track["index"], "locked": track["locked"]},
"item": item, "geometry": geometry, "snapshot_token": fingerprint(state),
"source_mapping": "not_calculated", "write_issued": False,
"next": "Manual operation on a copy, or a separately qualified reconstruction adapter. No implicit UI keys."}
# Optional context is reported explicitly, never silently interpreted as zero.
for name in ("GetLeftOffset", "GetRightOffset"):
try:
report[name] = {"status": "read", "frames_raw": str(number(invoke(objects[item_id], name, True), name))}
except Exception as exc:
report[name] = {"status": "unavailable", "error": type(exc).__name__}
fresh, _, _ = stable_snapshot(resolve, state["project_id"], state["timeline_id"])
if fresh != state:
raise PreflightError("Structure changed while preparing the boundary plan")
return report
def read_audio_tracks(resolve, project_id=None, timeline_id=None):
"""Targeted inventory: does not require clip access, buses or media decoding."""
def read():
project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
n = count(invoke(timeline, "GetTrackCount", "audio"), "audio track count")
rows = []
for index in range(1, n + 1):
rows.append({"index": index,
"name": text(invoke(timeline, "GetTrackName", "audio", index), "track name"),
"format": ident(invoke(timeline, "GetTrackSubType", "audio", index), "audio format"),
"enabled": boolean(invoke(timeline, "GetIsTrackEnabled", "audio", index), "track enable"),
"locked": boolean(invoke(timeline, "GetIsTrackLocked", "audio", index), "track lock")})
get_target(resolve, pid, tid)
return {"project_id": pid, "timeline_id": tid, "audio_track_count": n, "tracks": rows,
"scope": "Track inventory only; not clip channels, buses, routing or audible output"}
first, second = read(), read()
if first != second:
raise PreflightError("Audio track inventory changed during reads")
return second
def cli_target(parser, required=True):
parser.add_argument("--project-id", required=required)
parser.add_argument("--timeline-id", required=required)
def cli_write(parser):
parser.add_argument("--apply", action="store_true", help="Issue at most one write after review")
parser.add_argument("--expect-plan", help="Exact token from a separately reviewed plan")
parser.add_argument("--test-copy", action="store_true", help="Acknowledge this is a disposable test copy")
def print_result(fn):
try:
result = fn()
except Exception as exc:
print(json.dumps({"status": "stopped", "error": type(exc).__name__ + ": " + str(exc)}, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
return 3 if result.get("status") == "outcome_unconfirmed" else 0
Optional: inspect your local .pyi without execution
This standalone checker only parses a bounded local interface file. Reported declarations belong to that document; they are not a live capability probe or a permission to run a mutation. Use a Python version that supports the file’s syntax.
"""Read a LOCAL .pyi as text; never import or execute it. No connection to Resolve.
Presence is documentation evidence, not a live capability test. Missing names
refer only to this exact file, not every past or future Resolve release.
"""
import argparse
import ast
import hashlib
import json
from pathlib import Path
CHECKS = {
"Timeline": ["GetTrackCount", "AddTrack", "GetTrackSubType", "SetTrackLock",
"GetIsTrackLocked", "DeleteClips", "SplitClip", "SplitClips", "TrimClip"],
"TimelineItem": ["GetType", "GetLinkedItems", "GetStart", "GetEnd", "GetDuration",
"GetLeftOffset", "GetRightOffset", "SetStart", "SetEnd",
"SetLeftOffset", "SetRightOffset", "Split", "Trim"],
}
def audit(path):
path = Path(path)
if path.suffix.lower() != ".pyi":
raise ValueError("Pass the installed DaVinciResolveScript.pyi, not a script or binary")
if not path.is_file():
raise ValueError("A regular local .pyi file is required")
with path.open("rb") as stream:
raw = stream.read(2 * 1024 * 1024 + 1)
if len(raw) > 2 * 1024 * 1024:
raise ValueError("File exceeds this tutorial's 2 MiB bound")
tree = ast.parse(raw.decode("utf-8-sig"), filename=path.name)
result = {}
for name, wanted in CHECKS.items():
classes = [n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == name]
if len(classes) != 1:
raise ValueError("Expected one top-level class: " + name)
methods = {n.name: n for n in classes[0].body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
result[name] = {key: {"documented_in_this_file": key in methods,
"line": methods[key].lineno if key in methods else None,
"parameters": [a.arg for a in methods[key].args.args] if key in methods else []}
for key in wanted}
return {"schema": "videomontazher.resolve.local-contract-audit.v1",
"file_name": path.name, "sha256": hashlib.sha256(raw).hexdigest(),
"bytes": len(raw), "status": "document_only_not_live_verified", "classes": result}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--doc", type=Path, required=True)
args = parser.parse_args()
try:
print(json.dumps(audit(args.doc), ensure_ascii=False, indent=2))
return 0
except Exception as exc:
print(json.dumps({"status": "not_audited", "error": type(exc).__name__ + ": " + str(exc)}))
return 2
if __name__ == "__main__":
raise SystemExit(main())
Shared connection helper — inspect and download
This file only locates and loads the installed bridge. Native imports can write Python bytecode caches and can block; this is not a sandbox or a timeout mechanism. Use a trusted local installation.
"""Load the installed Blackmagic module; do not install a similarly named package.
This helper uses standard desktop installation paths or RESOLVE_SCRIPT_API.
It does not enable scripting, start Resolve, open projects or retry API calls.
Validate Python/native-library compatibility against your installed README.txt.
"""
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import struct
import sys
class ResolveConnectionError(RuntimeError):
"""An explicit environment, module, or application-connection failure."""
def scripting_root() -> Path:
configured = os.environ.get("RESOLVE_SCRIPT_API")
if configured is not None:
if not configured.strip() or configured != configured.strip():
raise ResolveConnectionError("RESOLVE_SCRIPT_API is empty or has outer whitespace")
if configured.startswith(('"', "'")) or configured.endswith(('"', "'")):
raise ResolveConnectionError("Do not store literal outer quotes in RESOLVE_SCRIPT_API")
root = Path(os.path.expandvars(configured)).expanduser()
elif sys.platform == "win32":
root = Path(os.environ.get("PROGRAMDATA", r"C:\ProgramData")) / "Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting"
elif sys.platform == "darwin":
root = Path("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting")
elif sys.platform.startswith("linux"):
root = Path("/opt/resolve/Developer/Scripting")
else:
raise ResolveConnectionError("Set RESOLVE_SCRIPT_API for this platform/installation")
if not root.is_absolute():
raise ResolveConnectionError("RESOLVE_SCRIPT_API must be an absolute Scripting directory")
return root
def module_file() -> Path:
# RESOLVE_SCRIPT_API points to Scripting, not its Modules subdirectory.
return scripting_root() / "Modules" / "DaVinciResolveScript.py"
def load_resolve_module():
if struct.calcsize("P") * 8 != 64:
raise ResolveConnectionError("Use a compatible 64-bit Python interpreter")
source = module_file()
if not source.is_file():
raise ResolveConnectionError("Official DaVinciResolveScript.py not found at: " + str(source))
configured_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
if configured_lib is not None:
lib = Path(os.path.expandvars(configured_lib)).expanduser()
if not lib.is_absolute() or not lib.is_file():
raise ResolveConnectionError("RESOLVE_SCRIPT_LIB must name the installed native library file")
name = "DaVinciResolveScript"
spec = importlib.util.spec_from_file_location(name, source)
if spec is None or spec.loader is None:
raise ResolveConnectionError("Cannot create a loader for the official module")
previous = sys.modules.get(name)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
try:
spec.loader.exec_module(module)
# Blackmagic's wrapper can replace its sys.modules entry with a native module.
loaded = sys.modules[name]
if not callable(getattr(loaded, "scriptapp", None)):
raise ResolveConnectionError("Loaded module does not expose scriptapp")
return loaded
except Exception as exc:
if previous is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = previous
raise ResolveConnectionError("Official module was found, but loading it failed") from exc
def connect():
module = load_resolve_module()
try:
resolve = module.scriptapp("Resolve")
except Exception as exc:
raise ResolveConnectionError("scriptapp failed; inspect the original exception") from exc
if not resolve:
raise ResolveConnectionError("No Resolve object: check running Studio, Local access and Python environment")
return resolve
def project_manager(resolve):
manager = resolve.GetProjectManager()
if not manager:
raise ResolveConnectionError("Resolve connected, but Project Manager is unavailable")
return manager
Launch commands
Inspect one target
python unlock_track.py --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --track-type video --index 2Replace the IDs. This command does not unlock anything.
One approved call
python unlock_track.py --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --track-type video --index 2 --apply --test-copy --expect-plan "REVIEWED_SHA256"Use the token from a separately reviewed plan for this exact target.
What the code does
Bind the target
The helper obtains current objects and checks the supplied IDs. It never loads another project or switches timelines to repair a mismatch.
Observe the structure
Two reads compare tracks, flags and item occurrences. This checks selected structural fields, not an atomic snapshot of every effect and setting.
Validate the operation
The plan contains exact arguments, the before-state and SHA-256. The hash is a reviewed-plan fingerprint, not a signature, permission mechanism or protection against every race.
One call, then read back
Only explicit apply can issue one mutation. State is then read separately; False, an exception or a mismatch never triggers a retry or an invented rollback.
Offline tests exercise our logic with API doubles. Native calls, external Python builds and the behaviour of your exact Resolve version require a separate local check.
verified_observed_scope confirms only the observed structural fields. Effects, associated data, routing and complete project state are not certified. After outcome_unconfirmed, establish the actual outcome before doing anything else.
Reports may contain project, track and clip names. Redact them manually before sharing; the script neither uploads them nor automatically anonymises them.
DEBUG / OBSERVATION FIRST
An error is a starting point, not a diagnosis.
Target mismatch
Possible reasonThe active project or timeline changed after the IDs were recorded.
Next checkStop and identify the target again. Never automatically replace the expected ID in a write command.
outcome_unconfirmed
Possible reasonThe write response or subsequent read does not establish the expected structure.
Next checkInspect the actual outcome in Resolve. Do not blindly repeat deletion, creation or unlocking.
Stale plan
Possible reasonAt least one observed field changed between review and apply.
Next checkUnderstand the change, then obtain and review a new plan separately. Do not bypass the fingerprint check.
SetTrackLock returned False
Possible reasonThe application did not confirm the mutation; its actual outcome needs inspection.
Next checkInspect the lock and retain the report. Do not enable the track or unlock other tracks as a fallback.
PRACTICE / YOUR OWN EVIDENCE
Unlock a disabled test V2
- Create V2 in a copy, disable it and lock it. Record project and timeline IDs.
- Read the video/2 plan and confirm enabled: false and locked: true.
- Apply the reviewed token and compare both the report and Resolve.
- Obtain another plan; it should require zero mutations.
Only V2 is unlocked and it remains disabled. Applying the new no-op plan calls no setter.
This is a checklist for your test, not a report of a test already performed on the website.
FAQ
Questions that matter in practice.
Can I unlock every track?
The method can address each track, but this example deliberately handles one. A bulk workflow needs explicit targets, separate approval and per-operation results rather than a loop that hides failures.[14]
Why is the track still disabled?
Enable state and lock state are different. The task was to unlock, so GetIsTrackEnabled should remain unchanged. Altering it would expand the operation beyond its approved scope.[14]
Does the plan hash guarantee safety?
No. It compares explicitly observed fields and arguments. Unobserved properties and changes just after the final read remain possible; use a test copy and avoid concurrent editing.
Does this save the project?
The example does not call SaveProject or control Resolve’s autosave settings. The absence of an explicit save is not a promise that the change is temporary; check application state and persistence separately.[14]
SOURCES / VERSION BOUNDARIES
Sources, not implied certification.
The source review, offline Python tests and UI checks are different kinds of evidence. None establishes that every example works with every Studio build, operating system or Python version. The date below is the editorial review date, not a live-test certificate.
- Blackmagic Design — DaVinci Resolve Studio 21.1 release notes
8 September 2026. The release announcement assigns Python scripting to Studio. Edition requirements, not a test report for our examples.
- Blackmagic Design — Support Center
Release catalogue checked on 5 October 2026: 21.1.1 is listed. The latest release is not the minimum version of every method.
- Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)
Public mirror of Blackmagic’s original README, maintained by X-Raym. Used for basic signatures; not official hosting and not a 21.1.1 README. Locate your installed Developer/Scripting/README.txt through Resolve’s Help/Documentation menu; exact menu nesting can vary.
- Blackmagic Design — Scripting and Automation
Official description of Studio Python/Lua scripting and integrations. Do not extend Studio capabilities to Free without checking the exact release.
- Blackmagic DaVinciResolveScript.pyi 21.1 — published mirror
S05 checks a published mirror of Blackmagic’s original 21.1 interface file. Third-party hosting; signatures do not replace live verification. Compare your installed Developer/Scripting/DaVinciResolveScript.pyi. Findings about absent split/trim declarations are scoped to that document.