Skip to content

fix: play the new crop straight after cropping or re-cropping - #757

Merged
ShaneIsrael merged 2 commits into
developfrom
fix/crop-playback-cache
Sep 30, 2026
Merged

ShaneIsrael merged 2 commits into
developfrom
fix/crop-playback-cache

Conversation

@ShaneIsrael

Copy link
Copy Markdown
Collaborator

Fixes #755
Fixes #756

Problem

#755: format error right after cropping. /_content/video/ looks for <id>-cropped.mp4 with try_files on every play of an uncropped video. With open_file_cache_errors on, nginx remembered "no such file" for up to 120 s (open_file_cache_valid). The player asked for the crop as soon as it was made, got a 404, and showed MEDIA_ELEMENT_ERROR: Format error. A freshly uploaded video has usually just been played, which is why it showed up mostly then. The sendfile() ... truncated alert on poster.jpg in the report comes from the same cache: ffmpeg rewrites the poster in place while nginx still holds its old size.

#756: re-crop plays the old crop. The crop and the transcodes keep their names when a video is re-cropped, but /_content/derived/ and /_content/video/ sent Cache-Control: public, max-age=31536000, immutable, so the browser kept the old files. nginx's file cache made it worse: after a re-crop it could keep serving the deleted crop from a cached descriptor.

Change

  • nginx (prod.conf)
    • open_file_cache is removed, with a comment explaining why it stays off.
    • A new location for /_content/derived/<id>/*.mp4 (the crop and the transcodes) sends no-cache, with the same auth gate as before. Other derived files (images, their thumbnails) keep the one-year immutable header.
    • /_content/video/ sends no-cache: it serves the crop when there is one and the original when there is not, so its content changes with the crop. og:video embeds use this URL too.
  • Client (utils.js, CompactVideoCard.js)
    • New getMediaVersion(info) returns <start>-<end> for a cropped video and nothing otherwise.
    • The player and the card hover preview add it as ?v= to the crop and transcode URLs. The headers alone are not enough: Chromium reuses media it already has in memory for an unchanged URL, which fits the report that only a new tab helped.
    • Uncropped videos keep their current URLs.
  • Server (util.py)
    • create_video_crop and create_poster write to a .tmp file and rename it into place, as transcode_video_quality already does. try_files picks up the crop as soon as the name exists, and +faststart rewrites the whole file in a second pass, so a half-written crop could previously reach viewers.
    • A failed poster attempt now leaves the old poster in place.
  • Version bumped to 1.8.5.

Trade-offs

  • Crop and transcode requests, and plays of /_content/video/, now check with nginx each time. When nothing changed that is a small 304 Not Modified. Password-protected videos also go through the auth gate on each of these checks now.
  • The crop's temp name, -cropped.tmp.mp4, matches the **/*.tmp.mp4 sweep that runs when a bulk transcode starts. If a bulk transcode starts during the second or so a crop takes, that crop fails and has to be saved again. The same was already true of transcodes.

Testing

  • nginx: ran prod.conf in nginx:stable-alpine with a stand-in for the Flask login check, and replayed play → play → crop → play → re-crop → play. nginx -t passes on both configs.
    • Before: a 404 on a crop that existed, and /_content/video/ still serving the deleted crop after the re-crop.
    • After: the right file every time. The headers are as listed above, with thumbnails and images unchanged.
  • ffmpeg: ran create_video_crop and create_poster against real ffmpeg.
    • The crop comes out at the right length (5–12 s gives 7.07 s), and only the .tmp name exists while it is written.
    • A failed crop or poster leaves no temp file and does not touch the existing crop or poster.
    • A regenerated poster replaces the old one.
  • Client: checked the URLs getVideoSources builds for uncropped, cropped, re-cropped, mid-crop and editor states. vite build passes.
  • Not run: the whole app end to end in a browser.

Cropping a video that had just been played could fail with a format error
until the page was reloaded. nginx's open_file_cache had remembered that the
crop did not exist yet, since /_content/video/ looks for it on every play,
and kept answering 404 for up to two minutes after it was made. The same
cache kept serving a deleted crop after a re-crop. The cache is gone.

A re-crop could also keep playing the previous crop until a force refresh.
The crop and the transcodes keep their names when a video is re-cropped,
but nginx told the browser to keep them for a year. They are now
revalidated instead, as is /_content/video/, whose content depends on
whether a crop exists. The player and the card hover preview also add the
crop bounds to their URLs, because Chromium reuses media it already has in
memory for an unchanged URL no matter what the headers say.

Crops and posters are now written to a temp file and renamed into place,
as transcodes already are, so nothing serves one that is half written. A
failed poster attempt leaves the old poster in place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant