Repository navigation
feat(website): serve the hero film in two qualities #839
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "nostream-website": minor | ||
| --- | ||
|
|
||
| Ship the hero film in two qualities: the page serves a high-res render by default and swaps to a low variant for data-saver clients and small screens (navigator.connection.saveData / effectiveType / viewport + DPR pick). build-media.mjs now emits both variants and records both sources in hero.meta.json. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,38 +45,65 @@ const landingVideo = [ | |
| path.join(LANDING, 'nostream-hero-low.mp4'), | ||
| ].find((p) => p && existsSync(p)) | ||
|
|
||
| if (landingVideo) { | ||
| const filmMeta = readFilmMeta(landingVideo) | ||
| if (filmMeta?.nipCount == null) { | ||
| if (existsSync(HERO_META)) unlinkSync(HERO_META) | ||
| console.error( | ||
| `build-media: ${landingVideo} has no film.meta.json beside it — re-render in website/hero-animation (pnpm run render) or pick a source that includes meta.`, | ||
| ) | ||
| process.exit(1) | ||
| } | ||
| // The low variant ships beside the full one for mobile / Save-Data. It is the | ||
| // previous site video when a fresh high-res render replaces hero.mp4, or the | ||
| // low render when that is all hero-animation has produced. | ||
| const landingLowVideo = [ | ||
| process.env.HERO_LOW_SRC, | ||
| path.join(LANDING, 'nostream-hero-low.mp4'), | ||
| landingVideo === path.join(LANDING, 'nostream-hero-low.mp4') ? null : landingVideo, | ||
| ].find((p) => p && existsSync(p) && p !== landingVideo) | ||
|
|
||
| const encodeHero = (source, outPath, crf) => | ||
| execFileSync( | ||
| 'ffmpeg', | ||
| [ | ||
| '-y', '-loglevel', 'error', | ||
| '-i', landingVideo, | ||
| '-i', source, | ||
| '-vf', 'unsharp=5:5:0.85:5:5:0.0', | ||
| '-an', '-c:v', 'libx264', '-preset', 'slow', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', | ||
| '-tune', 'animation', | ||
| '-crf', '15', | ||
| path.join(MEDIA, 'hero.mp4'), | ||
| '-crf', String(crf), | ||
| outPath, | ||
| ], | ||
| { stdio: 'inherit' }, | ||
| ) | ||
|
|
||
| if (landingVideo) { | ||
| const filmMeta = readFilmMeta(landingVideo) | ||
| if (filmMeta?.nipCount == null) { | ||
| if (existsSync(HERO_META)) unlinkSync(HERO_META) | ||
| console.error( | ||
| `build-media: ${landingVideo} has no film.meta.json beside it — re-render in website/hero-animation (pnpm run render) or pick a source that includes meta.`, | ||
| ) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| encodeHero(landingVideo, path.join(MEDIA, 'hero.mp4'), 15) | ||
| console.log('hero.mp4 ← landing film (film.ts)') | ||
|
|
||
| if (landingLowVideo) { | ||
| const lowMeta = readFilmMeta(landingLowVideo) | ||
| if (lowMeta?.nipCount === filmMeta.nipCount) { | ||
| encodeHero(landingLowVideo, path.join(MEDIA, 'hero-low.mp4'), 22) | ||
| console.log('hero-low.mp4 ← landing film (low variant)') | ||
| } else { | ||
| console.warn( | ||
| `skip hero-low.mp4: low source meta (${lowMeta?.nipCount}) disagrees with full source (${filmMeta.nipCount}) — regenerate both renders together`, | ||
| ) | ||
| } | ||
| } else { | ||
| console.warn('skip hero-low.mp4: no second source (run `pnpm run render:small` in website/hero-animation)') | ||
| } | ||
|
|
||
| writeFileSync( | ||
| HERO_META, | ||
| `${JSON.stringify( | ||
| { | ||
| nipCount: filmMeta.nipCount, | ||
| generatedAt: filmMeta.generatedAt, | ||
| sourceVideo: path.relative(HERO_DIR, landingVideo), | ||
| ...(landingLowVideo ? { sourceLowVideo: path.relative(HERO_DIR, landingLowVideo) } : {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }, | ||
| null, | ||
| 2, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,17 @@ import ScreenBar from './ScreenBar.astro' | |
|
|
||
| interface Props { | ||
| src: string | ||
| lowSrc?: string | ||
| poster: string | ||
| title: string | ||
| class?: string | ||
| } | ||
| const { src, poster, title, class: className } = Astro.props | ||
| const { src, lowSrc, poster, title, class: className } = Astro.props | ||
| --- | ||
|
|
||
| <figure class:list={['screen', className]}> | ||
| <ScreenBar title={title} /> | ||
| <video muted loop playsinline preload="auto" poster={poster} data-autoplay aria-label={title}> | ||
| <video muted loop playsinline preload="auto" poster={poster} data-autoplay data-low-src={lowSrc} aria-label={title}> | ||
| <source src={src} type="video/mp4" /> | ||
|
Comment on lines
+16
to
17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The full video is already discoverable with Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| </video> | ||
| </figure> | ||
|
|
@@ -22,3 +23,31 @@ const { src, poster, title, class: className } = Astro.props | |
| margin: 0; | ||
| } | ||
| </style> | ||
|
|
||
| <script> | ||
| // Quality pick: swap in the low variant for data-saver clients and small | ||
| // screens before the browser commits to the full download. The full source | ||
| // stays the default (the <source> above), so the swap only ever goes down. | ||
| const setup = (video: HTMLVideoElement) => { | ||
| const lowSrc = video.dataset.lowSrc | ||
| if (!lowSrc) return | ||
| const wantsLow = | ||
| navigator.connection?.saveData === true || | ||
| (navigator.connection?.effectiveType ?? '4g') in { 'slow-2g': 1, '2g': 1, '3g': 1 } || | ||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| (matchMedia('(max-width: 768px)').matches && window.devicePixelRatio <= 2) | ||
| if (!wantsLow) return | ||
| const source = video.querySelector('source') | ||
| if (!source) return | ||
| const time = video.currentTime | ||
| const wasPlaying = !video.paused | ||
| source.src = lowSrc | ||
| video.load() | ||
| video.currentTime = time | ||
| if (wasPlaying) void video.play() | ||
| delete video.dataset.lowSrc | ||
| } | ||
| document.querySelectorAll<HTMLVideoElement>('video[data-low-src]').forEach((video) => { | ||
| if (video.dataset.autoplay != null) setup(video) | ||
| else video.addEventListener('play', () => setup(video), { once: true }) | ||
| }) | ||
| </script> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowMetaandfilmMetaread the sameoutput/film.meta.jsonfor the default full and low renders.render.tsoverwrites that file after either render. If an old low render remains when a new full render finishes, this comparison always passes and ships the old film alongside the new one. Write and check metadata tied to each video before accepting the pair.