grab still at markers in Davinci Resolve

Quite often I need to get all stills for timeline markers. Resolve doesn’t provide this functionality directly. There is a way to cut at markers and then get stills for the first frame of each clip but this will also grab the actual first frames of the clips before they were cut. The good thing is resolve comes with python API that makes this process almost easy.

python docs are available in the installed Resolve (on windows) here: c:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting\
def grabStillsAtMarkers():
    resolve = dvr_script.scriptapp("Resolve")
    project = resolve.GetProjectManager().GetCurrentProject()
    timeline = project.GetCurrentTimeline()
    timeline_start = timeline.GetStartTimecode()
    markers = timeline.GetMarkers()
    fps = getFps()

    for marker in markers:
        timecode = frameToTimecode(marker, 0, timeline_start, fps)
        timeline.SetCurrentTimecode(timecode)
        timeline.GrabStill()

this is more or less what needs to be done. There are a few gotchas in the API unfortunately that make this a bit more painful.

The first problem is getting frame rate as float. As far as I know there is no way to do it and I ended up with a pretty junk way. Resolver returns string that is just the decimal part of the frame rate. For instance it gives us 29 for 29.97

def getFps():
    resolve_frame_rate: str = timeline.GetSetting("timelineFrameRate")
    if resolve_frame_rate == "29":
        return 29.97
    #TODO: extend for other fractional frame rates
    return float(resolve_frame_rate)

The next problem I faced is that setting timeline timecode is not reliable. In many cases I ended up with duplicated stills as the timeline wasn’t updated between the captures. The updated code checks if the timecode matches what we need:

def setTimecode(timeline, timecode):
    timeline.SetCurrentTimecode(timecode)

    while timeline.GetCurrentTimecode() != timecode:
        print(f"waiting for: {timecode}, current: {timeline.GetCurrentTimecode()}")
        timeline.SetCurrentTimecode(timecode)
        time.sleep(.1)

This it no longer pretty and the retry should probably be moved to a decorator.

I couldn’t find any timecode support in resolve API to convert frames (markers) to timecode (timeline) so more code to write. In the end the full script is much longer than I expected:

import time

import DaVinciResolveScript as dvr_script
from math import floor


def timecodeToSeconds(timecode, fps):
    bits = timecode.replace(";", ":").split(':')
    if len(bits) != 4:
        raise ValueError("expected timecode as '00:00:00;00'")

    frames = bits[-1]
    return sum([a * int(b) for a, b in zip((3600, 60, 1), bits[:-1])]) + int(frames) / float(fps)


def secondsToTimecode(seconds, fps):
    if seconds == 0:
        # in nuke divmod(0.0,3600) returns (nan, nan)
        return "{0:02d}:{1:02d}:{2:02d};{3:02d}".format(0, 0, 0, 0)

    h, rest = divmod(seconds, 3600)
    m, s = divmod(rest, 60)

    seconds = int(floor(s))
    fraction = s - seconds
    frame = int(round(fraction * fps))

    #correct for rounding errors for 29.97 fps 01:42:08;30 should be 01:42:09;00
    if frame >= fps:
        frame = 0
        seconds += 1
    if seconds >= 60:
        seconds = 0
        m += 1
    if m >= 60:
        m = 0
        h += 1

    return "{0:02d}:{1:02d}:{2:02d};{3:02d}".format(int(h), int(m), seconds, frame)


def frameToTimecode(frame, start_frame, start_timecode, fps):
    f = float(frame) - float(start_frame)
    frame_offset = f / float(fps)

    seconds = timecodeToSeconds(start_timecode, fps) + frame_offset

    return secondsToTimecode(seconds, fps)


def getFps():
    resolve_frame_rate: str = timeline.GetSetting("timelineFrameRate")
    if resolve_frame_rate == "29":
        return 29.97
    return float(resolve_frame_rate)

def setTimecode(timeline, timecode):
    timeline.SetCurrentTimecode(timecode)

    while timeline.GetCurrentTimecode() != timecode:
        print(f"waiting for: {timecode}, current: {timeline.GetCurrentTimecode()}")
        timeline.SetCurrentTimecode(timecode)
        time.sleep(.1)
        

def grabStillsAtMarkers():
    resolve = dvr_script.scriptapp("Resolve")
    project = resolve.GetProjectManager().GetCurrentProject()
    timeline = project.GetCurrentTimeline()
    timeline_start = timeline.GetStartTimecode()
    markers = timeline.GetMarkers()
    fps = getFps()

    for marker in markers:
        print("-----------------------------------------------")
        print(f"marker: {marker}")
        timecode = frameToTimecode(marker, 0, timeline_start, fps)
        print(f"marker timecode: {timecode}")
        setTimecode(timeline, timecode)

        if timeline.GetCurrentTimecode() == timecode:
            print(f"grabbing still for: {timeline.GetCurrentTimecode()}")
            timeline.GrabStill()

grabStillsAtMarkers()