-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Build a working show/hide app+phone demo video player
- Loading branch information
Showing
10 changed files
with
400 additions
and
8 deletions.
There are no files selected for viewing
This file contains 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
24 changes: 24 additions & 0 deletions
24
src/components/elements/direct-video-player/direct-video-player.styles.ts
This file contains 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,24 @@ | ||
'use client'; | ||
|
||
import { styled } from '@/styles'; | ||
|
||
export const StyledVideo = styled.video<{ | ||
$aspectRatio: string, | ||
$mounted: boolean | ||
}>` | ||
width: 100%; | ||
aspect-ratio: ${props => props.$aspectRatio}; | ||
border: none; | ||
${p => p.$mounted === false && | ||
// During SSR, we render both and use CSS only to show the right default | ||
` | ||
@media (prefers-color-scheme: dark) { | ||
&[data-hide-on-theme="dark"] { display: none; } | ||
} | ||
@media (prefers-color-scheme: light) { | ||
&[data-hide-on-theme="light"] { display: none; } | ||
} | ||
`} | ||
`; |
This file contains 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,123 @@ | ||
'use client'; | ||
|
||
import * as React from "react"; | ||
import { useTheme } from "next-themes"; | ||
|
||
import { useMounted } from '@/lib/hooks/use-mounted'; | ||
import { VideoCallback } from "@/lib/video-events"; | ||
|
||
import { | ||
darkPulLZoneId, | ||
lightPullZoneId, | ||
videoDictionary, | ||
VideoKey | ||
} from "@/content/data/video-dictionary"; | ||
|
||
import { StyledVideo } from "./direct-video-player.styles"; | ||
|
||
export const DirectVideoPlayer = (props: { | ||
videoId: VideoKey, | ||
eventListener?: VideoCallback, | ||
showControls?: boolean | ||
}) => { | ||
const { isMounted } = useMounted(); | ||
const { resolvedTheme: theme } = useTheme(); | ||
|
||
const { lightId, darkId, aspectRatio } = videoDictionary[props.videoId]; | ||
|
||
const dark720Url = `https://${darkPulLZoneId}.b-cdn.net/${darkId}/play_720p.mp4`; | ||
const light720Url = `https://${lightPullZoneId}.b-cdn.net/${lightId}/play_720p.mp4`; | ||
|
||
const darkVideoRef = React.useRef<HTMLVideoElement>(null); | ||
const lightVideoRef = React.useRef<HTMLVideoElement>(null); | ||
|
||
React.useEffect(() => { | ||
// Once we're mounted, there should always be at most one video element rendered: | ||
const videoElem = darkVideoRef.current || lightVideoRef.current; | ||
if (!videoElem || !props.eventListener) return; | ||
|
||
const abortController = new AbortController(); | ||
|
||
const videoAPI = { | ||
getCurrentTime() { | ||
return videoElem.currentTime; | ||
}, | ||
setCurrentTime(time: number) { | ||
videoElem.currentTime = time; | ||
}, | ||
play() { | ||
videoElem.play(); | ||
}, | ||
pause() { | ||
videoElem.pause(); | ||
} | ||
}; | ||
|
||
if (videoElem.readyState === 4) { | ||
props.eventListener('ready', videoAPI); | ||
} else { | ||
videoElem.addEventListener('canplaythrough', () => { | ||
props.eventListener?.('ready', videoAPI); | ||
}, { signal: abortController.signal }); | ||
} | ||
|
||
videoElem.addEventListener('play', () => { | ||
props.eventListener?.('play', videoAPI); | ||
}, { signal: abortController.signal }); | ||
|
||
videoElem.addEventListener('pause', () => { | ||
props.eventListener?.('pause', videoAPI); | ||
}, { signal: abortController.signal }); | ||
|
||
videoElem.addEventListener('seeked', () => { | ||
props.eventListener?.('seeked', videoAPI); | ||
}, { signal: abortController.signal }); | ||
|
||
videoElem.addEventListener('timeupdate', () => { | ||
props.eventListener?.('timeupdate', videoAPI); | ||
}, { signal: abortController.signal }); | ||
|
||
return () => abortController.abort(); | ||
}, [props.eventListener, darkVideoRef.current, lightVideoRef.current]); | ||
|
||
return <> | ||
{(!isMounted || theme === 'dark') && | ||
<StyledVideo | ||
controls={props.showControls ?? true} | ||
autoPlay | ||
loop | ||
muted | ||
|
||
ref={darkVideoRef} | ||
|
||
// During SSR, we show both but hide via CSS matching against system prefs: | ||
$mounted={isMounted} | ||
// Using this prop to css-hide for wrong theme | ||
data-hide-on-theme="light" | ||
|
||
$aspectRatio={aspectRatio} | ||
> | ||
<source src={dark720Url} type="video/mp4" /> | ||
</StyledVideo> | ||
} | ||
{(!isMounted || theme === 'light') && | ||
<StyledVideo | ||
controls | ||
autoPlay | ||
loop | ||
muted | ||
|
||
ref={lightVideoRef} | ||
|
||
// During SSR, we show both but hide via CSS matching against system prefs: | ||
$mounted={isMounted} | ||
// Using this prop to css-hide for wrong theme | ||
data-hide-on-theme="dark" | ||
|
||
$aspectRatio={aspectRatio} | ||
> | ||
<source src={light720Url} type="video/mp4" /> | ||
</StyledVideo> | ||
} | ||
</>; | ||
}; |
This file contains 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
This file contains 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
This file contains 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,66 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
|
||
import { PhoneWindow } from '@/components/elements/phone-window'; | ||
import { AppWindow } from '@/components/elements/app-window'; | ||
|
||
import { VideoKey, videoDictionary } from '@/content/data/video-dictionary'; | ||
|
||
import { DirectVideoPlayer } from '@/components/elements/direct-video-player'; | ||
import { useVideoLinking } from '@/lib/hooks/use-video-linking'; | ||
|
||
import { PairContainer } from './phone-app-video-pair.styles'; | ||
|
||
export const PhoneAppVideoPair = (props: { | ||
videoId: VideoKey | ||
}) => { | ||
const appVideo = videoDictionary[props.videoId]; | ||
|
||
const { phoneVideo } = appVideo; | ||
const phoneVideoKey = appVideo.phoneVideo?.key; | ||
|
||
const phoneVideoData = phoneVideo?.key | ||
? videoDictionary[phoneVideo?.key] | ||
: undefined; | ||
|
||
const [currentTime, setTime] = React.useState(0); | ||
|
||
const isPhoneVisible = phoneVideo && ( | ||
phoneVideo.startTime <= currentTime && | ||
phoneVideo.endTime >= currentTime | ||
); | ||
|
||
const phoneClassName = isPhoneVisible | ||
? 'phone visible-phone' | ||
: 'phone hidden-phone'; | ||
|
||
const { parentListener, childListener } = | ||
phoneVideo | ||
? useVideoLinking({ | ||
setTime, | ||
childOffset: phoneVideo?.startTime | ||
}) | ||
: {} as { parentListener: undefined, childListener: undefined }; | ||
|
||
return <PairContainer> | ||
{ phoneVideo && <> | ||
<PhoneWindow | ||
className={phoneClassName} | ||
aspectRatio={phoneVideoData!.aspectRatio} | ||
> | ||
<DirectVideoPlayer | ||
videoId={phoneVideoKey!} | ||
eventListener={childListener} | ||
showControls={false} | ||
/> | ||
</PhoneWindow> | ||
</> } | ||
<AppWindow> | ||
<DirectVideoPlayer | ||
videoId='chrome' | ||
eventListener={parentListener} | ||
/> | ||
</AppWindow> | ||
</PairContainer> | ||
}; |
39 changes: 39 additions & 0 deletions
39
src/components/modules/phone-app-video-pair/phone-app-video-pair.styles.ts
This file contains 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,39 @@ | ||
'use client'; | ||
|
||
import { screens, styled } from "@/styles"; | ||
|
||
export const PairContainer = styled.div` | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
justify-content: center; | ||
max-width: 100%; | ||
width: 100%; | ||
@media (min-width: ${screens['lg']}) { | ||
width: 1344px; | ||
height: 744px; | ||
} | ||
margin: 0 auto; | ||
padding: 0 10px; | ||
> .phone { | ||
max-width: min(20vw, 250px); | ||
transition: margin 0.5s, transform 0.5s; | ||
} | ||
> .hidden-phone { | ||
margin-right: calc(-1 * min(20vw, 250px)); | ||
transform: scale(80%); | ||
} | ||
> .visible-phone { | ||
margin-right: -5px; | ||
display: block; | ||
@media (min-width: ${screens['xl']}) { | ||
margin-right: 30px; | ||
} | ||
} | ||
`; |
This file contains 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
Oops, something went wrong.