Skip to content

RTOpacks Video Processing Pipeline — Standard Operating Procedure

Overview

This SOP governs the intake, gating, processing, naming, upload, and DB registration of all background video assets for rtopacks.com.au.

Source location: ~/Downloads/
Tim organises Envato downloads into numbered theme folders as he buys them:

~/Downloads/
  _hold-discard/           ← rejects go here (create this first)
  01 Transport and Logistics/
      wide/                ← landscape Envato downloads drop here
      tall/                ← portrait Envato downloads drop here
  02 Education and Training/
      wide/
      tall/
  ... (03 through 10)

Run all scripts from ~/Downloads/ unless stated otherwise. Process one theme folder at a time — gate, review, transcode, rename, upload, DB insert — before moving to the next.


Step 1 — Duration gate (automated)

Run the gating script against the source folder. Any file under 15 seconds is moved to _hold-discard/ immediately. No manual review needed for sub-15s files.

# Install ffprobe if not present: brew install ffmpeg
# Run from the theme source folder

for f in *.mp4 *.mov; do
  duration=$(ffprobe -v quiet -show_entries format=duration \
    -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null)
  if [ -z "$duration" ]; then continue; fi
  # Round to integer
  secs=$(printf "%.0f" "$duration")
  if [ "$secs" -lt 15 ]; then
    echo "DISCARD ($secs s): $f"
    mv "$f" "../_hold-discard/$f"
  else
    echo "KEEP ($secs s): $f"
  fi
done

Create _hold-discard/ folder at the same level as the theme folders before running.


Step 2 — Manual review of keepers

For files that pass the gate (≥15s), do a quick visual check:

Keep if: - Clear contextual relevance to the theme - Good exposure — not too dark, not washed out - No visible watermarks, logos, or text overlays - Landscape clips: wide composition with room for HUD overlay in lower 40% - Portrait clips: subject centred, vertical composition works at 9:16

Move to _hold-discard/ if: - Generic stock footage feel with no sector context (e.g. "hands on keyboard" for tech) - Heavy camera shake or strobing effects - Faces too prominent / closeup — background should be atmosphere, not portrait - Duplicate theme — if you already have 5 strong clips for a theme, excess go to hold

Target per theme: 3–5 landscape clips + 3–5 portrait clips. Not more than 8 of either.


Step 3 — Transcoding (standardise format)

All keepers must be transcoded to a consistent spec before upload. Run ffmpeg:

Landscape (desktop):

ffmpeg -i "input.mp4" \
  -vf "scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080" \
  -c:v libx264 -crf 23 -preset slow \
  -c:a none \
  -movflags +faststart \
  "output-landscape.mp4"

Portrait (mobile):

ffmpeg -i "input.mp4" \
  -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920" \
  -c:v libx264 -crf 23 -preset slow \
  -c:a none \
  -movflags +faststart \
  "output-portrait.mp4"

Notes: - -c:a none — strips audio (videos play muted, no point carrying audio track) - -movflags +faststart — moves metadata to front of file for faster browser streaming - crf 23 — good quality/size balance. Lower = better quality, larger file. Don't go below 20. - For .mov source files, same ffmpeg command works — ffmpeg handles the container conversion


Step 4 — Naming convention

Use this exact format:

rtopacks-{theme-slug}-{orientation}-{sequence}.mp4

Where: - {theme-slug} = the slug from the video_themes table (see list below) - {orientation} = wide or tall - {sequence} = zero-padded 2-digit number: 01, 02, 03 etc.

Examples:

rtopacks-transport-logistics-wide-01.mp4
rtopacks-transport-logistics-wide-02.mp4
rtopacks-transport-logistics-tall-01.mp4
rtopacks-transport-logistics-tall-02.mp4
rtopacks-health-care-wide-01.mp4
rtopacks-health-care-tall-01.mp4

Theme slugs (from video_themes table):

transport-logistics
education-training
agriculture-environment
health-care
tourism-hospitality
construction-trades
technology-automotive
creative-arts
resources-infrastructure
business-finance


Step 5 — Upload to R2

Upload all processed files to the R2 bucket at:

media.rtopacks.com.au/videos/

Use wrangler or the Cloudflare dashboard. Files are served directly from R2 via the media subdomain.

# Via wrangler
wrangler r2 object put media-rtopacks/videos/rtopacks-transport-logistics-wide-01.mp4 \
  --file ./rtopacks-transport-logistics-wide-01.mp4 \
  --content-type video/mp4

Step 6 — Update the DB

The existing video_themes table maps training package codes to theme slugs. A new video_clips table handles the many-clips-per-theme model with separate wide and tall URLs.

Create video_clips table (one-time migration — run once):

CREATE TABLE IF NOT EXISTS video_clips (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  theme_slug TEXT NOT NULL,
  video_url_wide TEXT NOT NULL,
  video_url_tall TEXT,
  duration_seconds INTEGER,
  active INTEGER NOT NULL DEFAULT 1,
  added_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_video_clips_theme ON video_clips(theme_slug);

Insert new clips after each batch upload:

INSERT INTO video_clips (theme_slug, video_url_wide, video_url_tall, duration_seconds)
VALUES 
  ('transport-logistics', 
   'https://media.rtopacks.com.au/videos/rtopacks-transport-logistics-wide-01.mp4',
   'https://media.rtopacks.com.au/videos/rtopacks-transport-logistics-tall-01.mp4',
   28),
  ('transport-logistics',
   'https://media.rtopacks.com.au/videos/rtopacks-transport-logistics-wide-02.mp4',
   'https://media.rtopacks.com.au/videos/rtopacks-transport-logistics-tall-02.mp4',
   35);

The qual page video selection query:

SELECT video_url_wide, video_url_tall 
FROM video_clips 
WHERE theme_slug = ? AND active = 1
ORDER BY RANDOM() 
LIMIT 1;

video_themes table stays unchanged — it still maps training package codes to theme slugs. The qual page joins video_themes (to get theme_slug from training_package_code) then video_clips (to get a random clip URL for that theme_slug).

Note: video_url_tall can be NULL if no portrait clip exists for that entry yet — the front-end falls back to video_url_wide in that case.


Step 7 — Dissolve logic (front-end)

The dissolve is handled in the qual page HUD component. Standard:

  • Min duration gate: 15s (enforced at ingest — see Step 1)
  • Dissolve trigger: at currentTime >= duration - 2.5
  • Transition: CSS opacity crossfade, 2.5s duration, ease-in-out
  • Next clip selection: random pick from pool for current theme_slug, exclude currently playing clip
  • Reduced motion: if prefers-reduced-motion, skip crossfade — hard cut or static image fallback
  • On mobile: use video_url_tall from video_clips if present, fall back to video_url_wide if no tall clip exists
// Pseudocode for dissolve trigger
video.addEventListener('timeupdate', () => {
  if (video.duration - video.currentTime <= 2.5 && !transitioning) {
    transitioning = true;
    fadeOut(video); // opacity 1 → 0 over 2.5s
    const next = pickRandom(themeClips.filter(c => c !== current));
    loadAndFadeIn(next); // preload + opacity 0 → 1 over 2.5s
  }
});

Summary checklist per batch

  • Gate script run — sub-15s files in _hold-discard/
  • Manual review complete — keepers confirmed
  • All files transcoded to 1920×1080 (wide) and 1080×1920 (tall) where applicable
  • Audio stripped from all files
  • Files named to convention: rtopacks-{theme-slug}-{orientation}-{sequence}.mp4
  • All files uploaded to media.rtopacks.com.au/videos/
  • video_themes rows inserted for all new clips (both wide and tall)
  • video_clips table created in rtopacks-db
  • All new clips inserted into video_clips with both wide and tall URLs where available
  • Dissolve logic updated in HUD component if not already implemented
  • Ops note logged: X landscape + Y portrait clips added for {theme}

Legacy files

The existing hero-1.mp4 through hero-13.mp4 at media.rtopacks.com.au/videos/ can remain until replaced. The DB rows referencing them stay active until updated. No destructive action on legacy files — just add new rows pointing to new filenames.