Skip to content

Commit

Permalink
Merge pull request #207 from RandomStudio/video-blur-bg
Browse files Browse the repository at this point in the history
Blurred video background
  • Loading branch information
ewaperlinska authored Aug 21, 2023
2 parents c34b219 + 45cd588 commit a14b758
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 255 deletions.
27 changes: 18 additions & 9 deletions netlify/functions/getVideoThumbnail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ const getImage = async url => {
export const createBlurredImage = async (thumbnailUrl: string) => {
const image = await getImage(thumbnailUrl);

const buffer = await sharp(image)
const blurredSharpImage = await sharp(image)
.raw()
.ensureAlpha()
.resize(12, 12, { fit: 'inside' })
.toFormat(sharp.format.png)
.toBuffer();
.resize(400, 400, { fit: 'inside' })
.blur(20)
.toFormat(sharp.format.jpeg);

return buffer.toString('base64');
const { dominant } = await blurredSharpImage.stats();

const dominantColorString = `rgb(${Object.values(dominant).join(',')})`;

const thumbnailString = (await blurredSharpImage.toBuffer()).toString(
'base64',
);

return {
thumbnail: thumbnailString,
dominantColor: dominantColorString,
};
};

export const handler = async (event: HandlerEvent) => {
Expand All @@ -35,12 +46,10 @@ export const handler = async (event: HandlerEvent) => {
};
}

const imageString = await createBlurredImage(thumbnailUrl);
const imagesStrings = await createBlurredImage(thumbnailUrl);

return {
statusCode: 200,
body: JSON.stringify({
imageString,
}),
body: imagesStrings,
};
};
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,17 @@
"lint": "eslint \"src/**/*.{js,jsx,json,ts,tsx}\" --fix --cache --cache-location ~/.eslintcache/eslintcache"
},
"dependencies": {
"@types/video.js": "^7.3.52",
"@ctrl/tinycolor": "3.6.0",
"classnames": "^2.3.2",
"graphql-request": "5.1.0",
"hls.js": "1.4.10",
"lodash-es": "4.17.21",
"next": "^13.2.1",
"prop-types": "15.8.1",
"react": "^18.2.0",
"react-datocms": "4.0.9",
"react-markdown": "8.0.5",
"rehype-raw": "^6.1.1",
"swr": "2.2.1",
"video.js": "^8.3.0",
"zustand": "^4.3.9"
},
"devDependencies": {
Expand Down
14 changes: 8 additions & 6 deletions src/components/ProjectDetail/ContentBlock/ContentBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ const ContentBlock = ({

return (
<>
<Video
hasControls={hasControls}
isAutoplaying={autoplay}
isLooping={loops}
video={video}
/>
{video && (
<Video
hasControls={hasControls}
isAutoplaying={autoplay}
isLooping={loops}
video={video}
/>
)}

<Caption caption={caption} marginLeft={marginLeft} />
</>
Expand Down
3 changes: 0 additions & 3 deletions src/components/Video/Controls/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ const Controls = ({
videoRef.current?.src ?? 'unknown',
);

console.log('isMuted', isMuted);
console.log(videoRef.current?.src);

useEffect(() => {
if (!videoRef.current) {
return;
Expand Down
36 changes: 36 additions & 0 deletions src/components/Video/Video.module.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
.frame {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}

.video {
position: relative;
width: 100%;
height: 100%;
top: 0;
left: 0;
object-fit: cover;
opacity: 0;
transition: opacity 300ms ease-in;

.isLoaded & {
opacity: 1;
}
}

.brokenVideo {
Expand All @@ -9,3 +27,21 @@
height: 100%;
background: var(--light-grey);
}

.placeholder {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
filter: blur(5px);
transform: scale(1.1);
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transition: opacity 300ms ease-in;

.hasSizeData & {
opacity: 1;
}
}
128 changes: 78 additions & 50 deletions src/components/Video/Video.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
import useSwr from 'swr';
import { forwardRef } from 'react';
import React, {
MutableRefObject,
forwardRef,
useCallback,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import styles from './Video.module.scss';
import VideoContent from './VideoContent/VideoContent';
import LazyLoad from '../LazyLoad/LazyLoad';
import Controls from './Controls/Controls';
import { VideoData } from '../../types/types';
import { getVideoDetailsById, sanitiseVideoId } from '../../utils/videoUtils';
import useHlsVideo from './useHlsVideo';

type VideoProps = {
export type VideoProps = {
className?: string;
hasControls?: boolean;
isAutoplaying?: boolean;
isLooping?: boolean;
id?: string;
onClick?: () => void;
onReady?: () => void;
video?: VideoData;
};

// Fetcher function that fetches data from getVideoData netlify function
const fetcher = async (videoRef: string, video: VideoData) => {
if (video) {
return video;
}

// Some old videos are a full URL, rather than an ID
const id = sanitiseVideoId(videoRef);

const details = await getVideoDetailsById(id);

if (!details) {
console.error('Unable to retrieve video details for ID', id);

throw new Error('No details found for id');
}

return details;
video: VideoData;
};

const Video = forwardRef<HTMLVideoElement, VideoProps>(
Expand All @@ -42,49 +28,91 @@ const Video = forwardRef<HTMLVideoElement, VideoProps>(
className,
isAutoplaying,
hasControls,
id,
isLooping,
onClick,
onReady,
video,
video: { baseUrl, blur, height, hls, width },
},
ref,
) => {
const { data, error, isLoading } = useSwr(id, () => fetcher(id, video), {
fallbackData: video,
const localRef = useRef<HTMLVideoElement>();

const videoRef =
(ref as unknown as MutableRefObject<HTMLVideoElement>) || localRef;

const [isMounted, setIsMounted] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);

const handleMount = useCallback(() => {
setIsMounted(true);
}, []);

const handleVideoReady = useCallback(() => {
setHasLoaded(true);
}, []);

const aspectRatioStyle = { aspectRatio: `${width} / ${height}` };

useHlsVideo({
isAutoplaying,
isMounted,
onReady,
videoRef,
src: hls,
});

if (isLoading || error || !data) {
return (
<div className={`${styles.frame} ${styles.brokenVideo} ${className}`} />
);
}
const frameClasses = classNames(styles.frame, className, {
[styles.isLoaded]: hasLoaded,
[styles.hasSizeData]: width && height,
});

return (
<VideoContent
className={className}
hasControls={hasControls}
isAutoplaying={isAutoplaying}
isLooping={isLooping}
onClick={onClick}
onReady={onReady}
ref={ref}
video={data}
/>
<LazyLoad onIntersect={handleMount}>
<div className={frameClasses} style={aspectRatioStyle}>
<img
alt="video placeholder"
aria-hidden
className={styles.placeholder}
src={`data:image/jpeg;base64,${blur.thumbnail}`}
/>

{isMounted && (
<>
<video
autoPlay
className={styles.video}
controls={false}
data-download-src={`${baseUrl}/original`}
loop={isLooping}
muted
onClick={onClick}
onPlaying={handleVideoReady}
playsInline
ref={videoRef}
src=""
style={aspectRatioStyle}
/>

{hasControls && hasLoaded && (
<Controls isAutoplaying={isAutoplaying} videoRef={videoRef} />
)}
</>
)}
</div>
</LazyLoad>
);
},
);

Video.displayName = 'Video';

Video.defaultProps = {
className: null,
hasControls: false,
id: '',
isAutoplaying: true,
isLooping: true,
onClick: () => null,
onReady: () => null,
video: null,
};

Video.displayName = 'Video';

export default Video;
48 changes: 0 additions & 48 deletions src/components/Video/VideoContent/VideoContent.module.scss

This file was deleted.

Loading

0 comments on commit a14b758

Please sign in to comment.