01 / BEFORE THE CODE
До кода — условия запуска.
Сначала выберите контекст
Используйте Python 3.8+ и настроенное подключение к Resolve Studio из S01. Основной файл, resolve_api_timecode.py и resolve_api_connection.py должны лежать рядом. Официальный модуль берётся только из своей установки Resolve.
Точная частота и единицы
Поддерживаемые частоты этого примера: 24, 25, 30, 48, 50, 60 и 24000/1001, 30000/1001, 48000/1001, 60000/1001. Это граница нашего helper, а не список возможностей Resolve. Дробь задаётся явно; строка 29.97 не заменяется молча на 30000/1001.
Граница безопасного опыта
Метки ограничены одними сутками, целыми кадрами и явно выбранным DF/NDF. Переход через полночь, отрицательные длительности, поля interlaced и субкадровый звук не угадываются. Для чтения приложения остановите воспроизведение; для перемещения playhead используйте тестовую копию и не меняйте цель во время вызова.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
GetStartFrame()- Вход
- Без аргументов.
- Возвращает
- int — начальная координата таймлайна.
- Проверить
- Проверить точный тип; не int(None) и не int(float).
GetEndFrame()- Вход
- Без аргументов.
- Возвращает
- int — конечная координата.
- Проверить
- Не переносить соглашение границ на рендер.
GetStartTimecode()- Вход
- Без аргументов.
- Возвращает
- Строка стартовой метки.
- Проверить
- Хранить отдельно, не заменять base.
GetUniqueId()- Вход
- Текущий объект.
- Возвращает
- Непустая строка ID.
- Проверить
- Сверить цель после чтения.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
Timeline.GetStartFrame и TimelineItem.GetStart — разные уровни
GetStartFrame принадлежит Timeline и возвращает начало последовательности. GetStart у TimelineItem описывает границу конкретного вхождения клипа. Даже первый клип может начинаться позже начала таймлайна: перед ним есть зазор, другой тип дорожки или иной монтажный материал.
В старом helper после неудачи GetStartFrame пробовался GetStart на том же объекте, а затем подставлялся 0. Такая цепочка прячет отсутствие нужного контракта. Теперь читатель видит либо валидный start_frame, либо ошибку конкретного чтения. В пустом таймлайне начало по-прежнему является отдельной настройкой, а не минимумом позиций клипов.[3]
04 / UNDERSTAND
Две простые формулы без повторного FPS
Когда absolute и base уже измерены в кадрах одной шкалы, offset = absolute − base. Обратно: absolute = base + offset. Для base 86400 позиция 86640 находится через 240 кадров. Повторное умножение на FPS ошибочно: обе величины уже являются кадрами.
Отрицательный offset в отчёте чтения сохраняется: он показывает, что переданная абсолютная координата раньше начала. Это не приказ переместить playhead. Поле inside_conservative_bounds отдельно сообщает попадание в используемую здесь проверку [start, end), а setter из соседней статьи отрицательное смещение отклоняет.
05 / UNDERSTAND
Число API и строка начала остаются независимыми
Отчёт хранит start_frame, start_timecode_raw и fps_raw. Числовой счёт, вычисленный из метки, можно сравнить с координатой API, но нельзя автоматически объявлять их равными для всех поддержанных форматов. Разницу нужно понимать в контексте конкретной временной базы и версии приложения.
Поэтому read_clock ничего не округляет и не исправляет. Он даже не требует превращать строку 29.97 в точную дробь ради чтения GetStartFrame. Точная частота нужна на следующем этапе — для секунд и форматирования TC, а получение исходной координаты само по себе не должно зависеть от угаданного FPS.[3]
06 / UNDERSTAND
Измените только координату базы
Оставьте 01:00:00:00, 24 NDF и offset 240 в калькуляторе. Измените учебный base с 86400 на 0: абсолютная координата изменится с 86640 на 240, а целевая метка останется 01:00:10:00. Калькулятор показывает выбранные входы, не утверждает, что любое сочетание существует в Resolve.
В рабочем сценарии значение base следует читать через GetStartFrame, а не копировать из этого упражнения. Так отчёт помогает увидеть ошибку двойного прибавления начала, но не становится источником настроек для чужого проекта.
Целые кадры: округления нет.
Кадр API и метка начала — независимые опорные значения, одно не вычисляется из другого. Калькулятор ограничен одними сутками таймкода, целокадровыми метками и перечисленными частотами. Настройки и границы вашего проекта он не проверяет. Ввод не отправляется и не сохраняется.
07 / UNDERSTAND
Что подтверждает повторное чтение
Пример дважды читает ID цели, начало, конец, стартовую метку и строку FPS. Между чтением полей и после него проверяется текущий проект/таймлайн. Несовпадение останавливает отчёт, чтобы не смешивать значения двух разных последовательностей в одном якобы достоверном результате.
Совпавшие наблюдения не являются транзакцией: проект мог измениться между ними и вернуться к прежним значениям. Нет и тайм-аута для зависшего нативного вызова. Сохраните работу, остановите воспроизведение и не переключайте объекты во время диагностического чтения. Отчёт не изменяет начало и не сохраняет проект.
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Скачайте timeline_base_frame.py и resolve_api_timecode.py. Для чтения приложения или перехода playhead нужен также resolve_api_connection.py. В комплекте шесть примеров, два helper и инструкции RU/EN. Чистые конвертеры не импортируют модуль Blackmagic; расчёт секунд подключается только с --timeline.
Скачать все примеры с инструкцией (.zip) ↓"""Read the API start coordinate without GetStart fallback or zero substitution."""
import argparse
import json
from resolve_api_connection import connect
from resolve_api_timecode import read_clock
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--absolute", type=int, help="Optional integer API coordinate for offset calculation")
a = p.parse_args()
state = read_clock(connect())
result = {"state": state, "operation": "read_only"}
if a.absolute is not None:
result["absolute_timeline_frame"] = a.absolute
result["offset_frames"] = a.absolute - state["start_frame"]
result["inside_conservative_bounds"] = state["start_frame"] <= a.absolute < state["end_frame"]
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Timecode-helper — точная математика, опоры и контролируемый переход
Авторская реализация, не SDK Blackmagic. DF/NDF-метки отделены от секунд длительности. Clock принимает явно выбранную точную частоту, а не угадывает её из сырой строки FPS. Только move_playhead с apply может вызвать setter, и только один раз.
"""S04 author helpers: exact timecode math and explicitly scoped Resolve reads.
Python 3.8+. Standard library only. Not a Blackmagic SDK, transaction or sandbox.
String timecode policy: HH:MM:SS:FF (NDF) / HH:MM:SS;FF (DF), within one 24h day.
Supported timebases: 24/25/30/48/50/60 and 24000/30000/48000/60000 over 1001.
DF is deliberately limited to 30000/1001 and 60000/1001. No silent rate aliases.
Only move_playhead(..., apply=True) calls a setter, once, after reviewed guards.
"""
from dataclasses import dataclass
from fractions import Fraction
import hashlib
import json
import re
from typing import Any, Dict
class ClockError(ValueError):
"""Input or observed state does not meet this example's contract."""
class OutcomeUnknown(RuntimeError):
"""A setter was dispatched but its result was not fully verified. Do not retry."""
def integer(value: Any, name: str) -> int:
# bool is a subclass of int; never accept it as a frame index.
if type(value) is not int:
raise ClockError(name + " must be an integer, not a float, bool or string")
return value
def exact_number(value: Any, name: str) -> Fraction:
if isinstance(value, Fraction):
return value
if type(value) is int:
return Fraction(value)
if not isinstance(value, str) or len(value) > 80:
raise ClockError(name + " must be exact decimal/rational text (not float)")
# Avoid scientific exponents/unbounded integers and locale-dependent commas.
if not re.fullmatch(r"[+-]?[0-9]{1,18}(?:\.[0-9]{1,18}|/[0-9]{1,18})?", value):
raise ClockError(name + " requires ASCII decimal or numerator/denominator text")
try:
return Fraction(value)
except (ValueError, ZeroDivisionError) as exc:
raise ClockError(name + " is invalid") from exc
SUPPORTED = {Fraction(x): x for x in (24, 25, 30, 48, 50, 60)}
SUPPORTED.update({Fraction(x * 1000, 1001): x for x in (24, 30, 48, 60)})
@dataclass(frozen=True)
class Clock:
rate: Fraction
mode: str
def __post_init__(self):
rate = exact_number(self.rate, "rate")
object.__setattr__(self, "rate", rate)
if rate not in SUPPORTED:
raise ClockError("Unsupported/ambiguous rate. Use e.g. 24 or 30000/1001, not 29.97")
if self.mode not in ("ndf", "df"):
raise ClockError("mode must explicitly be ndf or df")
if self.mode == "df" and rate not in (Fraction(30000, 1001), Fraction(60000, 1001)):
raise ClockError("DF example supports only 30000/1001 and 60000/1001")
@property
def nominal(self) -> int:
return SUPPORTED[self.rate]
@property
def drop(self) -> int:
return self.nominal // 15 if self.mode == "df" else 0
@property
def frames_per_day(self) -> int:
return self.nominal * 86400 - self.drop * (1440 - 144)
def frames_to_timecode(frames: int, clock: Clock) -> str:
"""Actual whole frames since TC zero -> label. No wrapping or rounding."""
frames = integer(frames, "frames")
if not 0 <= frames < clock.frames_per_day:
raise ClockError("Frame count must be within one timecode day; no negative/wrap")
labelled = frames
if clock.drop:
# Independent implementation of the ten-minute/minute numbering rule.
per_ten = clock.nominal * 600 - clock.drop * 9
per_short_minute = clock.nominal * 60 - clock.drop
tens, rest = divmod(frames, per_ten)
skipped_minutes = max(0, (rest - clock.drop) // per_short_minute)
labelled += clock.drop * (tens * 9 + skipped_minutes)
seconds, ff = divmod(labelled, clock.nominal)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
separator = ";" if clock.mode == "df" else ":"
return "{:02d}:{:02d}:{:02d}{}{:02d}".format(hours, minutes, seconds, separator, ff)
def timecode_to_frames(timecode: str, clock: Clock) -> int:
"""Label -> actual whole frames since TC zero. Invalid labels never become 0."""
if not isinstance(timecode, str):
raise ClockError("timecode must be text")
match = re.fullmatch(r"([0-9]{2}):([0-9]{2}):([0-9]{2})([:;])([0-9]{2})", timecode)
if not match:
raise ClockError("Expected HH:MM:SS:FF (NDF) or HH:MM:SS;FF (DF)")
h, m, s, sep, f = match.groups()
h, m, s, f = map(int, (h, m, s, f))
if h >= 24 or m >= 60 or s >= 60 or f >= clock.nominal:
raise ClockError("Timecode field outside this clock's range")
if sep != (";" if clock.mode == "df" else ":"):
raise ClockError("Timecode separator disagrees with explicit DF/NDF mode")
if clock.drop and m % 10 != 0 and s == 0 and f < clock.drop:
raise ClockError("This label is skipped by drop-frame numbering")
minutes = h * 60 + m
count = (h * 3600 + m * 60 + s) * clock.nominal + f
return count - clock.drop * (minutes - minutes // 10)
def offset_to_timecode(offset: int, start_timecode: str, clock: Clock) -> str:
offset = integer(offset, "offset")
if offset < 0:
raise ClockError("Negative offsets are outside this tutorial's scope")
return frames_to_timecode(timecode_to_frames(start_timecode, clock) + offset, clock)
def timecode_to_offset(timecode: str, start_timecode: str, clock: Clock) -> int:
offset = timecode_to_frames(timecode, clock) - timecode_to_frames(start_timecode, clock)
if offset < 0:
raise ClockError("Timecode precedes start; no implicit midnight wrap")
return offset
def seconds_to_frames(seconds: Any, clock: Clock, rounding: str) -> Dict[str, Any]:
"""Duration -> offset, with exact arithmetic and an explicit rounding policy."""
value = exact_number(seconds, "seconds")
if value < 0:
raise ClockError("Negative durations are rejected, not clamped to zero")
exact = value * clock.rate
floor = exact.numerator // exact.denominator
if rounding == "exact":
if exact.denominator != 1:
raise ClockError("Duration is not a whole frame; choose a rounding policy")
result = floor
elif rounding == "floor":
result = floor
elif rounding == "ceil":
result = floor + (exact.denominator != 1)
elif rounding == "nearest":
# Documented half-up for non-negative durations, not Python's ties-to-even.
result = floor + ((exact - floor) >= Fraction(1, 2))
else:
raise ClockError("rounding must be exact, floor, ceil or nearest")
return {"rate": str(clock.rate), "seconds_exact": str(value), "frames_exact": str(exact),
"offset_frames": result, "rounding": rounding,
"error_seconds_exact": str(Fraction(result, 1) / clock.rate - value)}
def call(obj: Any, method: str, *args: Any) -> Any:
try:
fn = getattr(obj, method, None)
if not callable(fn):
raise ClockError(method + " is not callable")
return fn(*args)
except ClockError:
raise
except Exception as exc:
raise ClockError(method + " failed; original exception retained") from exc
def text(value: Any, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ClockError(field + " must be non-empty text")
return value
def context(resolve):
manager = call(resolve, "GetProjectManager")
project = call(manager, "GetCurrentProject")
if project is None:
raise ClockError("No open project")
timeline = call(project, "GetCurrentTimeline")
if timeline is None:
raise ClockError("No active timeline")
identity = {"project_id": text(call(project, "GetUniqueId"), "project ID"),
"timeline_id": text(call(timeline, "GetUniqueId"), "timeline ID")}
return project, timeline, identity
def observation(resolve, include_playhead=False):
_, timeline, identity = context(resolve)
result = dict(identity)
result.update({"start_frame": integer(call(timeline, "GetStartFrame"), "start_frame"),
"end_frame": integer(call(timeline, "GetEndFrame"), "end_frame"),
"start_timecode_raw": text(call(timeline, "GetStartTimecode"), "start timecode"),
"fps_raw": text(call(timeline, "GetSetting", "timelineFrameRate"), "timeline FPS")})
if include_playhead:
page = call(resolve, "GetCurrentPage")
if page not in ("cut", "edit", "color", "fairlight", "deliver"):
raise ClockError("Open Cut/Edit/Color/Fairlight/Deliver manually; no page switch is made")
result["page"] = page
result["current_timecode_raw"] = text(call(timeline, "GetCurrentTimecode"), "current timecode")
_, _, after = context(resolve)
if identity != after:
raise ClockError("Active target changed while reading")
return result
def read_clock(resolve, include_playhead=False):
"""Two matching observations, NOT an atomic snapshot or a process timeout."""
a = observation(resolve, include_playhead)
b = observation(resolve, include_playhead)
if a != b:
raise ClockError("Clock observations differ; stop playback/editing and inspect")
return dict(a, observation_policy="two_matching_reads_not_atomic")
# These labels are accepted only AFTER the caller explicitly selects the exact rate.
# The report retains the raw label and marks that rate origin as user-declared.
DISPLAY_LABELS = {Fraction(24000, 1001): "23.976", Fraction(30000, 1001): "29.97",
Fraction(48000, 1001): "47.952", Fraction(60000, 1001): "59.94"}
def validate_clock(state, clock: Clock):
raw = state["fps_raw"].strip()
has_df = raw.upper().endswith(" DF")
numeric = raw[:-3].strip() if has_df else raw
actual = exact_number(numeric, "observed FPS label")
alias = DISPLAY_LABELS.get(clock.rate)
if actual != clock.rate and (alias is None or actual != Fraction(alias)):
raise ClockError("Explicit rate conflicts with the observed timeline FPS label")
if has_df and clock.mode != "df":
raise ClockError("Observed FPS label says DF but NDF was requested")
# Absence of DF in the FPS string does not itself prove NDF; inspect start label.
timecode_to_frames(state["start_timecode_raw"], clock)
if "current_timecode_raw" in state:
timecode_to_frames(state["current_timecode_raw"], clock)
def seconds_at_timeline(resolve, seconds, rate, mode, rounding):
clock = Clock(rate, mode)
state = read_clock(resolve)
validate_clock(state, clock)
result = seconds_to_frames(seconds, clock, rounding)
result.update({"state": state, "rate_origin": "explicit_user_selection_checked_against_label",
"absolute_timeline_frame": state["start_frame"] + result["offset_frames"],
"timecode": offset_to_timecode(result["offset_frames"], state["start_timecode_raw"], clock),
"inside_conservative_bounds": state["start_frame"] + result["offset_frames"] < state["end_frame"],
"operation": "calculation_only_no_seek_or_insert"})
return result
def make_plan(state, offset, clock):
validate_clock(state, clock)
offset = integer(offset, "offset")
absolute = state["start_frame"] + offset
if offset < 0 or not state["start_frame"] <= absolute < state["end_frame"]:
raise ClockError("Target outside conservative [start, end) guard; end endpoint is excluded")
target_tc = offset_to_timecode(offset, state["start_timecode_raw"], clock)
plan = {"state": state, "offset_frames": offset, "absolute_timeline_frame": absolute,
"target_timecode": target_tc, "rate": str(clock.rate), "mode": clock.mode,
"rate_origin": "explicit_user_selection_checked_against_label"}
payload = json.dumps(plan, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
plan["plan_digest"] = hashlib.sha256(payload.encode("utf-8")).hexdigest()
return plan
def move_playhead(resolve, offset, rate, mode, apply=False, expected_project_id=None,
expected_timeline_id=None, expected_plan_digest=None):
"""Plan only unless explicit guards match. Setter dispatched at most once."""
if type(apply) is not bool:
raise ClockError("apply must be bool")
clock = Clock(rate, mode)
before = read_clock(resolve, include_playhead=True)
plan = make_plan(before, offset, clock)
if not apply:
return dict(plan, status="plan_only", setter_called=False)
if (not expected_project_id or not expected_timeline_id or not expected_plan_digest or
expected_project_id != before["project_id"] or expected_timeline_id != before["timeline_id"] or
expected_plan_digest != plan["plan_digest"]):
raise ClockError("--apply requires matching reviewed project, timeline and plan digest")
if read_clock(resolve, include_playhead=True) != before:
raise ClockError("Target or playhead changed since the plan; no setter called")
_, timeline, identity = context(resolve)
if any(identity[k] != before[k] for k in identity):
raise ClockError("Target changed before dispatch")
if before["current_timecode_raw"] == plan["target_timecode"]:
return dict(plan, status="already_at_target", setter_called=False)
try:
# The only setter in this file. No fallback, polling, save, playback or retry.
accepted = call(timeline, "SetCurrentTimecode", plan["target_timecode"])
after = read_clock(resolve, include_playhead=True)
static_before = {k: v for k, v in before.items() if k != "current_timecode_raw"}
static_after = {k: v for k, v in after.items() if k != "current_timecode_raw"}
if accepted is not True or static_before != static_after:
raise ClockError("Setter not confirmed or target/clock changed")
if after["current_timecode_raw"] != plan["target_timecode"]:
raise ClockError("Read-back does not match requested timecode exactly")
except Exception as exc:
raise OutcomeUnknown("Playhead setter dispatched; inspect Resolve before any retry") from exc
return dict(plan, status="read_back_matched_check_ui", setter_called=True, after=after,
ui_verified=False)
Общий helper подключения — посмотреть и скачать
Этот файл находит и загружает установленный мост. При импорте Python может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.
"""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
Команды запуска
Прочитать опору
python timeline_base_frame.pyНикаких изменений. Нужен открытый тестовый проект с таймлайном.
Посчитать offset известной API-координаты
python timeline_base_frame.py --absolute 8664086640 — учебный ввод. С реальной целью используйте собственную прочитанную координату.
Что делает код
Получить цель
Открытый проект и таймлайн обязательны; ID используются вместо предположений по имени.
Прочитать int
GetStartFrame проверяется без усечения и без fallback к другому методу.
Сопоставить наблюдения
Два чтения должны согласоваться по ID и часам; это не блокировка интерфейса.
Вычесть базу
Необязательный --absolute добавляет offset и отдельную проверку границ, ничего не перемещая.
Все преобразования в helper — авторские функции, не методы Blackmagic. Успешный расчёт не подтверждает FPS, настройки проекта или возможность вызова в установленном Resolve.
При ошибке старый результат не подставляется. Native-вызовы не имеют добавленного тайм-аута; два одинаковых чтения не создают атомарный снимок и не блокируют интерфейс.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
GetStartFrame failed
Возможная причинаНедоступен документированный метод или соединение потеряно.
Следующая проверкаПроверьте установленный README и подключение; не переходите к GetStart.
start_frame must be an integer
Возможная причинаПолучен None, bool, float или строка.
Следующая проверкаСохраните реальный тип и выясните расхождение вместо int(...).
Отрицательный offset
Возможная причинаПереданная координата расположена до прочитанного начала.
Следующая проверкаУточните единицы и происхождение absolute; не обнуляйте ответ.
Clock observations differ
Возможная причинаВо время чтения поменялся таймлайн или его часы.
Следующая проверкаОстановите действия и перечитайте отдельно, не объединяя старые значения.
PRACTICE / YOUR OWN EVIDENCE
Проверьте зазор перед первым клипом
- На тестовом таймлайне оставьте зазор перед первым клипом.
- Прочитайте start_frame и отдельно позицию клипа из S03.
- Проверьте absolute = base + offset и знак offset для координаты до начала.
Начало таймлайна не подменяется первым клипом; формулы согласованы; ошибка чтения не даёт 0.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Стартовый кадр всегда 86400?
Нет. Это лишь пример для привычной часовой опоры при 24 NDF. Всегда читайте значение у своей цели.
Нужно искать первый клип?
Нет. Начало последовательности и начало первого клипа могут отличаться: перед первым клипом возможен пустой участок. Для общей опоры читайте GetStartFrame, а не минимальный старт найденного клипа.[3]
Можно считать FPS ненужным вообще?
Для разности двух координат в кадрах — да. Для секунд, таймкода и сопоставления разных шкал понадобится точная временная база.
Почему одновременно читается стартовый TC?
Чтобы сохранить обе опоры для проверки, не заменяя одну другой. Сам base не вычисляется из строки.
SOURCES / VERSION BOUNDARIES
Источники, а не подразумеваемая сертификация.
Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.
- Blackmagic Design — DaVinci Resolve Studio 21.1 release notes
8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.
- Blackmagic Design — Support Center
Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.
- Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)
Публичная копия первичного README Blackmagic, поддерживаемая X-Raym. По ней сверены базовые сигнатуры; это не официальный хостинг и не README 21.1.1. Оригинал своей установки: Help → Documentation → Developer → Scripting → README.txt; расположение пункта зависит от версии.
- Blackmagic Design — Scripting and Automation
Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.