-
Notifications
You must be signed in to change notification settings - Fork 562
feat(website): add responsive media tooling for marketing assets #11869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4fdee97
feat(website): add responsive video component and url helper
Glary-Bot 36186c2
feat(website): add ffmpeg helper for marketing video encoding
Glary-Bot 8c2218b
feat(website): document marketing image conventions for astro:assets
Glary-Bot 7941ce1
fix(website): make SiteVideo reactive and a11y-correct
Glary-Bot d1d1be8
fix(website): make process-videos.sh handle short clips and uppercase…
Glary-Bot de2932c
docs(website): clarify SiteVideo vs VideoPlayer in scripts README
Glary-Bot b62248f
chore(website): silence knip for unused-but-staged media tooling
Glary-Bot 0b7de9d
docs(website): label process-videos.sh README output block
Glary-Bot cbb897c
fix(website): validate ffprobe duration before awk comparison
Glary-Bot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Website Scripts | ||
|
|
||
| ## `refresh-ashby-snapshot.ts` | ||
|
|
||
| Pulls the latest job postings from Ashby and writes | ||
| `src/data/ashby-roles.snapshot.json`. Invoked by the `Release: Website` | ||
| GitHub Actions workflow; also runnable locally via | ||
| `pnpm --filter @comfyorg/website ashby:refresh-snapshot`. | ||
|
|
||
| ## `process-videos.sh` | ||
|
|
||
| Generates multi-resolution VP9/WebM + H.264/MP4 variants and a poster | ||
| frame for marketing videos using `ffmpeg`. Run **locally** before | ||
| uploading the outputs to `media.comfy.org`; this is not wired into CI. | ||
|
|
||
| ```sh | ||
| apps/website/scripts/process-videos.sh \ | ||
| ./video-sources \ | ||
| ./dist/videos \ | ||
| "640 960 1280 1920" | ||
| ``` | ||
|
|
||
| ### Output | ||
|
|
||
| For each source video at `./video-sources/foo.mp4`, you get: | ||
|
|
||
| ```text | ||
| foo-640.webm foo-640.mp4 | ||
| foo-960.webm foo-960.mp4 | ||
| foo-1280.webm foo-1280.mp4 | ||
| foo-1920.webm foo-1920.mp4 | ||
| foo-poster.jpg | ||
| ``` | ||
|
|
||
| The naming convention is enforced by `buildVideoSources()` in | ||
| `src/utils/video.ts`, which the `<SiteVideo>` Vue component uses to | ||
| emit `<source>` URLs. | ||
|
|
||
| ### Pairing with `<SiteVideo>` | ||
|
|
||
| Once the assets are uploaded, render them with: | ||
|
|
||
| ```vue | ||
| <SiteVideo | ||
| name="foo" | ||
| base-url="https://media.comfy.org/website/marketing" | ||
| :width="1280" | ||
| :formats="['webm', 'mp4']" | ||
| poster="https://media.comfy.org/website/marketing/foo-poster.jpg" | ||
| autoplay | ||
| loop | ||
| /> | ||
| ``` | ||
|
|
||
| ### `<SiteVideo>` vs `<VideoPlayer>` | ||
|
|
||
| - **`SiteVideo`** — lightweight multi-source `<video>` for decorative or | ||
| autoplay marketing clips. No custom controls, no captions UI. | ||
| - **`VideoPlayer`** — full-featured player with custom scrubber, mute, | ||
| fullscreen, and caption toggles. Use this for content with subtitles or | ||
| user-driven playback. | ||
|
|
||
| If you need both responsive sources and the rich `VideoPlayer` chrome, the | ||
| two are not yet combined; either pick one or extend `VideoPlayer` to accept | ||
| a source list. | ||
|
|
||
| ### Encoder choices | ||
|
|
||
| - **VP9/WebM** at CRF 32 — preferred by Chrome and Firefox; smaller files. | ||
| - **H.264/MP4** at CRF 23, High profile, `+faststart` — universal fallback, | ||
| required for Safari iOS. | ||
| - **Poster JPG** at q4 — extracted from t=1s when the clip is long enough, | ||
| otherwise t=0; scaled to 1280w. Use this as the `poster` attribute so | ||
| the video shows something while loading. | ||
|
|
||
| ### Why a single resolution per video | ||
|
|
||
| `<source media="...">` inside `<video>` is unreliable across browsers | ||
| (Safari ignores it). The simplest correct strategy is to ship one | ||
| well-sized resolution and let CSS scale it down on smaller viewports. | ||
| The script generates multiple widths so you can pick a different one | ||
| per page (e.g. 1280w for a hero, 640w for a thumbnail), or wire up | ||
| JavaScript-based selection later if metrics demand it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Generate multi-resolution VP9/WebM + H.264/MP4 variants and a poster frame | ||
| # for every source video in a given directory. Intended to be run locally | ||
| # before uploading the outputs to media.comfy.org. | ||
| # | ||
| # Usage: | ||
| # apps/website/scripts/process-videos.sh <input-dir> <output-dir> [widths] | ||
| # | ||
| # Example: | ||
| # apps/website/scripts/process-videos.sh \ | ||
| # ./video-sources \ | ||
| # ./dist/videos \ | ||
| # "640 960 1280 1920" | ||
| # | ||
| # Defaults to widths "1280" if omitted. | ||
| # | ||
| # Output naming matches buildVideoSources() in src/utils/video.ts: | ||
| # <name>-<width>.webm | ||
| # <name>-<width>.mp4 | ||
| # <name>-poster.jpg (single 1280w poster, suitable for SiteVideo) | ||
| # | ||
| # Requires ffmpeg and ffprobe on PATH. Tested with ffmpeg 6.x and 7.x. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| if [[ $# -lt 2 ]]; then | ||
| cat <<USAGE >&2 | ||
| Usage: $0 <input-dir> <output-dir> [widths] | ||
| widths: space-separated list, e.g. "640 1280 1920" (default: "1280") | ||
| USAGE | ||
| exit 64 | ||
| fi | ||
|
|
||
| input_dir=$1 | ||
| output_dir=$2 | ||
| widths=${3:-1280} | ||
|
|
||
| for tool in ffmpeg ffprobe; do | ||
| if ! command -v "$tool" >/dev/null 2>&1; then | ||
| echo "error: $tool not found on PATH" >&2 | ||
| exit 127 | ||
| fi | ||
| done | ||
|
|
||
| if [[ ! -d $input_dir ]]; then | ||
| echo "error: input dir not found: $input_dir" >&2 | ||
| exit 66 | ||
| fi | ||
|
|
||
| mkdir -p "$output_dir" | ||
|
|
||
| shopt -s nullglob nocaseglob | ||
| sources=("$input_dir"/*.{mp4,mov,webm,mkv}) | ||
| shopt -u nullglob nocaseglob | ||
|
|
||
| if [[ ${#sources[@]} -eq 0 ]]; then | ||
| echo "error: no source videos in $input_dir (looked for .mp4 .mov .webm .mkv)" >&2 | ||
| exit 66 | ||
| fi | ||
|
|
||
| for src in "${sources[@]}"; do | ||
| name=$(basename "$src") | ||
| name=${name%.*} | ||
| echo "==> $name" | ||
|
|
||
| for w in $widths; do | ||
| webm_out="$output_dir/${name}-${w}.webm" | ||
| mp4_out="$output_dir/${name}-${w}.mp4" | ||
|
|
||
| echo " encoding ${w}w VP9/WebM -> $webm_out" | ||
| ffmpeg -y -hide_banner -loglevel error \ | ||
| -i "$src" \ | ||
| -vf "scale=${w}:-2:flags=lanczos" \ | ||
| -c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 -tile-columns 2 \ | ||
| -c:a libopus -b:a 96k \ | ||
| -f webm "$webm_out" | ||
|
|
||
| echo " encoding ${w}w H.264/MP4 -> $mp4_out" | ||
| ffmpeg -y -hide_banner -loglevel error \ | ||
| -i "$src" \ | ||
| -vf "scale=${w}:-2:flags=lanczos" \ | ||
| -c:v libx264 -crf 23 -preset slow -profile:v high -pix_fmt yuv420p \ | ||
| -c:a aac -b:a 128k \ | ||
| -movflags +faststart \ | ||
| "$mp4_out" | ||
| done | ||
|
|
||
| poster_out="$output_dir/${name}-poster.jpg" | ||
| duration=$(ffprobe -v error -show_entries format=duration \ | ||
| -of default=noprint_wrappers=1:nokey=1 "$src" 2>/dev/null || echo 0) | ||
| if awk "BEGIN { exit !($duration >= 1.0) }"; then | ||
| poster_seek=1 | ||
| else | ||
| poster_seek=0 | ||
| fi | ||
| echo " extracting poster (t=${poster_seek}s) -> $poster_out" | ||
| ffmpeg -y -hide_banner -loglevel error \ | ||
| -ss "$poster_seek" -i "$src" \ | ||
| -vframes 1 -vf "scale=1280:-2:flags=lanczos" \ | ||
| -q:v 4 \ | ||
| "$poster_out" | ||
| done | ||
|
|
||
| echo "done. upload contents of $output_dir to media.comfy.org." | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Marketing Assets | ||
|
|
||
| Source images committed here are processed by Astro at build time and emitted | ||
| as multiple formats (AVIF, WebP) at multiple widths (640w, 960w, 1280w, 1920w). | ||
|
|
||
| ## Usage | ||
|
|
||
| Drop a high-resolution source image (PNG or JPG) here, then render it with | ||
| Astro's built-in `<Picture>` component plus the shared defaults: | ||
|
|
||
| ```astro | ||
| --- | ||
| import { Picture } from 'astro:assets' | ||
| import { | ||
| MARKETING_FORMATS, | ||
| MARKETING_WIDTHS | ||
| } from '../utils/marketingImage' | ||
| import hero from '../assets/marketing/hero.png' | ||
| --- | ||
| <Picture | ||
| src={hero} | ||
| alt="ComfyUI workflow preview" | ||
| formats={[...MARKETING_FORMATS]} | ||
| widths={[...MARKETING_WIDTHS]} | ||
| sizes="(max-width: 768px) 100vw, 50vw" | ||
| /> | ||
| ``` | ||
|
|
||
| The component generates a `<picture>` element with `<source>` tags for AVIF | ||
| and WebP, plus an `<img>` fallback. Output files are hashed and emitted under | ||
| `dist/_website/` for long-term caching. | ||
|
|
||
| A custom Astro wrapper component is intentionally not provided: Astro's | ||
| discriminated union `LocalImageProps | RemoteImageProps` for `<Picture>` makes | ||
| a thin wrapper that mutates `widths` / `formats` impractical to type safely | ||
| without `as` casts. The shared constants give us the same consistency benefit | ||
| without that cost. | ||
|
|
||
| ## When to use this vs. `media.comfy.org` | ||
|
|
||
| - **Use `src/assets/marketing/`** for static marketing images that are part of | ||
| page content (hero shots, product imagery, illustrations). Build-time | ||
| processing gives you AVIF/WebP variants automatically. | ||
| - **Use `media.comfy.org`** for video content, large/changing image libraries | ||
| (gallery), and anything shared across properties. | ||
|
|
||
| ## Source image guidelines | ||
|
|
||
| - Provide the largest size you'll ever need (≥1920px wide). | ||
| - PNG for screenshots/illustrations with sharp edges; JPG for photographs. | ||
| - Astro will downscale; it will not upscale. Always supply at least 1920w. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| <script setup lang="ts"> | ||
| import { cn } from '@comfyorg/tailwind-utils' | ||
| import { computed } from 'vue' | ||
|
|
||
| import { buildVideoSources, videoKey } from '../../utils/video' | ||
| import type { VideoFormat } from '../../utils/video' | ||
|
|
||
| const { | ||
| name, | ||
| baseUrl, | ||
| width = 1280, | ||
| formats = ['webm', 'mp4'], | ||
| poster, | ||
| alt, | ||
| autoplay = false, | ||
| loop = false, | ||
| muted = autoplay, | ||
| controls = false, | ||
| preload = autoplay ? 'auto' : 'metadata', | ||
| containerClass, | ||
| videoClass | ||
| } = defineProps<{ | ||
| name: string | ||
| baseUrl: string | ||
| width?: number | ||
| formats?: VideoFormat[] | ||
| poster?: string | ||
| alt?: string | ||
| autoplay?: boolean | ||
| loop?: boolean | ||
| muted?: boolean | ||
| controls?: boolean | ||
| preload?: 'auto' | 'metadata' | 'none' | ||
| containerClass?: string | ||
| videoClass?: string | ||
| }>() | ||
|
|
||
| const sources = computed(() => | ||
| buildVideoSources({ name, baseUrl, width, formats }) | ||
| ) | ||
| const remountKey = computed(() => videoKey(sources.value)) | ||
| const decorative = computed(() => !alt && !controls) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div :class="cn('relative', containerClass)"> | ||
| <video | ||
| :key="remountKey" | ||
| :class="cn('size-full', videoClass)" | ||
| :poster | ||
| :preload | ||
| :autoplay | ||
| :loop | ||
| :muted | ||
| :controls | ||
| :aria-label="alt" | ||
| :aria-hidden="decorative ? true : undefined" | ||
| playsinline | ||
| > | ||
| <source | ||
| v-for="source in sources" | ||
| :key="source.src" | ||
| :src="source.src" | ||
| :type="source.type" | ||
| /> | ||
| </video> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const MARKETING_FORMATS = ['avif', 'webp'] as const | ||
|
|
||
| export const MARKETING_WIDTHS = [640, 960, 1280, 1920] as const |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.