RESOLVE API / TIMECODE LAB / 01

Convert frames to timecode in Python

Choose the origin before formatting the frame.

Frames to HH:MM:SS:FF or HH:MM:SS;FF with an explicit rate, NDF/DF mode, timeline origin and round-trip check. Complete Python example and an interactive calculator without a Resolve connection.

THE RESULT

A validated timecode label for a whole-frame offset from an explicit start.

PYTHON / TIMECODESTUDY MODEL
01start = "01:00:00:00"

Offset zero means timeline start, not necessarily 00:00:00:00. An API coordinate is not substituted here.

CHECK

The start label is explicit.

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

The mathematical example runs in Python 3.8+ without Resolve or third-party packages. Put the main file beside resolve_api_timecode.py. The connection helper is not needed.

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.

Author helperClock(rate, mode)
Input
Exact rate text and ndf/df.
Returns
Validated rate/nominal object.
Check
Ambiguous 29.97 is rejected.
Author helperframes_to_timecode(frames, clock)
Input
Whole frames from TC zero.
Returns
NDF or DF label.
Check
Not an API coordinate or seconds.
Author helperoffset_to_timecode(offset, start, clock)
Input
Offset from the specified start.
Returns
Target label within the same day.
Check
Parse the origin before addition.
Author helpertimecode_to_offset(tc, start, clock)
Input
Result label and the same origin.
Returns
Original integer offset.
Check
Check the round trip.

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

Three different values must not all be called frame

An API timeline coordinate, an offset from timeline start and a count in timecode numbering are different quantities. With GetStartFrame = 86400 and an offset of 240, the API coordinate is 86640. Starting at 01:00:00:00 in 24 NDF gives 01:00:10:00. Passing 86640 as the offset would add the origin twice.

The function therefore takes start_timecode and offset separately. Its lower-level frames_to_timecode counts from the zero timecode label; the executable example converts an offset from your chosen start. Source timecode belongs to the source: edits and retiming can break any assumed correspondence with timeline coordinates.[3]

04 / UNDERSTAND

The numbering base is not the physical rate

At 24 NDF, FF uses labels 00–23. At 24000/1001 NDF it uses the same labels, but frames arrive slightly less often. Consequently 2400 frames have label 00:01:40:00 in both cases, while elapsed durations are 100 and 100.1 seconds respectively. Formatting a label and calculating duration are different operations.

The old helper rounded any FPS and frame input and raised invalid rates to at least one. The replacement accepts only explicitly listed timebases and whole offsets. Fraction preserves the chosen rational value. A 23.976 display label is not proof of 24000/1001 and is not automatically rewritten.[9]

05 / UNDERSTAND

DF skips frame numbers, not frames

At 30000/1001 DF, labels ;00 and ;01 at second zero are skipped at every minute except each tenth minute. The sequence is 00:00:59;29 → 00:01:00;02. At 60000/1001 DF, four numbers are skipped: 00:00:59;59 is followed by 00:01:00;04. No video frames are removed between them.

A ten-minute block of 29.97 DF contains 17982 frames and ends at the next block’s label 00:10:00;00. At 59.94 DF the counts double. The example processes complete blocks and remaining minutes with integers. It does not support 23.976 DF; a two-label rule cannot be extended to an arbitrary frame rate.[12]

06 / UNDERSTAND

Try an origin and a minute boundary

In the 24 NDF example, change only the API base while preserving the start label and offset. The API coordinate changes but the timecode does not. This is intentional: the calculator does not assume the two anchors are equal and shows what gets added in each coordinate system.

Load the DF boundary preset and change 1800 to 1799. The labels should change between 00:00:59;29 and 00:01:00;02. Then try the inverse direction. These are teaching inputs; the calculator does not check whether they match your currently open project.

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

Why this example rejects midnight wrapping and fractional FF

A four-field label contains no day index. The label 00:00:05:00 after 23:59:59:23 might belong to the next day or the start of the same scale. Without another field that choice is ambiguous. This example stops at the day boundary rather than hiding it with modulo arithmetic.

FF represents a whole frame. Audio subframes and methods using subframe_precision need their own contract; the formatter must not round them silently. Choose the quantization policy first. The S04 seconds article demonstrates that decision for elapsed durations.[3]

PYTHON / COMPLETE EXAMPLE

Read the code. Then run it deliberately.

Two files; no Resolve connection

Download frames_to_timecode.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) ↓
frames_to_timecode.py
"""Whole-frame offset from an explicit start label -> timecode. No Resolve required."""
import argparse
import json
from resolve_api_timecode import Clock, offset_to_timecode, timecode_to_offset


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("offset", type=int, help="Whole frames from start, not absolute API coordinates")
    p.add_argument("--start", required=True, help="HH:MM:SS:FF or HH:MM:SS;FF")
    p.add_argument("--rate", required=True, help="24 or e.g. 30000/1001; no 29.97 alias")
    p.add_argument("--mode", choices=("ndf", "df"), required=True)
    a = p.parse_args()
    clock = Clock(a.rate, a.mode)
    tc = offset_to_timecode(a.offset, a.start, clock)
    print(json.dumps({"offset_frames": a.offset, "start_timecode": a.start, "timecode": tc,
                      "rate": str(clock.rate), "mode": clock.mode,
                      "round_trip_offset": timecode_to_offset(tc, a.start, clock)}, 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)

Launch commands

240 frames after a one-hour origin

240 frames after a one-hour origin
python frames_to_timecode.py 240 --start "01:00:00:00" --rate 24 --mode ndf

Expected: 01:00:10:00 and round-trip offset 240.

First non-tenth DF minute

First non-tenth DF minute
python frames_to_timecode.py 1800 --start "00:00:00;00" --rate 30000/1001 --mode df

Expected: 00:01:00;02, without deleting frames.

What the code does

  1. Parse explicit inputs

    argparse accepts the integer offset, origin, exact rate and mode. No project-dependent defaults.

  2. Validate Clock

    The selected rate and DF support are validated. Nominal FPS is not guessed by rounding arbitrary input.

  3. Add the offset

    The start label is parsed before adding actual whole frames and formatting the result.

  4. Check the inverse

    JSON includes round_trip_offset. Agreement checks this conversion, not API connectivity.

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.

ClockError: ambiguous rate

Possible reasonA short decimal label or unlisted rate was supplied.

Next checkEstablish the exact task rate; do not automatically choose the nearest.

Separator error

Possible reasonDF was selected with a colon start label, or vice versa.

Next checkVerify both mode and label; do not blindly replace punctuation.

Frame count outside one day

Possible reasonOffset plus origin exceeded 24 hours or became negative.

Next checkDefine a separate day-index contract outside this helper.

Unexpectedly large label

Possible reasonAn absolute API coordinate was used as an offset.

Next checkSubtract the observed GetStartFrame once before conversion.

PRACTICE / YOUR OWN EVIDENCE

Build boundary cases, not one happy-path example

  1. Check 0, 23, 24 and 240 frames at 24 NDF.
  2. Check 1799, 1800 and 17982 at 30000/1001 DF.
  3. Compare inverse offsets and deliberately submit a fractional frame.
ACCEPT WHEN

Boundary labels are correct, inverse counts agree, and fractional frames/day wrapping are rejected.

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

FAQ

Questions that matter in practice.

Why not divide frames by 29.97?

That produces approximate duration, not the proper label sequence. Seconds require the exact rate; TC also needs a nominal base and DF/NDF numbering.[12]

Does this work without Resolve?

Yes. Two files perform pure arithmetic on input values; the Blackmagic module is not imported.

Can I pass 240.5 frames?

No: this formatter does not choose how a subframe position should be quantized.

What does the semicolon mean?

In this example’s convention it marks DF. The character does not enable DF in Resolve; inspect the application setting separately.[12]

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.

  5. Python — fractions.Fraction

    Exact representation of numeric text. A decimal 23.976 label does not establish a 24000/1001 source timebase; the raw string is retained.

  6. FFmpeg — timecode.c (primary implementation)

    The primary implementation distinguishes nominal rate, rational timebase, DF skipped labels and day-wrapping flags. Our independently written tutorial helper has a narrower explicit scope: two DF rates, strict labels and no automatic wrapping.

KEEP BUILDING

Continue with a precise next task.

API / Timeline

Convert timecode to frames in Python

Strict NDF/DF parsing: valid fields, skipped labels, actual whole frames and offsets relative to timeline start. No float arithmetic, zero fallback or inferred midnight wrap.

API / Timeline

Move the Resolve playhead with Python

SetCurrentTimecode without fallback methods: plan the seek, anchor the offset, verify project and timeline, require --apply and read back the result. NDF/DF within the selected timebase.

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 →