video-frames returned a null url: the job succeeded, one frame failed

If one instant fails to extract, video-frames sets that frame's url to null and the job still completes. How to detect it and retry only that time.

5 min readSume
All posts

A Sume video frames job can finish as ready and still contain a frame with url: null. The docs say that when extraction fails for one instant, that frame's url is null and the job does not fail. So a status check of ready is not enough: loop over frames[] and look at each url before you build a contact sheet, a thumbnail set or an upload.

What the result looks like

The read route is GET /v1/video-frames/:id. When resource_status is ready, frames is a list of { t, url, width, height } entries where each URL is a durable artf_ image on media.sume.com. source_duration_seconds is the duration the worker probed. Submits always return 202, because this route pins async mode, so you poll.

Why a frame can be missing

The docs do not list a cause, so treat the null as a signal to retry and not as a diagnosis. Two things are documented. A timestamp outside [0, duration) fails the whole job with frame_time_out_of_range, so a null is a different case from a bad time. And on sources longer than 90 seconds the job can add a low_confidence_long_video warning, so read warnings[] when you see gaps on long clips.

Detect and retry

The snippet below reads a finished extract with the standard library, lists the missing times, and prints them so you can request just those with a new at[] (keep each request to 24 values or fewer, and send a fresh Idempotency-Key). It reads the key from the environment.

import json, os, urllib.request


def read_frames(extract_id):
    req = urllib.request.Request(
        f"https://api.sume.com/v1/video-frames/{extract_id}",
        headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.load(resp)


def missing_times(body):
    frames = body.get("frames") or body.get("video_frames", {}).get("frames", [])
    return [f["t"] for f in frames if not f.get("url")]


if __name__ == "__main__":
    body = read_frames(os.environ["EXTRACT_ID"])
    print("missing:", missing_times(body))

Where this matters

Contact sheets are the obvious case. A grid built from 24 stills with one hole looks like a rendering error, and a thumbnail picker that sorts by t can end up offering an empty slot. For a character-drift review with fps, a null at the one frame where the drift happens defeats the check. Build the tile list from non-null entries, log the missing times, and request them again. The extract is billed by its Modal compute, with a reserved ceiling that the bill never exceeds, so a retry of a few frames is a small job.

Frames keep the source size unless you send max_edge (16 to 2160), and the source clip must be 300 seconds or shorter.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume