Drop-frame timecode, explained: 29.97 and 59.94 fps

9eye guides ·

Timecode gives every frame of video a label: hours, minutes, seconds and frames, HH:MM:SS:FF. At 25 frames per second the labels and the clock agree — after one hour of video the timecode reads exactly one hour. At 29.97 frames per second they do not, and over a broadcast day the difference grows to almost a minute and a half.

Drop-frame timecode is the fix. It skips some frame numbers — never frames of picture — so that the timecode keeps up with the clock. This guide explains why it is needed, exactly which numbers are skipped, and how to convert between frame counts and drop-frame timecode.

Why 29.97 fps drifts

The frame rates used in North America, Japan and other former NTSC regions are not whole numbers. “29.97” is exactly 30000/1001 frames per second, and “59.94” is 60000/1001: both run 0.1% slower than 30 and 60.

Timecode, though, counts 30 labels per second. In one real hour, 29.97 fps video has 107,892 frames, while 30 labels a second would need 108,000. Counted the plain way — non-drop-frame — the timecode reads 00:59:56:12 when the clock says one hour has passed: 3.6 seconds behind, and 86.4 seconds behind after a full day.

For a film edit that may not matter. For a broadcast schedule, where the news must start at 19:00:00 by the clock, it does.

Which frame numbers are skipped

At 29.97 fps, drop-frame timecode skips the frame numbers ;00 and ;01 at the start of every minute, except every tenth minute (minutes 00, 10, 20, 30, 40 and 50).

  • 00:00:59;29 is followed by 00:01:00;02 — the labels ;00 and ;01 of minute one do not exist.
  • 00:09:59;29 is followed by 00:10:00;00 — minute ten keeps all its numbers.

That is 2 numbers in 54 minutes of every hour: 108 numbers, exactly the 108 frames the plain count was ahead. After one real hour, drop-frame timecode reads 01:00:00;00.

At 59.94 fps the rule is the same with four numbers: ;00 to ;03 are skipped at the start of every minute except every tenth.

What is left over is tiny: because 29.97 is really 30000/1001, drop-frame timecode is still about 2.6 frames (0.09 seconds) off the clock after 24 hours. Systems that follow the time of day usually correct it once a day, by re-syncing timecode to the clock.

How to recognise drop-frame timecode

By convention the last separator changes: a semicolon (or, in some systems, a full stop) before the frames means drop-frame, colons throughout mean non-drop-frame.

  • 01:00:00;00 — drop-frame
  • 01:00:00:00 — non-drop-frame

The separator is only a convention, so when two systems exchange times, agree explicitly which one each side uses.

Frame rates and timecode at a glance

Frame rateExact rateDrop-framePlain count after one hour
24, 25, 30, 50Whole numbersNot neededMatches the clock
23.97624000/1001Not defined — always counted plain3.6 s behind
29.9730000/1001Skips ;00 and ;013.6 s behind
59.9460000/1001Skips ;00 to ;033.6 s behind

Converting between frames and drop-frame timecode

The safe way to work with time is to count frames and turn them into timecode only for display. These two functions do the conversion; set FPS to 60 and DROP to 4 for 59.94 fps.

// Drop-frame timecode <-> frame count. 29.97 fps: FPS 30, DROP 2. 59.94 fps: FPS 60, DROP 4.
const FPS = 30;                              // labels per second
const DROP = 2;                              // numbers skipped at the start of most minutes
const PER_MIN = FPS * 60 - DROP;             // frames in a minute that skips: 1798
const PER_10_MIN = FPS * 600 - 9 * DROP;     // frames in ten minutes: 17982
const pad = (n: number) => String(n).padStart(2, '0');

function toTimecode(frames: number): string {
  const tens = Math.floor(frames / PER_10_MIN);
  const rest = frames % PER_10_MIN;
  const skipped = 9 * DROP * tens + (rest >= DROP ? DROP * Math.floor((rest - DROP) / PER_MIN) : 0);
  const n = frames + skipped;                // the label, counted as if the rate were FPS
  const ff = n % FPS;
  const ss = Math.floor(n / FPS) % 60;
  const mm = Math.floor(n / (FPS * 60)) % 60;
  const hh = Math.floor(n / (FPS * 3600));
  return `${pad(hh)}:${pad(mm)}:${pad(ss)};${pad(ff)}`;
}

function toFrames(hh: number, mm: number, ss: number, ff: number): number {
  const minutes = 60 * hh + mm;
  return (3600 * hh + 60 * mm + ss) * FPS + ff - DROP * (minutes - Math.floor(minutes / 10));
}

toTimecode(1800);       // "00:01:00;02"
toTimecode(17982);      // "00:10:00;00"
toFrames(1, 0, 0, 0);   // 107892

From timecode to frames: count every label as if the rate were 30, then subtract the two skipped numbers for each minute that is not a multiple of ten. From frames to timecode: add the skipped numbers back, then split the result into hours, minutes, seconds and frames.

What this means for a broadcast schedule

  • Store frames, not strings. Durations add up exactly as frame counts. Adding timecode strings at 29.97 goes wrong as soon as a minute boundary is crossed.
  • One frame rate per channel. A 25 fps channel and a 29.97 fps channel can share a catalog, but a 30-second commercial is 750 frames on one and 900 (00:00:30;00) on the other — each channel needs its own count.
  • Agree with playout. If traffic counts drop-frame and the automation does not, every time in the day drifts by up to 3.6 seconds an hour, and the as-run reconciliation fills with false differences.
  • Check exchange formats. When schedules travel as BXF or in an automation format, confirm how each side writes timecode and whether times are local or UTC.

Timecode in 9eye

9eye keeps every time as a whole number of frames at the channel’s own frame rate — 23.976, 24, 25, 29.97 drop-frame, 30, 50 or 59.94 drop-frame — and shows timecode only on screen. Each channel also has its own time zone, broadcast day start and first weekday, so a 29.97 fps feed in one region and a 25 fps feed in another run side by side without conversion errors. See the platform, or request a demo on a channel like yours.

More guides

See it on a channel like yours.

9eye is broadcast scheduling and traffic software with AI agents built in. Early access starts with a demo.

Request a demo