skills/hubeiqiao/skills/responsive-video-source-selection

responsive-video-source-selection

Installation
SKILL.md

Responsive Video Source Selection

Problem

The HTML <source media="..."> attribute inside <video> elements does NOT work reliably across browsers for responsive video selection. Chrome/Blink and Firefox ignore the media attribute entirely, always selecting the first <source> that matches by type. Only Safari/WebKit respects it.

This means code like this only works in Safari:

<!-- BROKEN in Chrome & Firefox -->
<video controls>
  <source src="portrait.mp4" type="video/mp4" media="(max-width: 639px)" />
  <source src="landscape.mp4" type="video/mp4" />
</video>

Chrome and Firefox will always load portrait.mp4 (the first matching source) regardless of viewport width.

Context / Trigger Conditions

  • Building a video lightbox or player with different video files for mobile vs desktop
  • Using <source media="(max-width: ...)"> inside <video> and the wrong video loads
  • Portrait video loads on desktop, or landscape video loads on mobile
  • Works in Safari but fails in Chrome/Edge/Firefox
  • The media attribute was removed from the HTML spec in 2014, re-added in 2023, but Chrome and Firefox still don't implement it

Solution

Use JavaScript window.matchMedia() to select the correct source at render time:

React Pattern (Recommended)

const [isPortrait, setIsPortrait] = useState(false);

useEffect(() => {
  if (isOpen) {
    setIsPortrait(window.matchMedia('(max-width: 639px)').matches);
  }
}, [isOpen]);

const videoSrc = isPortrait ? portraitSrc : landscapeSrc;
const posterSrc = isPortrait ? portraitPoster : landscapePoster;

return (
  <video
    key={videoSrc}  // Force remount when source changes
    controls
    playsInline
    poster={posterSrc}
    preload="metadata"
  >
    <source src={videoSrc} type="video/mp4" />
  </video>
);

Key Details

  1. key={videoSrc} on the <video> element forces React to remount it when the source changes. Without this, swapping <source> children alone won't trigger a reload in all browsers.

  2. Evaluate at open/mount time, not continuously. Video source selection is a one-time decision per playback session (unlike images, you can't seamlessly swap video mid-stream without losing playback position).

  3. Select poster frame too — if you have responsive poster frames, select them with the same logic.

  4. Single <source> — since JS already selected the right file, you only need one <source> element (not two with media queries).

Vanilla JS Pattern

const mql = window.matchMedia('(max-width: 639px)');
const video = document.querySelector('video');
video.src = mql.matches ? 'portrait.mp4' : 'landscape.mp4';
video.load(); // Required to apply new source

Verification

  1. Open Chrome DevTools → Network tab
  2. Open the video player on desktop → verify landscape.mp4 loads (not portrait)
  3. Use DevTools responsive mode at 390px width → reload → verify portrait.mp4 loads
  4. Check the <source> element's src attribute in Elements panel

Example

From this project's VideoLightbox.tsx:

// Select video source based on viewport width at open time.
// <source media="..."> is unreliable in <video> across browsers,
// so we use JS-based selection instead.
const [isPortrait, setIsPortrait] = useState(false);

useEffect(() => {
  if (isOpen) {
    setIsPortrait(window.matchMedia('(max-width: 639px)').matches);
  }
}, [isOpen]);

const videoSrc = isPortrait ? portraitSrc : landscapeSrc;
const posterSrc = isPortrait ? portraitPoster : landscapePoster;

Notes

  • The media attribute on <source> works reliably inside <picture> — this issue is specific to <video> and <audio>.
  • The attribute was removed from the WHATWG HTML5 spec in 2014, re-added in 2023, but Chrome (Blink) and Firefox have not re-implemented support.
  • Safari/WebKit never removed support, so it works there. Don't be fooled by Safari-only testing.
  • For production video delivery at scale, consider HLS/DASH streaming which handles adaptive bitrate natively. The JS workaround is best for simple use cases (1-2 video variants).
  • Remember to test with window.matchMedia mock in unit tests:
function mockMatchMedia(matches: boolean) {
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: vi.fn().mockImplementation((query: string) => ({
      matches,
      media: query,
      onchange: null,
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    })),
  });
}

References

Weekly Installs
1
First Seen
7 days ago