From 398dd9b559a601927762f32e152a7c65666291de Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 29 Nov 2023 22:03:39 -0800 Subject: [PATCH 1/3] Fix console.log --- packages/react/src/VisualWrapper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/VisualWrapper.tsx b/packages/react/src/VisualWrapper.tsx index 9877067..d3d50e9 100644 --- a/packages/react/src/VisualWrapper.tsx +++ b/packages/react/src/VisualWrapper.tsx @@ -25,7 +25,6 @@ export default function VisualWrapper({ aspectCalculator: aspect, sourceMedia, image, video })) - console.log(aspectClasses, aspectStyleTag ) } else aspectRatio = aspect // Make the wrapper style. If expanding, use normal fill rules. Otherwise, From 3a7718179b5aba9661ae981bf478a8ae7848b73c Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 29 Nov 2023 22:04:33 -0800 Subject: [PATCH 2/3] Add @react-hook/media-query package --- packages/react/package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/react/package.json b/packages/react/package.json index af1adcf..a600ea3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -14,6 +14,7 @@ "test": "cypress run --component" }, "dependencies": { + "@react-hook/media-query": "^1.1.1", "react-intersection-observer": "^9" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index b36fffb..01aae2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -493,6 +493,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@react-hook/media-query@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@react-hook/media-query/-/media-query-1.1.1.tgz#7fc4e52591784a39be924b62b4270ae3e18ec578" + integrity sha512-VM14wDOX5CW5Dn6b2lTiMd79BFMTut9AZj2+vIRT3LCKgMCYmdqruTtzDPSnIVDQdtxdPgtOzvU9oK20LopuOw== + "@sanity/asset-utils@^1": version "1.3.0" resolved "https://registry.yarnpkg.com/@sanity/asset-utils/-/asset-utils-1.3.0.tgz#6460cd993a2c24368a6308028f3bc57df91f131e" From 6cca758bae20a31ad97f875c2a116c6dbb621512 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 29 Nov 2023 22:05:04 -0800 Subject: [PATCH 3/3] Switch video sources using JS --- .../react/cypress/component/LazyVideo.cy.tsx | 30 ++++++ .../cypress/component/ReactVisual.cy.tsx | 20 ++++ packages/react/src/LazyVideo.tsx | 97 +++++++++++++++---- 3 files changed, 127 insertions(+), 20 deletions(-) diff --git a/packages/react/cypress/component/LazyVideo.cy.tsx b/packages/react/cypress/component/LazyVideo.cy.tsx index d607fde..e22c0b5 100644 --- a/packages/react/cypress/component/LazyVideo.cy.tsx +++ b/packages/react/cypress/component/LazyVideo.cy.tsx @@ -40,3 +40,33 @@ describe('playback', () => { }) }) + +describe('responsive video', () => { + + it('supports switching sources based on media', () => { + cy.mount( { + if (media?.includes('portrait')) return src.portrait + else return src.landscape + }} + alt='Responsive video test' + />) + + // Portrait loaded initially + cy.get('video').its('[0].currentSrc').should('contain', 'portrait') + + // Switch to landscape + cy.viewport(500, 250) + cy.get('video').its('[0].currentSrc').should('contain', 'landscape') + + // Switch back to portrait again + cy.viewport(500, 600) + cy.get('video').its('[0].currentSrc').should('contain', 'portrait') + }) + +}) diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index caca052..446b2fe 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -200,6 +200,16 @@ describe('sources', () => { aspect: 1, } }} + video={{ + landscape: { + url: 'https://placehold.co/500x250.mp4?text=landscape+video', + aspect: 2, + }, + portrait: { + url: 'https://placehold.co/500x500.mp4?text=portrait+video', + aspect: 1, + } + }} sourceTypes={['image/webp']} sourceMedia={['(orientation: landscape)', '(orientation: portrait)']} imageLoader={({ src, type, media, width }) => { @@ -222,6 +232,10 @@ describe('sources', () => { return `https://placehold.co/${dimensions}${ext}?text=`+ encodeURIComponent(text) }} + videoLoader={({ src, media }) => { + return media?.includes('landscape') ? + src.landscape.url : src.portrait.url + }} aspect={({ image, media }) => { return media?.includes('landscape') ? image.landscape.aspect : @@ -239,6 +253,9 @@ describe('sources', () => { .should('contain', 'https://placehold.co/640x320') .should('contain', 'landscape') + // And a landscape video + cy.get('video').its('[0].currentSrc').should('contain', 'landscape') + // Check that the aspect is informing the size, not the image size cy.get('[data-cy=react-visual]').hasDimensions(500, 250) @@ -248,6 +265,9 @@ describe('sources', () => { .should('contain', 'https://placehold.co/640x640') .should('contain', 'portrait') + // And video + cy.get('video').its('[0].currentSrc').should('contain', 'portrait') + // Check aspect again cy.get('[data-cy=react-visual]').hasDimensions(500, 500) diff --git a/packages/react/src/LazyVideo.tsx b/packages/react/src/LazyVideo.tsx index 7d0a30f..b6ccda6 100644 --- a/packages/react/src/LazyVideo.tsx +++ b/packages/react/src/LazyVideo.tsx @@ -2,18 +2,24 @@ "use client"; import { useInView } from 'react-intersection-observer' -import { useEffect, type ReactElement, useRef, useCallback } from 'react' +import { useMediaQueries } from '@react-hook/media-query' +import { useEffect, type ReactElement, useRef, useCallback, type MutableRefObject } from 'react' import type { LazyVideoProps } from './types/lazyVideoTypes'; -import type { SourceMedia } from './types/reactVisualTypes' -import { makeSourceVariants } from './lib/sources' import { fillStyles, transparentGif } from './lib/styles' type VideoSourceProps = { src: Required['src'] videoLoader: LazyVideoProps['videoLoader'] - media?: SourceMedia } +type ResponsiveVideoSourceProps = Pick, + 'src' | 'videoLoader' | 'sourceMedia' +> & { + videoRef: VideoRef +} + +type VideoRef = MutableRefObject + // An video rendered within a Visual that supports lazy loading export default function LazyVideo({ src, sourceMedia, videoLoader, @@ -61,8 +67,10 @@ export default function LazyVideo({ // Simplify logic for whether to load sources const shouldLoad = priority || inView - // Make source variants - const sourceVariants = makeSourceVariants({ sourceMedia }) + // Multiple media queries and a loader func are necessary for responsive + const useResponsiveSource = sourceMedia + && sourceMedia?.length > 1 + && !!videoLoader // Render video tag return ( @@ -92,23 +100,72 @@ export default function LazyVideo({ }}> {/* Implement lazy loading by not adding the source until ready */} - { shouldLoad && sourceVariants.map(({ media, key }) => ( - - ))} + { shouldLoad && (useResponsiveSource ? + : + + )} ) } -// Make a video source tag. Note, media attribute on source isn't supported -// in Chrome. This will need to be converted to a JS solution at some point. -// https://github.com/BKWLD/react-visual/issues/35 +// Return a simple source element function Source({ - videoLoader, src, media -}: VideoSourceProps): ReactElement { - const srcUrl = videoLoader ? - videoLoader({ src, media }) : - src - return ( - - ) + src, videoLoader +}: VideoSourceProps): ReactElement | undefined { + let srcUrl + if (videoLoader) srcUrl = videoLoader({ src }) + else if (typeof src == 'string') srcUrl = src + if (!srcUrl) return + return () +} + +// Switch the video asset depending on media queries +function ResponsiveSource({ + src, videoLoader, sourceMedia, videoRef +}: ResponsiveVideoSourceProps): ReactElement | undefined { + + // Prepare a hash of source URLs and their media query constraint in the + // style expected by useMediaQueries + const queries = Object.fromEntries(sourceMedia.map(media => { + const url = videoLoader({ src, media }) + return [url, media] + })) + + // Find the src url that is currently active + const { matches } = useMediaQueries(queries) + const srcUrl = getFirstMatch(matches) + + // Reload the video since the source changed + useEffect(() => reloadVideoWhenSafe(videoRef), [ matches ]) + + // Return new source + return () +} + +// Get the URL with a media query match +function getFirstMatch(matches: Record): string | undefined { + for (const srcUrl in matches) { + if (matches[srcUrl]) { + return srcUrl + } + } +} + +// Safely call load function on a video +function reloadVideoWhenSafe(videoRef: VideoRef): void { + if (!videoRef.current) return + const video = videoRef.current + + // If already playing safely, load now + if (video.readyState >= 2) { + video.load() + + // Else, wait for video to finish loading + } else { + const handleLoadedData = () => { + video.load() + video.removeEventListener('loadeddata', handleLoadedData) + } + video.addEventListener('loadeddata', handleLoadedData) + } }