Skip to content

Improve homepage brewery video reliability and replace fallback poster - #136

Merged
spizeck merged 3 commits into
mainfrom
fix/issue-135-homepage-video
Sep 24, 2026
Merged

spizeck merged 3 commits into
mainfrom
fix/issue-135-homepage-video

Conversation

@spizeck

@spizeck spizeck commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two-part fix for #135:

  1. Why users saw only a static image. Partly by design (mobile ≤768px and reduced-motion intentionally render only the poster), partly because Safari/iOS and older browsers fall through VP9 WebM to a 13.6MB, 5.1Mbps MP4 — heavy enough that canplay may never arrive on constrained connections, leaving the static state indefinitely. Autoplay denials were also silently swallowed. No codec/MIME/range/faststart defect was found — production delivery is verified correct (correct Content-Type, Accept-Ranges: bytes → 206, moov before mdat).

  2. Static experience now looks intentional. The brewery section poster is the owner-approved brewery still instead of a duplicate of the grain hero above it.

Closes #135

Changes

  • public/videos/ddbwebvid.mp4 — re-encoded H.264 High CRF 27, -movflags +faststart, no audio: 13.6MB → 2.1MB (−85%), SSIM 0.985 vs original.
  • public/videos/ddbwebvid.webm — re-encoded VP9 CRF 33, no audio: 4.3MB → 2.2MB (−48%), SSIM 0.990. Same 1280×720/24fps/21.25s. Source order unchanged (WebM first, MP4 fallback for Safari/iOS).
  • public/photos/video-still.jpg — new 78.7KB JPEG derivative of the approved video-still.png (1280×720, committed as the source).
  • components/hero-video.tsx — POSTER_SRC → video-still.jpg; dev-only console.info diagnostics on play() rejection (logs the DOMException name + MediaError code) and on the last <source> error, so genuine failures are distinguishable locally without sending expected conditions to monitoring. Preload→IntersectionObserver→play() sequence and canplay fade threshold verified correct and unchanged.
  • smoke-tests/hero-video.spec.ts — new (7 tests): still-vs-hero poster split, desktop source order/attributes, still preserved until readiness, refused play() leaves the design intact, no layout shift/overflow, mobile + reduced-motion static-only.
  • scripts/hero-video-check.mjs, docs/operations/performance.md — updated stale references/sizes.

Verification

  • npm ci · check:react-versions · tsc --noEmit · lint — pass
  • npm test — 355/355 · test:rules — 29/29 · build — pass
  • test:smoke — 130/130 (incl. 7 new tests)
  • check:md-links — pass · npm audit --omit=dev — 0 vulns
  • Chromium desktop: video plays (readyState 4, fading in) — verified live, not just DOM
  • Mobile 375px + reduced-motion: still only, no <video>, no overflow
  • ffprobe audit of both sources before/after; HTTP headers verified against production Vercel and local next start
  • Real Safari/iOS hardware — not available in this environment; H.264 MP4 + faststart + playsInline+muted is the correct coverage, and Playwright WebKit equivalence is noted as a limitation

Risk / deployment notes

Static public/ assets serve with must-revalidate (unhashed names) — repeat visits revalidate via ETag, so updated bytes deploy cleanly. No config, schema, or API changes.

Generated with Devin

Summary by Sourcery

Improve the brewery video experience by optimizing its media assets, using an intentional static still, and preserving graceful behavior when playback is unavailable.

New Features:

  • Add a distinct approved brewery still image for the video section’s static experience.
  • Add smoke coverage for poster selection, video behavior, graceful playback failure, responsive gating, and layout stability.

Bug Fixes:

  • Improve desktop brewery video reliability by reducing media payload sizes and keeping the poster visible until playback actually starts.
  • Add development-only diagnostics for autoplay rejection and complete media-source failure.

Enhancements:

  • Replace the duplicated grain hero poster with the brewery still and retain intentional still-only behavior on mobile and reduced-motion settings.

Documentation:

  • Update performance documentation and video checks to reflect the optimized assets and new brewery poster.

Tests:

  • Add seven smoke tests covering the brewery video’s visual, playback, responsive, and failure-state contracts.

- Re-encode both sources: MP4 H.264 Main→High CRF 27 faststart
  (13.6MB→2.1MB) and WebM VP9 CRF 33 (4.3MB→2.2MB), SSIM ≥0.98 vs the
  originals, no audio tracks, same 1280x720/24fps/21.25s.
- Swap the brewery section poster from the duplicated grain hero image
  to the approved video-still.jpg derivative (PNG source 678KB → JPEG
  78.7KB; the PNG remains the committed source).
- Log play() rejections and a fully-exhausted source list in development
  only — expected autoplay denials stay out of monitoring.
- Document the deliberate <=768px static policy and update the hero
  video diagnostic script + performance notes.
- Add hero-video.spec.ts: still-not-hero-poster split, desktop source
  order/attributes, still preserved until canplay, refused play() leaves
  the design intact, no layout shift, mobile + reduced-motion static.

Closes #135

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
deepdivebrewing-web Ready Ready Preview Sep 24, 2026 2:37pm UTC

Request Review

@sourcery-ai

sourcery-ai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR makes the brewery video substantially smaller and faster to load, replaces the duplicated grain poster with the approved brewery still, adds development-only playback diagnostics while preserving graceful static fallback behavior, and expands smoke coverage for desktop, mobile, reduced-motion, and failure scenarios.

Sequence diagram for reliable brewery video playback

sequenceDiagram
    participant Browser
    participant HeroVideo
    participant Video
    participant CDN

    Browser->>HeroVideo: IntersectionObserver enters view
    HeroVideo->>Video: set preload to metadata
    HeroVideo->>Video: play()
    Video->>CDN: Fetch WebM source
    alt WebM unsupported or fails
        Video->>CDN: Fetch MP4 fallback
    end
    alt canplay
        Video-->>HeroVideo: onCanPlay()
        HeroVideo-->>Browser: Fade from still to video
    else play() rejected or all sources fail
        Video-->>HeroVideo: Rejection or last source onError
        HeroVideo-->>Browser: Keep video-still.jpg visible
        HeroVideo-->>HeroVideo: Development-only console.info()
    end
Loading

Flow diagram for responsive brewery video behavior

flowchart TD
    A[Render brewery section] --> B{Reduced motion or viewport <= 768px?}
    B -->|Yes| C[Show video-still.jpg only]
    B -->|No| D[Render video with WebM then MP4 sources]
    D --> E{Section enters viewport?}
    E -->|No| F[Keep approved still visible]
    E -->|Yes| G["Set preload to metadata and call play()"]
    G --> H{Video reaches canplay?}
    H -->|Yes| I[Fade in video]
    H -->|No| C
    G -->|Rejected or sources fail| C
Loading

File-Level Changes

Change Details Files
Reduce hero video download cost while preserving browser fallback compatibility.
  • Re-encode the H.264 MP4 with faststart and the VP9 WebM without audio.
  • Retain WebM-first, MP4-fallback source ordering and existing dimensions, frame rate, and duration.
  • Update operational documentation with the new asset sizes and encoding details.
public/videos/ddbwebvid.mp4
public/videos/ddbwebvid.webm
docs/operations/performance.md
Replace the duplicated static poster with the approved brewery still.
  • Add a compressed JPEG derivative of the approved 1280×720 brewery still.
  • Point the video section at the new still while leaving the grain hero image unchanged.
  • Update the smoke-check script to identify the brewery still asset.
public/photos/video-still.jpg
components/hero-video.tsx
scripts/hero-video-check.mjs
Improve graceful playback failure behavior and local diagnostics.
  • Keep the still visible until video readiness and when autoplay or source playback fails.
  • Add development-only diagnostics for play rejection and final source failure.
  • Preserve desktop viewport/reduced-motion gating and the existing preload, observer, and fade-in flow.
components/hero-video.tsx
Add smoke coverage for the poster, media contract, responsive gating, and failure states.
  • Verify poster separation, source order/types, media attributes, and no layout shift or overflow.
  • Verify the still remains visible before readiness and after refused autoplay.
  • Verify mobile and reduced-motion modes render only the static still.
smoke-tests/hero-video.spec.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="smoke-tests/hero-video.spec.ts" line_range="76" />
<code_context>
+  const video = section.locator("video");
+  await expect(still).toBeVisible();
+  // Before readiness the video is transparent over the still.
+  if (!(await video.evaluate((v) => (v as HTMLVideoElement).readyState >= 4))) {
+    await expect(video).toHaveClass(/opacity-0/);
+  }
+  // Once the browser reports it can play, the video fades in — the still
</code_context>
<issue_to_address>
**issue (testing):** The test requires `readyState >= 4` before skipping the opacity assertion, but the component sets `canPlay` on `canplay`, which fires when the ready state is only `HAVE_FUTURE_DATA` (3). When playback reaches `canplay` with readyState 3, the test still expects `opacity-0` even though the component correctly renders `opacity-100`, causing a false failure.

**Triggers:** When headless playback reaches `canplay` before the video reaches `HAVE_ENOUGH_DATA`.

**Suggested fix:** Check the component's readiness condition (`readyState >= 3` or the `canplay` event) instead of requiring `readyState >= 4`.

```suggestion
  if (!(await video.evaluate((v) => (v as HTMLVideoElement).readyState >= 3))) {
```
</issue_to_address>

### Comment 2
<location path="smoke-tests/hero-video.spec.ts" line_range="79-89" />
<code_context>
+  if (!(await video.evaluate((v) => (v as HTMLVideoElement).readyState >= 4))) {
+    await expect(video).toHaveClass(/opacity-0/);
+  }
+  // Once the browser reports it can play, the video fades in — the still
+  // remains mounted underneath regardless.
+  await video
+    .evaluate(
+      (v) =>
+        new Promise<void>((resolve) => {
+          const el = v as HTMLVideoElement;
+          if (el.readyState >= 4) resolve();
+          else el.addEventListener("canplay", () => resolve(), { once: true });
+          setTimeout(resolve, 15000);
+        })
+    )
+    .catch(() => {});
</code_context>
<issue_to_address>
**issue (testing):** The readiness wait always resolves after 15 seconds without asserting that `canplay` occurred, and the surrounding `.catch(() => {})` suppresses failures. The test therefore passes even when the video never becomes playable, so it does not verify the documented fade-in/readiness behavior.

**Triggers:** When both video playback and the `canplay` event fail or are unavailable.

**Suggested fix:** Use a bounded Playwright expectation or reject the promise on timeout, then assert the expected opacity/readiness state separately.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: smoke-tests/hero-video.spec.ts:76, smoke-tests/hero-video.spec.ts:89


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread smoke-tests/hero-video.spec.ts Outdated
Comment thread smoke-tests/hero-video.spec.ts Outdated
Per review: the wait previously resolved on a timeout even if canplay
never arrived, and gated the opacity assertion on readyState 4 while the
component fades in at canplay (readyState 3). The test now waits for the
opacity-100 transition directly (bounded), so it genuinely fails if the
video never becomes playable.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

canplay fires even when play() is refused (the fetch proceeds), so the
old threshold could fade in a frozen first frame over the still. The
video now fades in on the playing event — actual rendered frames — so
the approved still remains whenever playback never starts.

The refused-play test now stubs the media route, leaving no in-flight
fetch to abort at teardown (CI requestfailed noise).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@spizeck
spizeck merged commit e1c3b1c into main Sep 24, 2026
4 checks passed
@spizeck
spizeck deleted the fix/issue-135-homepage-video branch September 24, 2026 14:44

This branch was successfully deployed

1 active deployment
Preview — 53ab22ba Deployed Sep 24, 2026 by vercel[bot]
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.

Improve homepage brewery video reliability and replace duplicate fallback poster

1 participant