Measure the frame rate rather than believing the container
`probe` took `r_frame_rate` whenever it was at or under the cap, on the grounds that it is the rate that keeps every distinct source frame. It is not a claim about frames at all: ordinary iPhone footage declares 120 over a stream whose timestamps are 1/30s apart, and resampling it up turned an 11-second clip into 1293 proxy frames instead of 323 — four times the encode, four times the tracing stills (91MB against 23MB), four times the blobs and the rows, for 970 frames that are copies of their neighbours. So `_measured_rate` reads the timestamps and `_choose_rate` keeps whichever declared rate they bear out. Two details carry it: the times are sorted before differencing, because an HEVC stream arrives in decode order and differencing that measures the reordering delay instead of the rate; and the statistic is the MEDIAN interval, which is what keeps the property the nominal rate was being taken for — a take held on one frame still reports the rate of the parts that move, so no distinct frame is dropped. A genuine 120fps capture still extracts at 120, and there is a test on that specifically. `Source.probe` also stopped being the place a reading goes to be preserved. The facts are a pure function of bytes that are the row's own identity, so a re-upload re-reads them: otherwise every already-uploaded source would have gone on resampling to four times the frames with no way to correct it short of deleting the row. Already-extracted footage is untouched — `extraction_key` still says scheme 3, so those jobs stay done and reachable. Bumping it re-extracts everything at the corrected rate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8d20097e61
commit
e26ad723fa
3 changed files with 210 additions and 7 deletions
|
|
@ -218,6 +218,17 @@ def sources(request):
|
|||
"media_type": upload.content_type or "video/mp4"})
|
||||
row, created = Source.objects.get_or_create(
|
||||
blob=blob, defaults={"filename": Path(upload.name).name[:255], "probe": facts})
|
||||
if not created and row.probe != facts:
|
||||
# THE FACTS ARE RE-READ, NOT REMEMBERED. They are a pure function of
|
||||
# the bytes, and the bytes are this row's identity — so a
|
||||
# disagreement means the server reads the file differently now from
|
||||
# whenever it first saw it, and the fresh reading is the one to keep.
|
||||
# Storing the first reading forever pins a source to a rate the code
|
||||
# no longer believes in, and makes it unfixable without deleting the
|
||||
# row: `extraction.probe` got better at phone footage and every
|
||||
# already-uploaded source would have gone on being wrong.
|
||||
row.probe = facts
|
||||
row.save(update_fields=["probe"])
|
||||
return JsonResponse({"id": str(row.id), "digest": digest,
|
||||
"filename": row.filename, "probe": row.probe,
|
||||
"created": created}, status=201 if created else 200)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue