DEV Community

Cover image for Factory photos and video: a repeatable pipeline from raw files to CDN
AumCreate
AumCreate

Posted on

Factory photos and video: a repeatable pipeline from raw files to CDN

A factory sends 400 photos and nine videos. Six weeks later the product pages still use the two images that happened to be small enough to upload. This is the pipeline that stops that: the same eight steps every batch, with size targets decided once.

Set the budget first

Pick numbers before touching a file, because "make it smaller" has no end. These are starting points; adjust per catalogue.

Slot Long edge Target size Format
Grid thumbnail 400 px ≤ 40 KB AVIF / WebP / JPEG
Product page 1600 px ≤ 200 KB AVIF / WebP / JPEG
Zoom / full view 2000 px ≤ 400 KB AVIF / WebP / JPEG
Silent hero loop 1280×720 ≤ 2.5 MB, 6–10 s H.264 MP4
Product video 1920×1080 ≤ 2 MB per 10 s H.264 MP4

The video rows decide whether the page loads on a phone in a warehouse. A 40 MB showcase MP4 is not a video, it is a bounce.

Step 1 — Get originals, not chat-app copies

Ask the factory for files, not messages. Anything that travelled through a chat app has been resized, re-encoded and had its metadata stripped, and you cannot get it back. Send a transfer link or ask for an SD card dump.

Attach a shot list, one line per image:

  • front, straight on, product centred
  • 45° angle
  • detail shot of the part customers actually ask about
  • in use or installed
  • scale reference (a hand, a ruler, a standard fitting)
  • packaging and label

Rename on arrival, before anything else: SKU_view_v01.jpg. Six weeks from now you will not remember which of 400 files was the good one.

Step 2 — Cull and normalise

Cull hard. Reject motion blur, mixed colour temperature between shots, and anything with a person's shoes or a half-finished lunch in the background.

Then fix the two things compression cannot fix:

  • Colour space. Print files arrive in CMYK. Convert to sRGB first or the web version looks muddier than the original.
  • Orientation. Apply the EXIF rotation, then strip EXIF. Some pipelines strip first and end up with sideways photos in browsers that no longer read the flag.

Strip GPS too. Factory photos routinely carry location data.

Step 3 — Resize, then compress

Compressing a 6000 px original down to 200 KB gives you a soft, muddy image. Resize to the target width first, sharpen lightly, then encode.

for w in 400 1600 2000; do
  ffmpeg -y -i "in/$f" \
    -vf "scale=${w}:-2:flags=lanczos,unsharp=5:5:0.4" \
    -q:v 3 "out/${base}-${w}.jpg"
done
Enter fullscreen mode Exit fullscreen mode

-2 keeps the aspect ratio and rounds to an even number — irrelevant for stills, essential the moment you reuse the same expression for video.

Then WebP and AVIF from the resized JPEG:

cwebp -q 78 -m 6 -mt -metadata none "out/${base}-1600.jpg" -o "out/${base}-1600.webp"
avifenc -q 55 -s 4 --ignore-exif --ignore-xmp "out/${base}-1600.jpg" "out/${base}-1600.avif"
Enter fullscreen mode Exit fullscreen mode

Keep the ICC profile. Stripping it is how you get a red product that photographs orange on one machine and pink on another.

Step 4 — Choose the format honestly

Format Size vs JPEG Encode cost Notes
JPEG baseline low universal, fine for photos, your fallback
WebP ~25–35% smaller low broadly supported, safe as the middle layer
AVIF ~40–55% smaller high slower to encode, heavier to decode on old phones

Serve all three and let the browser pick:

<picture>
  <source type="image/avif"
          srcset="/media/sku-front-400.5c1a9b3e.avif 400w,
                  /media/sku-front-1600.5c1a9b3e.avif 1600w"
          sizes="(max-width: 700px) 100vw, 50vw">
  <source type="image/webp"
          srcset="/media/sku-front-400.5c1a9b3e.webp 400w,
                  /media/sku-front-1600.5c1a9b3e.webp 1600w"
          sizes="(max-width: 700px) 100vw, 50vw">
  <img src="/media/sku-front-1600.5c1a9b3e.jpg"
       srcset="/media/sku-front-400.5c1a9b3e.jpg 400w,
               /media/sku-front-1600.5c1a9b3e.jpg 1600w,
               /media/sku-front-2000.5c1a9b3e.jpg 2000w"
       sizes="(max-width: 700px) 100vw, 50vw"
       width="1600" height="1600" loading="lazy" decoding="async"
       alt="Stainless steel ball valve, DN50, female thread both ends">
</picture>
Enter fullscreen mode Exit fullscreen mode

Do not pick the format with Accept header negotiation on the server. It splits your CDN cache into variants for a gain the browser can make for itself, and one misconfigured Vary header is a week of debugging.

Note the width and height. They are what stops the page jumping as images land, and they cost nothing.

Step 5 — Video: trim, mute, poster

Nobody watches a factory walkthrough on a product page. Ten to thirty seconds, no audio, one idea.

ffmpeg -i raw.mov \
  -vf "scale=1920:-2:flags=lanczos,fps=30" \
  -c:v libx264 -profile:v high -pix_fmt yuv420p -crf 23 -preset slow \
  -c:a aac -b:a 128k \
  -movflags +faststart product.mp4
Enter fullscreen mode Exit fullscreen mode

+faststart moves the index to the front of the file so playback can start before the download finishes. Without it the video appears to hang on slower connections even though the bytes are arriving.

Silent hero loop:

ffmpeg -i hero.mov -t 8 -an \
  -vf "scale=1280:-2:flags=lanczos,fps=25" \
  -c:v libx264 -crf 28 -preset slow -movflags +faststart hero.mp4
Enter fullscreen mode Exit fullscreen mode

-an drops the audio track entirely — a muted attribute on a video that still has an audio track downloads the audio anyway.

Poster frame, taken from the encoded file so what you see is what ships:

ffmpeg -ss 0.5 -i product.mp4 -frames:v 1 -q:v 3 product-poster.jpg
Enter fullscreen mode Exit fullscreen mode
<video src="/media/product.7d1e04a2.mp4"
       poster="/media/product-poster.7d1e04a2.jpg"
       width="1280" height="720"
       muted playsinline loop preload="none"
       aria-label="Valve assembly, 12 second loop"></video>
Enter fullscreen mode Exit fullscreen mode

preload="none" is the difference between a page that loads and a page that starts buffering 2.5 MB of decoration before the headline renders.

Step 6 — Hash the filenames, then cache forever

Content-hashed names let you set a one-year immutable cache and still update instantly, because the new file has a new name.

f="sku-front-1600.avif"
h=$(sha1sum "$f" | cut -c1-8)   # shasum -a 1 on macOS
mv "$f" "${f%.*}.$h.${f##*.}"  # sku-front-1600.5c1a9b3e.avif
Enter fullscreen mode Exit fullscreen mode

Then at the bucket or CDN:

Cache-Control: public, max-age=31536000, immutable
Enter fullscreen mode Exit fullscreen mode

HTML stays short-lived (max-age=300); the assets it points at never change. Never cache-bust with ?v=2 — many CDNs and proxies key on the path and serve the old file anyway.

Two more settings to verify on the CDN:

  • Range requests. Video needs Accept-Ranges: bytes passed through, or seeking restarts the download.
  • CORS. Playback does not need it; drawing a frame to a canvas does. Set it on the media path only if you actually do that.

Step 7 — Upload, keep masters out

rclone copy out/ remote:my-bucket/media/ \
  --header-upload "Cache-Control: public, max-age=31536000, immutable" \
  --checksum
Enter fullscreen mode Exit fullscreen mode

Masters — RAW files, the 4 GB source video, layered edits — go to cold storage, not the CDN bucket. Keep them. The day someone asks for a three-metre print, the compressed web asset is not the answer.

Step 8 — Check before publish

  • File sizes match the budget table; nothing over 400 KB slipped through.
  • Dimensions are what you expect, not 6000 px.
  • Colours match the physical sample under neutral light.
  • EXIF is gone, including GPS.
  • alt describes the product specifically — "ball valve, DN50, female thread" beats "product image".
  • Poster frame renders before playback starts.
  • Video plays muted and inline on a phone.
  • The first four images on the page are not lazy-loaded; the rest are.

The mistakes that cost the most

  • Re-compressing already-compressed files. Every pass loses. Always start from the original.
  • CMYK nobody converted — fine on the designer's screen, wrong on the site.
  • Uploading the 40 MB MP4 and blaming the CDN. The CDN delivers what you gave it.
  • Autoplay with sound. Browsers block it, so you ship a video that never starts.
  • Missing poster. A black rectangle for two seconds is worse than no video.
  • Query-string cache busting. Silently serves stale files behind some proxies.
  • Deleting the masters. Irreversible.

Run the same eight steps for every batch and the answers stop being judgement calls. That is the whole point of a pipeline.

Top comments (0)