On this page
Name the time model before comparing rates
The source dates shown in this guide identify documentation checks on 22 October 2026, not original publication dates. Follow each citation for the decoding-software documentation being discussed.
Keep stream index, codec, declared time base, average frame rate, r_frame_rate when supplied, start time, duration, frame count, and controlled artifact reference as separate fields. A stream-level rate is a summary. It is not a substitute for the sequence of presentation timestamps, or PTS, that decoding software exposes for individual frames.
The PyAV, (opens in a new tab) documentation distinguishes average_rate, base_rate, and guessed_rate concepts on a video stream. Those names are useful clues that a field has a purpose and derivation; they are not interchangeable evidence of every frame presentation time. The PyAV, (opens in a new tab) documentation also separates PTS and DTS, so use decoded frame PTS for a question about when a frame should be shown and retain packet timing separately when decode order matters.
| Field | Record | Do not infer |
|---|---|---|
| time_base | Integer timestamp units and source stream reference | That one unit is one displayed frame |
| avg_frame_rate | Summary rate and whether it was supplied | That every frame uses its reciprocal |
| r_frame_rate | Summary rate when present | That it is the editorial target |
| frame PTS | Bounded list or count plus first and last indexes | That a gap proves corruption |
| packet DTS | Separate decode-order observation when needed | That DTS is display timing |
| expected delta | Explicit local policy or unknown | That the header alone defines tolerance |
Ask for selected stream and frame fields
FFprobe supports stream, frame, and packet inspection with selected entries and machine-readable output FFmpeg, (opens in a new tab). Request only fields needed for the worksheet. Keep a controlled identifier in the command, not a private filesystem path, footage title, packet payload, or decoding log.
Selected stream and frame timing
python3 - "$MEDIA_REFERENCE" <<'PY'
import json
import os
import selectors
import subprocess
import sys
import time
MAX_BYTES = 200_000
TIMEOUT_SECONDS = min(30.0, max(0.01, float(os.environ.get("RIGHTMARK_TIMEOUT_SECONDS", "30"))))
command = [
"ffprobe", "-v", "error", "-read_intervals", "%+30", "-select_streams", "v:0",
"-show_entries", "stream=index,avg_frame_rate,r_frame_rate,time_base,start_time,duration:"
"frame=best_effort_timestamp_time,pts_time,pkt_duration_time",
"-show_streams", "-show_frames", "-of", "json", sys.argv[1],
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
selector = selectors.DefaultSelector()
selector.register(process.stdout, selectors.EVENT_READ)
data, reason = bytearray(), None
deadline = time.monotonic() + TIMEOUT_SECONDS
while process.poll() is None or selector.get_map():
if time.monotonic() >= deadline:
reason = "timeout"; process.kill(); break
if not selector.select(deadline - time.monotonic()):
reason = "timeout"; process.kill(); break
chunk = process.stdout.read1(16_384)
if not chunk: break
data.extend(chunk)
if len(data) > MAX_BYTES:
reason = "output_limit_reached"; process.kill(); break
process.wait()
exit_status = process.returncode
if reason is not None:
print(json.dumps({"status": "incomplete", "reason": reason, "exit_status": exit_status, "captured_bytes": len(data)}))
raise SystemExit(125 if reason == "output_limit_reached" else 124)
if exit_status != 0:
print(json.dumps({"status": "incomplete", "reason": "ffprobe_exit_status", "exit_status": exit_status}))
raise SystemExit(exit_status)
try:
payload = json.loads(data)
except json.JSONDecodeError:
print(json.dumps({"status": "incomplete", "reason": "invalid_complete_json", "exit_status": exit_status}))
raise SystemExit(65)
print(json.dumps({"status": "complete", "exit_status": exit_status, "payload": payload}, separators=(",", ":")))
PYThe -read_intervals %+30 request asks ffprobe for an approximately bounded 30-second read, but seeking is format- and decoding-software-dependent and is not a hard frame or byte cap. The wrapper bounds process time and captured output without truncating a JSON document silently. It emits status: complete only after a zero exit status and complete JSON parse; timeout, output-limit, nonzero-exit, and invalid-JSON cases emit status: incomplete with a reason and the ffprobe exit status. Do not add a raw-data dump to a public diagnostic. Preserve the command policy, tool version, selected stream, and evidence reference. This bounded check is not a whole-file completeness claim. The report can say how many frames were inspected and which stable issue classes occurred without copying images, audio, titles, or unbounded warnings.
Synthetic timing summary
{
"stream": {"avg_frame_rate": "30/1", "r_frame_rate": "30/1", "time_base": "1/30"},
"frames": [
{"index": 0, "pts": 0, "pts_time": 0.0},
{"index": 1, "pts": 1, "pts_time": 0.033333},
{"index": 2, "pts": 3, "pts_time": 0.1}
]
}The values are invented for this worksheet. The synthetic header says 30 frames per second, while the PTS jump from 1 to 3 in a 1/30 time base. That is a candidate interval finding, not proof of a corrupted file or a required edit. Keep frame indexes and calculation policy so another reviewer can reproduce the observation.
Compute bounded PTS deltas
Convert PTS values using the stream time base, then compare adjacent deltas with an explicit local expectation. The expectation can come from a capture contract, a nominal rate selected for this review, or an unknown state. Do not infer variable timing from one header field or label every difference as a defect.
Bounded timestamp arithmetic
from fractions import Fraction
def review_pts(pts_values, time_base: Fraction, expected_delta=None,
tolerance=0.0005, max_frames=10_000):
if time_base is None or time_base <= 0:
return {
"inspected_frames": 0,
"deltas": [],
"issues": [{"kind": "time_base_unavailable"}],
}
times = []
truncated = False
for index, pts in enumerate(pts_values):
if index >= max_frames:
truncated = True
break
times.append(None if pts is None else float(Fraction(pts) * time_base))
deltas, issues, previous = [], [], None
for index, current in enumerate(times):
if current is None:
issues.append({"index": index, "kind": "missing_pts"})
previous = None
continue
if previous is not None:
delta = current - previous
deltas.append(delta)
if delta <= 0:
issues.append({"index": index, "kind": "non_monotonic_or_duplicate_pts"})
elif expected_delta is not None and abs(delta - expected_delta) > tolerance:
issues.append({"index": index, "kind": "interval_differs_from_policy"})
previous = current
if truncated:
issues.append({"kind": "frame_limit_reached"})
return {"inspected_frames": len(times), "deltas": deltas, "issues": issues}
synthetic_pts = [0, 1, 2, 4, 5]
print(review_pts(synthetic_pts, Fraction(1, 30), Fraction(1, 30)))The function is bounded by max_frames and emits issue classes without timestamps tied to a private recording. An absent, zero, or negative time base returns time_base_unavailable before PTS arithmetic, rather than turning an invalid stream contract into a timing result. The synthetic list contains one two-tick gap under a 1/30 policy. A production worksheet can retain numeric times in an access-controlled evidence store while publishing only counts, ranges, and stable classes. Missing PTS stays distinct from zero or repeated PTS.
A deliberate edit, a segment boundary, a decoding implementation choice, or a source time-base change can create the same observation. Record discontinuity markers or edit-list context instead of rewriting frame times in place. If a constant-rate derivative is required, create a new artifact reference and preserve the original timing evidence.
Use a timing worksheet and disposition table
Record one row per stream or bounded frame window: artifact reference, stream index, time base, summary rate fields, first and last inspected frame, PTS count, missing PTS count, non-monotonic count, positive delta summary, expected-delta policy, tolerance, truncation state, evidence reference, owner decision, and unresolved questions.
| Observation | Safe interpretation | Next action |
|---|---|---|
| Rate summaries are present | The container exposes rate declarations | Compare them with decoded PTS; do not choose one automatically |
| PTS deltas match policy | No timing variation was observed in the window | Preserve window and policy; do not generalize |
| PTS deltas vary | Candidate variable timing or a discontinuity | Inspect segment boundaries, time base, and source notes |
| PTS repeats or decreases | Duplicate or non-monotonic presentation timing | Hold for decoding-software and edit-list review; do not sort silently |
| PTS is absent for frames | Timing evidence is incomplete | Preserve indexes and distinguish missing from zero |
| Frame limit reached | Review coverage was intentionally bounded | Mark the remainder uninspected |
| PTS checks pass | Selected timing checks have evidence | Continue separate playback, provenance, privacy, rights, and accuracy review |
Keep “not inspected,” “not available,” and “not observed under this policy” distinct. A short window can miss a transient event, and a timing summary cannot answer whether motion is visually acceptable or editorially intended.
Keep diagnostics safe and the next step qualified
A public report can contain a controlled artifact identifier, stream index, numeric counts, time-base notation, bounded frame indexes, issue classes, and tool versions. It should not contain private paths, footage labels, decoded images, audio, or unbounded decoding logs. A timestamp can disclose an event sequence even without a filename, so share only the precision needed for the approved review.
Pair this check with the timestamp and timezone guide and data provenance log. Those pages own general timestamp semantics and source, rights, and processing evidence; this guide owns bounded video timing observations.
After the owner has a supported non-health scope, authority to describe the footage, and a metadata or synthetic timing packet, the qualified appraisal request path may be a next preparation step. It does not validate frame timing, re-encode video, approve a derivative, produce a quote, match a buyer, issue a legal opinion, or grant a license. Keep raw footage outside this guide and within the owner-approved handling boundary.
Takeaways
- Treat avg_frame_rate, r_frame_rate, and time_base as distinct stream observations.
- Compare decoded frame PTS deltas to an explicit local policy instead of relying on a header rate.
- Keep missing, duplicate, non-monotonic, and unusually large deltas as separate findings.
- Bound frame inspection and publish counts or ranges, not raw footage or private labels.
- Preserve original timing evidence when a later workflow creates a re-timed derivative.