Skip to content

Commit

Permalink
Build a working show/hide app+phone demo video player
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Aug 23, 2024
1 parent 35efa72 commit 1513966
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 8 deletions.
4 changes: 4 additions & 0 deletions src/app/(integrations)/android/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IntegrationBento } from '@/components/sections/integration/single-page/
import { IntegrationDeviceMedia } from '@/components/sections/integration/single-page/device-media';
import { IntegrationSinglePageHero } from '@/components/sections/integration/single-page/hero';
import { buildMetadata } from '@/lib/utils/build-metadata';
import { PhoneAppVideoPair } from '@/components/modules/phone-app-video-pair';

export const metadata: Metadata = buildMetadata({
title: 'Intercept, mock & debug Android HTTP traffic',
Expand All @@ -27,6 +28,9 @@ export default function AndroidIntegrationPage() {
icon={AndroidLogo}
breadcrumbText="android"
/>
<PhoneAppVideoPair
videoId='android'
/>
<IntegrationDeviceMedia
mobileImage={{
darkSrc: '/images/mobile-placeholder-dark.png',
Expand Down
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; }
}
`}
`;
123 changes: 123 additions & 0 deletions src/components/elements/direct-video-player/index.tsx
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>
}
</>;
};
4 changes: 2 additions & 2 deletions src/components/elements/phone-window/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
export const PhoneWindow = (p: {
aspectRatio: string;
children: React.ReactNode;
}) =>
<PhoneOutline>
className?: string;
}) => <PhoneOutline className={p.className}>
<PhoneScreen $aspectRatio={p.aspectRatio}>
{ p.children }
</PhoneScreen>
Expand Down
2 changes: 0 additions & 2 deletions src/components/elements/phone-window/phone-window.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export const PhoneOutline = styled.div`
box-sizing: border-box;
position: relative;
max-width: 250px;
--phone-border-radius: 10px;
border-radius: var(--phone-border-radius);
padding: 14px 0 18px;
Expand Down
66 changes: 66 additions & 0 deletions src/components/modules/phone-app-video-pair/index.tsx
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>
};
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;
}
}
`;
34 changes: 30 additions & 4 deletions src/content/data/video-dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export interface VideoData {
lightId: string;
darkId: string;
aspectRatio: string;
phoneVideo?: {
key: VideoKey;
startTime: number;
endTime: number;
};
}

export const lightLibraryId = '293624';
Expand All @@ -11,7 +16,15 @@ export const lightPullZoneId = 'vz-232e9c2f-cfb';
export const darkLibraryId = '77143';
export const darkPulLZoneId = 'vz-7ada43d8-9d3';

export const videoDictionary = {
export type VideoKey =
| 'chrome'
| 'javascript'
| 'python'
| 'ruby'
| 'android'
| 'android-device';

export const videoDictionary: Record<VideoKey, VideoData> = {
'chrome': {
darkId: 'de631d3f-c4dd-4bcb-bb16-c5242e815c57',
lightId: '91477e53-40dd-4290-baef-cb9d9be6de8d',
Expand All @@ -31,7 +44,20 @@ export const videoDictionary = {
darkId: '01483aac-60a5-4611-bc4a-c3cba9310033',
lightId: '7ae7d7f5-9833-416f-aa3a-6c67e524de6e',
aspectRatio: '16/9'
},
'android': {
darkId: 'de631d3f-c4dd-4bcb-bb16-c5242e815c57',
lightId: '91477e53-40dd-4290-baef-cb9d9be6de8d',
aspectRatio: '16/9',
phoneVideo: {
key: 'android-device',
startTime: 5,
endTime: 10
}
},
'android-device': {
darkId: '40645bae-535e-422f-9a28-de02741f05a6',
lightId: '13820f1f-6a0f-4a84-82c2-c6c2e5eb460b',
aspectRatio: '9/20'
}
} as const;

export type VideoKey = keyof typeof videoDictionary;
} as const;
Loading

0 comments on commit 1513966

Please sign in to comment.