RESOLVE API / TIMECODE LAB / 04

Read the timeline start frame with Python

The API origin is an observation, not an assumed zero.

GetStartFrame: read the timeline origin, convert an absolute coordinate to an offset and retain the independent start label. No GetStart fallback, float truncation or default zero.

THE RESULT

Target IDs, the observed start coordinate and an explicitly calculated offset.

PYTHON / RESOLVESTUDY MODEL
01base = timeline.GetStartFrame()

Zero is accepted only as an actual integer API response. Read failure never produces zero.

CHECK

An int, not bool.

Switching steps changes the explanation. Nothing connects to Resolve or runs on your computer.

01 / BEFORE THE CODE

Set the conditions before you run.

01

Choose the execution context first

Use Python 3.8+ and the configured Resolve Studio connection from S01. Keep the main file, resolve_api_timecode.py and resolve_api_connection.py together. The official module must come from your installed Resolve.

02

Exact rate and units

This helper supports 24, 25, 30, 48, 50, 60 and 24000/1001, 30000/1001, 48000/1001, 60000/1001. This is the example scope, not Resolve’s capability list. Select the rational rate explicitly; 29.97 is not silently rewritten as 30000/1001.

03

Keep the exercise bounded

Labels are limited to one timecode day, whole frames and explicit DF/NDF selection. No inferred midnight wrapping, negative durations, interlaced fields or audio subframes. Stop playback for application reads; use a test copy for seeking and do not change targets during the call.

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.

TimelineGetStartFrame()
Input
No arguments.
Returns
int — timeline start coordinate.
Check
Check the type; no int(None) or int(float).
TimelineGetEndFrame()
Input
No arguments.
Returns
int — end coordinate.
Check
Do not transfer boundary conventions to rendering.
TimelineGetStartTimecode()
Input
No arguments.
Returns
Start-label string.
Check
Keep separate; never replace base.
Project / TimelineGetUniqueId()
Input
Current object.
Returns
Non-empty ID string.
Check
Recheck the target after reading.

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

Timeline.GetStartFrame and TimelineItem.GetStart describe different levels

GetStartFrame belongs to Timeline and describes the sequence origin. TimelineItem.GetStart describes one clip occurrence’s boundary. Even the first clip may begin after timeline start because of a gap, another track type or a different arrangement of material.

The previous helper tried GetStart on the same object after GetStartFrame failed, then substituted 0. That sequence concealed an unavailable contract. The reader now gets either a valid start_frame or an explicit read failure. An empty timeline still has an origin; it is not the minimum of clip positions.[3]

04 / UNDERSTAND

Two simple equations without another FPS multiplication

When absolute and base are already frames on the same axis, offset = absolute − base. Conversely, absolute = base + offset. With base 86400, coordinate 86640 lies 240 frames after start. Multiplying by FPS again is wrong because both quantities are already frames.

The read-only report preserves a negative offset to show that the supplied coordinate precedes the origin. This is not a seek request. inside_conservative_bounds separately reports membership of this example’s [start, end) guard; the neighbouring setter rejects negative offsets.

05 / UNDERSTAND

Keep the API coordinate and start label independent

The report keeps start_frame, start_timecode_raw and fps_raw separate. A count parsed from the label may be compared to the API coordinate, but the example does not automatically equate them for every format. Investigate any difference using the specific timebase and application version.

read_clock therefore performs no rounding or correction. Reading GetStartFrame does not even require converting a 29.97 label to a rational rate. An exact rate is needed later for seconds or TC formatting; observing the original coordinate must not depend on guessed FPS.[3]

06 / UNDERSTAND

Change only the base coordinate

Keep 01:00:00:00, 24 NDF and offset 240 in the calculator. Change the teaching base from 86400 to 0: the absolute coordinate changes from 86640 to 240, but the target label stays 01:00:10:00. The calculator illustrates chosen inputs, not every valid Resolve configuration.

In a real workflow, obtain base through GetStartFrame rather than copying it from this exercise. The model exposes a double-added origin without becoming a source of project settings.

TIME / COORDINATE LABLocal teaching data · not Resolve
Load a complete example:
OFFSET240frames from start
API COORDINATE86640base + offset
TIMECODE01:00:10:0024 · NDF

Whole-frame conversion: no rounding.

The base coordinate and start label are independent anchors, not values inferred from each other. This calculator supports one 24-hour timecode day, whole-frame labels and explicitly listed rates. It does not validate your project's settings or timeline bounds. Input is not sent or saved.

07 / UNDERSTAND

What repeated reading establishes

The example reads target IDs, start/end coordinates, start label and FPS text twice. It checks the current project/timeline around the field reads. A mismatch stops the report rather than mixing two sequences into one apparently authoritative result.

Matching observations are not a transaction: state could change and return between them. There is also no timeout for a blocked native call. Save your work, stop playback and avoid switching targets while inspecting. The report does not change the origin or save the project.

PYTHON / COMPLETE EXAMPLE

Read the code. Then run it deliberately.

Three files for the application workflow

Download timeline_base_frame.py and resolve_api_timecode.py. For application reads or a seek, also download resolve_api_connection.py. The bundle contains all six examples, both helpers and RU/EN instructions. Pure converters never import the Blackmagic module; seconds conversion connects only with --timeline.

Download all examples + instructions (.zip) ↓
timeline_base_frame.py
"""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 — exact arithmetic, origins and one guarded seek

Author implementation, not the Blackmagic SDK. DF/NDF label rules are separate from elapsed seconds. Clock accepts an explicit exact rate; raw FPS labels do not automatically select one. Only move_playhead with apply can call a setter, and only once.

resolve_api_timecode.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)

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.

resolve_api_connection.py
"""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

Read the origin

Read the origin
python timeline_base_frame.py

No changes. Requires an open test project and timeline.

Calculate an offset from a known API coordinate

Calculate an offset from a known API coordinate
python timeline_base_frame.py --absolute 86640

86640 is teaching input. Use an observed coordinate for your actual target.

What the code does

  1. Obtain the target

    An open project/timeline is required; IDs replace assumptions based on names.

  2. Read an integer

    GetStartFrame is validated without truncation or another-method fallback.

  3. Compare observations

    Both observations must agree on IDs and clock; this does not lock the interface.

  4. Subtract the base

    Optional --absolute adds an offset and bounds check without moving anything.

Boundaries to keep

All helper conversions are author functions, not Blackmagic methods. A successful calculation does not validate project settings, frame rate or a call in your installed Resolve.

An error never reuses a previous result. Native calls have no added timeout; two matching reads are not an atomic snapshot and do not lock the interface.

DEBUG / OBSERVATION FIRST

An error is a starting point, not a diagnosis.

GetStartFrame failed

Possible reasonThe documented method is unavailable or the connection was lost.

Next checkCheck the installed README and connection; do not fall back to GetStart.

start_frame must be an integer

Possible reasonThe response was None, bool, float or text.

Next checkInspect the actual type instead of coercing with int(...).

Negative offset

Possible reasonThe supplied coordinate precedes the observed start.

Next checkCheck the units and origin of absolute; do not clamp it to zero.

Clock observations differ

Possible reasonTimeline identity or clock values changed while reading.

Next checkStop concurrent actions and read afresh without merging old values.

PRACTICE / YOUR OWN EVIDENCE

Test a gap before the first clip

  1. Leave a gap before the first clip in a test timeline.
  2. Read start_frame and the clip position separately using S03.
  3. Check absolute = base + offset and the sign of an offset before start.
ACCEPT WHEN

The origin is not replaced by the first clip; equations agree; a read failure never returns 0.

This is a checklist for your test, not a report of a test already performed on the website.

FAQ

Questions that matter in practice.

Is the start always 86400?

No. That is only a familiar one-hour example at 24 NDF. Read the value from the actual target.

Should I find the first clip?

No. Sequence start and the first clip’s start can differ, for example when a gap precedes the first item. Read GetStartFrame for the shared origin instead of taking the earliest enumerated clip.[3]

Is FPS unnecessary everywhere?

For subtracting two frame coordinates, yes. Seconds, timecode and mappings between timebases need an exact rate.

Why also read the start timecode?

To preserve both anchors for inspection without substituting one for the other. Base is not calculated from that string.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

KEEP BUILDING

Continue with a precise next task.

API / Timeline

Read the timeline start timecode in Python

GetStartTimecode without guessing a timelineStartTimecode setting: preserve the label, FPS and start_frame separately. Optional strict NDF/DF parsing with an explicit rate and no origin changes.

API / Timeline

Convert seconds to timeline frames in Python

Seconds to offset to API coordinate: Fraction, 24000/1001 and 30000/1001, exact/floor/ceil/nearest, rounding error and optional anchoring to an open timeline. Negative values are never clamped to zero.

API / Timeline

Read timeline FPS through the API

Read timelineFrameRate from Timeline and Project independently, preserve the original string and handle fractional labels and DF. Never substitute a rate after failure or guess the underlying timebase.

Back to the API library →MCP and AI assistants →