The method, for a non-drop-frame rate
Given a frame number n (a non-negative integer) and a frame rate
fps (a whole number — 24, 25, 30, 50 or 60, or the rounded nominal 30/60
used for 29.97/59.94 non-drop-frame), the conversion is four steps of division and
remainder:
FF = n mod fps
totalSeconds = floor(n / fps)
SS = totalSeconds mod 60
totalMinutes = floor(totalSeconds / 60)
MM = totalMinutes mod 60
HH = floor(totalMinutes / 60)
Each line peels off one field and carries the remainder up to the next. FF
is simply how far into the current second the frame sits; dividing that away and taking the
remainder mod 60 gives the seconds field within the current minute; the same pattern
repeats for minutes within the hour. There is no drop-frame consideration in this version at
all — it is the same division you would use to convert a count of seconds into
hours, minutes and seconds, one level deeper.
The method, for a drop-frame rate
At 29.97 or 59.94 fps drop-frame, the frame number is not simply the nominal count, because some frame numbers were skipped on the way there. The conversion needs an extra step that adds the skipped count back in before the division above applies cleanly:
framesPerMinute = nominal × 60
framesPer10Minutes = framesPerMinute × 10 − dropFrames × 9
d = floor(n / framesPer10Minutes)
within = n mod framesPer10Minutes
if within < dropFrames:
n' = n + dropFrames × 9 × d
else:
n' = n + dropFrames × 9 × d
+ dropFrames × floor((within − dropFrames) / (framesPerMinute − dropFrames))
# then apply the non-drop-frame method above to n', using nominal as fps
framesPer10Minutes is how many actual frames occur in ten real minutes at
the drop-frame rate — 17,982 at 29.97, 35,964 at 59.94, both smaller than the
nominal 18,000/36,000 because of the dropped numbers. d counts how many
complete ten-minute blocks the frame number has passed; within is the position
inside the current block. The conditional handles the one wrinkle: frame numbers that fall
in the very first two (or four) positions of a block need only the whole-block correction,
because they have not yet crossed into a non-exempt minute within that block.
Frame field range by rate
| Rate | Frames field (FF) range | Frames per nominal second |
|---|---|---|
| 23.976 | 00–23 | 24 |
| 24 | 00–23 | 24 |
| 25 | 00–24 | 25 |
| 29.97 NDF / DF | 00–29 | 30 |
| 30 | 00–29 | 30 |
| 50 | 00–49 | 50 |
| 59.94 NDF / DF | 00–59 | 60 |
| 60 | 00–59 | 60 |
The field range is always 00 through nominal − 1, where
nominal is the rounded frame rate — 24 for 23.976, 30 for 29.97 (either
labelling), 60 for 59.94 (either labelling). Drop-frame changes which frame numbers
appear at the start of certain minutes; it never changes this range.
Implementation mistakes worth checking for
If you are writing your own frame-to-timecode conversion rather than using a tested one, these are the specific failure modes that show up most often in practice, roughly in order of how often they actually bite:
- Off-by-one on the frames field boundary. Using
n % fpsis correct; usingn % (fps - 1)or a hand-written loop with a wrong termination condition silently shifts every frame number by one once you cross a rollover, which then looks like a one-frame sync error everywhere downstream. - Truncating instead of flooring on negative input. In several languages, integer division of a negative number truncates toward zero rather than flooring toward negative infinity, which changes the result of the modulo operation for negative frame numbers. A subtraction that legitimately produces a negative result needs floor-based division specifically, or the seconds/minutes/hours fields come out wrong by one in the direction away from zero.
- Treating a drop-frame frame number as if it were nominal. Applying the plain non-drop-frame formula directly to a number that was actually produced under a drop-frame count gives a timecode that is systematically wrong by a growing number of frames the further you get from the start — exactly the kind of bug that looks fine in a quick test near frame zero and only shows up later.
- Losing precision on very large frame counts. Frame numbers stay well within safe integer range for any realistic duration in every language's native integer or double type, but code that converts through a 32-bit integer type, or through a floating-point type after heavy accumulated arithmetic, can silently lose the low bits on an unusually long timecode. Using plain 64-bit or arbitrary-precision integers throughout avoids this entirely.
- Assuming the frames field is always two digits. True for every rate in current professional use (see the table above), but code that hard-codes a two-character pad without checking the rate will silently truncate if it is ever pointed at a rate outside this common set.
A dimensional sanity check worth doing by hand
Before trusting any implementation of the above — yours or this tool’s —
it is worth checking that the units actually work out, because a transposed division is an
easy mistake to make and an easy one to miss by eye. n mod fps must produce a
value from 0 to fps − 1: check that the maximum, fps − 1,
is what your code actually reports for n = fps − 1, not fps
itself (an off-by-one here is the single most common bug in hand-rolled timecode code).
Then check that n = fps itself rolls over to 00:00:01:00, not
00:00:00: followed by the rate value. Both checks take seconds and catch the
most common class of implementation error before it reaches real footage.
Worked examples at every rate
1. Frame 3,000 at three different rates
The same frame count, three different answers, because the rate changes how much real time it represents:
24 fps frame 3000 → 00:02:05:00
25 fps frame 3000 → 00:02:00:00
30 fps frame 3000 → 00:01:40:00
At 25 fps, 3,000 frames is exactly two minutes (25 × 120 = 3,000) — a clean
result that only happens because 25 divides 3,000 evenly at that particular multiple.
Try frame 3,001 at 25 fps and the result is 00:02:00:01, no longer clean,
which is the more typical case.
2. The rollover at the top of the frames field
At 30 fps, frame 29 is 00:00:00:29 — the last frame of the first
second. Frame 30 rolls the frames field back to zero and carries into seconds:
00:00:01:00. Enter frame 30 at 24 fps instead and the frames
field only reaches 23 before rolling over, so frame 30 is already
00:00:01:06 — one full second plus six frames, because fewer frames
make up each second at the lower rate.
3. A large frame number, to check the hour carry
At 30 fps, frame 90,000 is 00:50:00:00 (90,000 ÷
30 = 3,000 seconds = 50 minutes exactly). Frame 108,000 is
01:00:00:00 — the hour field increments correctly once the minute field
would otherwise reach 60. Try it above and confirm the frame number typed back in from
that timecode returns exactly 108,000.
4. The drop-frame correction, worked through the formula above
At 29.97 DF, take frame 18,000. framesPer10Minutes is
17,982, so d = floor(18000 / 17982) = 1 and
within = 18000 mod 17982 = 18. Since 18 is not less than
dropFrames = 2, the second branch applies:
n' = 18000 + 2×9×1 + 2×floor((18−2)/(1800−2))
= 18000 + 18 + 2×floor(16/1798) = 18018 + 0 = 18018. Dividing 18,018 by the
nominal 30 gives 600.6 seconds — 10 minutes and 0.6 seconds, i.e.
00:10:00;18. Enter frame 18,000 at 29.97 fps DF into the converter above and
confirm that exact result.
5. Negative results from a subtraction
On the Arithmetic tab, at any rate, set Timecode A to 00:00:10:00, choose
Subtract, and enter a frame count of 500 — more frames than exist
between the start and Timecode A. The result is reported with a leading minus sign on the
hours field rather than silently wrapping into a large, meaningless positive timecode,
which is exactly the failure mode a naive modulo-based implementation produces on negative
input in several common programming languages.
Frequently asked questions
What formula converts a frame number to HH:MM:SS:FF?
For non-drop-frame rates: FF = frame mod fps; total seconds =
floor(frame / fps); SS = total seconds mod 60; total minutes =
floor(total seconds / 60); MM = total minutes mod 60;
HH = floor(total minutes / 60).
For drop-frame rates (29.97 and 59.94), the frame number is first adjusted upward to account for the skipped frame numbers before applying the same division, because a drop-frame frame number is not simply the nominal count — see the worked formula above.
Why does frame 0 correspond to 00:00:00:00?
By convention, frame numbering starts at zero, matching how the hours/minutes/seconds/frames
fields all start at zero at the very beginning of a piece of timecode. Some systems offer a
configurable start timecode (01:00:00:00 is a common editorial convention to
keep all timecodes positive and avoid an all-zero first frame), but the underlying frame
count driving that display still starts at 0 relative to whatever the start timecode
is.
How many digits does the frames field use?
Two, for every standard rate — 00 through 23 at 24 fps,
00 through 29 at 30 fps, up to 00 through
59 at 60 fps. No standard frame rate in professional use needs a third
digit.
Does the same frame count mean the same timecode at every rate?
No — frame 3,000 is 00:02:05:00 at 24 fps but
00:01:40:00 at 30 fps, because the same number of frames represents less
real time at a higher rate. Frame count alone is meaningless without knowing the rate it
was counted at, which is why this tool always asks for a rate before converting.
Can a frame number be negative?
In an absolute sense no video frame is “negative,” but a computed result — subtracting a larger timecode from a smaller one, or a frame count relative to a marker that comes later in the timeline — can legitimately be negative, and this tool reports it with a leading minus sign on the hours field rather than wrapping it into a large positive number, which is a common and confusing failure in hand-rolled timecode code.
Where this comes up
- Image sequence naming. VFX pipelines routinely name individual frame
files
shot0010.0003000.exrand need the equivalent timecode for a shot report or a client review note, so that a note like “fix the flicker at frame 3000” can be translated into a timecode a reviewer can jump to directly in a playback tool without counting frames by hand. - Scripting against an API that returns raw frame counts. Some automation and rendering APIs report progress, markers, or in/out points purely as frame numbers, leaving the human-readable conversion to the caller. A render-farm status page that reports “frame 43,201 of 108,000” is more useful to a human once it is also expressed as a timecode and a percentage of the total duration.
- Checking a custom implementation. The worked examples above, especially the rollover and drop-frame cases, are exactly the boundary conditions worth testing a new piece of timecode-handling code against before trusting it with real media — a unit test suite built from the reference values on this page and the drop-frame guide would catch the most common implementation mistakes listed above before they ever touch a real delivery.
- Reverse-engineering a burned-in timecode. Given a frame you can see in an export and its position in the sequence, working out what frame number and what original source timecode it corresponds to — useful when a colleague sends a still frame or a short clip with no accompanying metadata and asks where in the full sequence it came from.
- Building a cue sheet or a subtitle file by hand. Formats such as SRT
use plain
HH:MM:SS,mmmmillisecond timing rather than frame-based timecode, so converting a known frame-accurate edit point into the equivalent time is a routine step when handing a cut list to a captioning vendor that works in a different time base.
More on timecode
- Timecode converterThe full tool: frame numbers, real time, arithmetic and rate conversion
- What is drop-frame timecode?The full mechanics, the minute-ten exemption, and why it exists
- 29.97 fps vs 30 fpsWhere the 1000/1001 ratio comes from and what it costs you
- About this toolWhy it’s built this way, and how the arithmetic was verified