ВИДЕОМОНТАЖЁРDAVINCI RESOLVE · ВИДЕО · AI

RESOLVE API / TIMECODE LAB / 02

Как перевести таймкод в кадры

Неверная метка — ошибка. Не нулевой кадр.

Строгий разбор NDF/DF: допустимые поля, пропущенные номера, целые кадры и смещение относительно начала таймлайна. Без float, подстановки 0 и угадывания перехода через полночь.

НА ВЫХОДЕ

Два явно подписанных результата: счёт от нуля TC и смещение от начала.

PYTHON / TIMECODEУЧЕБНАЯ МОДЕЛЬ
01tc = "00:01:00;02"

Проверяем форму строки и каждый диапазон, включая номера, пропущенные в DF.

ПРОВЕРКА

Ошибки не дают 0.

Переключатели меняют объяснение. Ничего не подключается к Resolve и не запускается на вашем компьютере.

01 / BEFORE THE CODE

До кода — условия запуска.

01

Сначала выберите контекст

Математический пример работает в Python 3.8+ без Resolve и без сторонних пакетов. Положите основной файл и resolve_api_timecode.py в одну папку. Модуль подключения не нужен.

02

Точная частота и единицы

Поддерживаемые частоты этого примера: 24, 25, 30, 48, 50, 60 и 24000/1001, 30000/1001, 48000/1001, 60000/1001. Это граница нашего helper, а не список возможностей Resolve. Дробь задаётся явно; строка 29.97 не заменяется молча на 30000/1001.

03

Граница безопасного опыта

Метки ограничены одними сутками, целыми кадрами и явно выбранным DF/NDF. Переход через полночь, отрицательные длительности, поля interlaced и субкадровый звук не угадываются. Для чтения приложения остановите воспроизведение; для перемещения playhead используйте тестовую копию и не меняйте цель во время вызова.

Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]

02 / API CONTRACT

Какой объект. Какой метод. Какой результат.

Pythonre.fullmatch(pattern, timecode)
Вход
Строка из четырёх двухзначных полей.
Возвращает
Полное совпадение или отказ.
Проверить
Не принимать хвост, пробелы и пять полей.
Авторский helpertimecode_to_frames(tc, clock)
Вход
Валидная метка и явный Clock.
Возвращает
Счёт кадров от нуля TC.
Проверить
Не вернуть 0 при ошибке.
Авторский helpertimecode_to_offset(tc, start, clock)
Вход
Целевая и стартовая метки.
Возвращает
Неотрицательный offset.
Проверить
Метка до начала отклоняется.
TimelineGetStartFrame()
Вход
Без аргументов; нужен для API-координаты.
Возвращает
Начальный кадр таймлайна.
Проверить
Основной математический пример его не вызывает.

Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]

03 / UNDERSTAND

Нулевой кадр — допустимый результат, не код ошибки

Старый parse_tc_to_frames ловил любое исключение и возвращал 0. В результате опечатка в таймкоде могла выглядеть как корректная команда перейти в начало, а отсутствие данных — как таймлайн с нулевой опорой. Состояния «метка равна нулю» и «метка не прочитана» должны быть различимы до следующего действия.

Теперь ClockError останавливает скрипт. Формат требует ровно четыре поля по две цифры, часы от 00 до 23, минуты и секунды от 00 до 59, FF меньше номинала. В нашей строгой форме NDF использует двоеточие, DF — точку с запятой. Невалидная строка не очищается и не дополняется автоматически.

04 / UNDERSTAND

Обратная формула: номинал минус пропущенные номера

Для NDF сначала считается ((HH × 3600 + MM × 60 + SS) × nominal) + FF. При 24000/1001 NDF nominal равен 24. Умножение часового поля на 23.976 вместо 24 смешало бы часы нумерации с физическими секундами и уже не было бы обратным преобразованием.

Для DF из номинального счёта вычитают drop × (total_minutes − total_minutes // 10). Здесь drop равен 2 или 4 для двух поддержанных DF-частот. Метки, которые должны быть пропущены, проверяются до формулы: арифметическое выражение само по себе не обнаружит, что 00:01:00;00 недопустим.[12]

05 / UNDERSTAND

Из метки сначала получаем offset, а не recordFrame

Обе метки — начало и цель — разбираются в одном Clock. Их разность даёт смещение. Например, 01:00:10:00 и 01:00:00:00 при 24 NDF дают 86640 и 86400 от нуля TC, поэтому offset равен 240. Для API-координаты затем прибавляют отдельно прочитанный start_frame.

Не считайте численное равенство метки начала и GetStartFrame обязательным свойством всех временных баз. В helper эти два значения хранятся независимо. Такая модель позволяет сопоставлять их явно и диагностировать разницу, вместо того чтобы исправлять прочитанное API-значение на вычисленное.[3]

06 / UNDERSTAND

Проверьте недопустимую метку, а не только красивый результат

Выберите готовый DF-пример, переключитесь в «TC → Кадры» и замените ;02 на ;00 на первой минуте. Численный результат должен исчезнуть, а сообщение — объяснить пропущенный номер. При переключении на ;02 он снова появится. Старое число не остаётся рядом с новым ошибочным вводом.

Для 24 NDF проверьте FF = 24 и минуту 60. Это не перенос разряда, а неправильная строка в данном контракте. Исправить исходные данные должен вызывающий код или человек. Автоматическое приведение сделало бы невозможным различить ошибку ввода и осознанную операцию.

TIME / COORDINATE LABЛокальные учебные данные · не Resolve
Загрузить готовый пример:
OFFSET240кадров от начала
API COORDINATE86640base + offset
TIMECODE01:00:10:0024 · NDF

Целые кадры: округления нет.

Кадр API и метка начала — независимые опорные значения, одно не вычисляется из другого. Калькулятор ограничен одними сутками таймкода, целокадровыми метками и перечисленными частотами. Настройки и границы вашего проекта он не проверяет. Ввод не отправляется и не сохраняется.

07 / UNDERSTAND

Метка до начала и последовательности длиннее суток

Отрицательная разность может быть полезна в отдельном математическом API, но основной пример предназначен для позиции внутри последовательности после её начала. Поэтому цель раньше start_timecode отклоняется. Это наша явная граница, не утверждение, что отрицательные значения никогда не встречаются в монтаже.

Таймкод не содержит даты. Для материала, пересекающего полночь, нужен дополнительный счётчик суток или абсолютная шкала времени с документированным отображением меток. Молчаливое прибавление 24 часов может направить действие в совсем другое место, поэтому здесь оно запрещено.

PYTHON / COMPLETE EXAMPLE

Прочитайте код. Затем запускайте осознанно.

Два файла; без подключения к Resolve

Скачайте timecode_to_frames.py и resolve_api_timecode.py. Для чтения приложения или перехода playhead нужен также resolve_api_connection.py. В комплекте шесть примеров, два helper и инструкции RU/EN. Чистые конвертеры не импортируют модуль Blackmagic; расчёт секунд подключается только с --timeline.

Скачать все примеры с инструкцией (.zip) ↓
timecode_to_frames.py
Скачать .py
"""Validated timecode -> offset from an explicit origin, never error-as-zero."""
import argparse
import json
from resolve_api_timecode import Clock, timecode_to_frames, timecode_to_offset


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("timecode")
    p.add_argument("--start", required=True)
    p.add_argument("--rate", required=True)
    p.add_argument("--mode", choices=("ndf", "df"), required=True)
    a = p.parse_args()
    clock = Clock(a.rate, a.mode)
    print(json.dumps({"timecode": a.timecode, "start_timecode": a.start,
                      "frames_since_tc_zero": timecode_to_frames(a.timecode, clock),
                      "offset_frames": timecode_to_offset(a.timecode, a.start, clock),
                      "rate": str(clock.rate), "mode": clock.mode}, indent=2))


if __name__ == "__main__":
    main()

Timecode-helper — точная математика, опоры и контролируемый переход

Авторская реализация, не SDK Blackmagic. DF/NDF-метки отделены от секунд длительности. Clock принимает явно выбранную точную частоту, а не угадывает её из сырой строки FPS. Только move_playhead с apply может вызвать setter, и только один раз.

resolve_api_timecode.py
Скачать .py
"""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)

Команды запуска

Смещение от часовой метки

Смещение от часовой метки
python timecode_to_frames.py "01:00:10:00" --start "01:00:00:00" --rate 24 --mode ndf

В JSON: frames_since_tc_zero = 86640; offset_frames = 240.

Обратный переход DF

Обратный переход DF
python timecode_to_frames.py "00:01:00;02" --start "00:00:00;00" --rate 30000/1001 --mode df

Ожидается 1800. Вариант ;00 должен завершиться ошибкой.

Что делает код

  1. Проверить форму

    fullmatch не допускает частичного совпадения, хвоста строки и скрытых лишних полей.

  2. Проверить диапазоны

    Часы, минуты, секунды, FF и пропущенные DF-номера проверяются до вычисления.

  3. Вычесть DF-пропуски

    Номинальный счёт корректируется по общему числу минут, исключая каждую десятую.

  4. Отделить origin

    В JSON сохраняются и полный счёт от TC-zero, и относительное смещение.

Границы, которые важно сохранить

Все преобразования в helper — авторские функции, не методы Blackmagic. Успешный расчёт не подтверждает FPS, настройки проекта или возможность вызова в установленном Resolve.

При ошибке старый результат не подставляется. Native-вызовы не имеют добавленного тайм-аута; два одинаковых чтения не создают атомарный снимок и не блокируют интерфейс.

DEBUG / OBSERVATION FIRST

Ошибка — отправная точка, не готовый диагноз.

Метка превратилась в 0 в старом коде

Возможная причинаОбщее except скрыло реальную ошибку строки.

Следующая проверкаПеренесите обработку исключения на границу задачи; не передавайте 0 дальше.

This label is skipped

Возможная причинаВведён запрещённый номер DF в начале недесятой минуты.

Следующая проверкаПроверьте исходный таймкод и режим, не округляйте к ближайшему.

Timecode precedes start

Возможная причинаЦель раньше опоры либо предполагались следующие сутки.

Следующая проверкаУкажите правильную опору или отдельную модель дней.

Offset меньше ожидаемого

Возможная причинаСчёт от нуля TC перепутан со смещением от начала.

Следующая проверкаСмотрите названия frames_since_tc_zero и offset_frames отдельно.

PRACTICE / YOUR OWN EVIDENCE

Проверка валидатора на плохих входах

  1. Переведите 01:00:10:00 относительно 01:00:00:00.
  2. Отправьте FF = 24 при 24 NDF и 00:01:00;00 при 29.97 DF.
  3. Проверьте цель до начала и корректную нулевую метку.
КРИТЕРИЙ ПРИЁМКИ

Корректные значения различаются по пространству координат; все неправильные входы дают явную ошибку, не 0.

Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.

FAQ

Вопросы, которые возникают на практике.

00:00:00:00 вернёт 0?

Да, при подходящем NDF-clock. Но неправильная строка даст исключение — её результат не спутать с законным нулём.

Можно передать 1:2:3:4?

В этом строгом примере нет. Сначала явно нормализуйте известный внешний формат отдельной функцией и сохраните исходный текст.

Почему 29.97 NDF и DF дают разные числа?

Они используют разную нумерацию при той же физической частоте. В DF из номинального счёта вычитаются пропущенные номера.[12]

Это способ получить путь к кадру исходника?

Нет. Offset таймлайна не учитывает In исходника, speed change, обратное воспроизведение и составные клипы.

SOURCES / VERSION BOUNDARIES

Источники, а не подразумеваемая сертификация.

Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.

  2. Blackmagic Design — Support Center

    Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.

  3. 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; расположение пункта зависит от версии.

  4. Blackmagic Design — Scripting and Automation

    Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.

  5. FFmpeg — timecode.c (primary implementation)

    Первичная реализация различает номинал, рациональную частоту, DF-пропуски и флаг перехода суток. Наш независимый учебный helper имеет более узкий явный scope: две DF-частоты, строгие метки и запрет автоматического wrap.

KEEP BUILDING

Продолжайте с конкретной задачей.

API / Timeline

Как переместить playhead через Python API

SetCurrentTimecode без запасных методов: план перемещения, offset от начала, проверка проекта и таймлайна, явный --apply и сверка результата. Поддержка NDF/DF в пределах выбранного timebase.

API / Timeline

Как получить стартовый кадр таймлайна

GetStartFrame: чтение начала таймлайна, перевод абсолютной координаты в offset и независимая проверка стартовой метки. Без GetStart на другом объекте, усечения float и fallback 0.

API / Timeline

Как прочитать FPS таймлайна через API

Читаем timelineFrameRate у Timeline и Project раздельно, сохраняем исходную строку, разбираем дробные значения и пометку DF. Не подставляем частоту при ошибке и не угадываем временную базу.

Вернуться в библиотеку API →MCP и AI-ассистенты →