From 1b32ce4bdce091cb4e43d7b4f7ee3ed8c76a7770 Mon Sep 17 00:00:00 2001 From: Colby Fayock Date: Thu, 14 Mar 2024 12:02:14 -0400 Subject: [PATCH] starting to add native lzy loading to cldvideoplayer --- .../CldVideoPlayer/CldVideoPlayer.tsx | 249 +++++++++++------- .../CldVideoPlayer/CldVideoPlayer.types.ts | 1 + 2 files changed, 158 insertions(+), 92 deletions(-) diff --git a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx index 3818e7ad..1261332e 100644 --- a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx +++ b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx @@ -1,5 +1,5 @@ -import React, {useRef, MutableRefObject, useEffect} from 'react'; -import Script from 'next/script'; +import React, { useRef, MutableRefObject, useEffect} from 'react'; +import Script, { ScriptProps } from 'next/script'; import Head from 'next/head'; import { parseUrl } from '@cloudinary-util/util'; import { CloudinaryVideoPlayer, CloudinaryVideoPlayerOptionsLogo, CloudinaryVideoPlayerOptions, } from '@cloudinary-util/types'; @@ -15,7 +15,6 @@ let playerInstances: string[] = []; const PLAYER_VERSION = '1.10.6'; const CldVideoPlayer = (props: CldVideoPlayerProps) => { - const { autoplay, className, @@ -26,6 +25,7 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { id, language, languages, + loading = 'lazy', logo = true, loop = false, muted = false, @@ -44,10 +44,10 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { ...otherCldVidPlayerOptions } = props as CldVideoPlayerProps; + const shouldInitializeRef = useRef(false); const playerTransformations = Array.isArray(transformation) ? transformation : [transformation]; let publicId: string = src || ""; - // If the publicId/src is a URL, attempt to parse it as a Cloudinary URL // to get the public ID alone @@ -83,15 +83,19 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { } // Check if the same id is being used for multiple video instances. + const checkForMultipleInstance = playerInstances.filter((id) => id === playerId).length > 1 if (checkForMultipleInstance) { console.warn(`Multiple instances of the same video detected on the - page which may cause some features to not work. - Try adding a unique id to each player.`) + page which may cause some features to not work. + Try adding a unique id to each player.`) } else { playerInstances.push(playerId) } + + // Map callback handlers based on names for the player options + const events: Record = { error: onError, loadeddata: onDataLoad, @@ -101,6 +105,87 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { ended: onEnded }; + // Collect player configuration + + let logoOptions: CloudinaryVideoPlayerOptionsLogo = {}; + + if ( typeof logo === 'boolean' ) { + logoOptions.showLogo = logo; + } else if ( typeof logo === 'object' ) { + logoOptions = { + ...logoOptions, + showLogo: true, + logoImageUrl: logo.imageUrl, + logoOnclickUrl: logo.onClickUrl + } + } + + // Parse the value passed to 'autoplay'; + // if its a boolean or a boolean passed as string ("true") set it directly to browser standard prop autoplay else fallback to default; + // if its a string and not a boolean passed as string ("true") set it to cloudinary video player autoplayMode prop else fallback to undefined; + + let autoPlayValue: boolean | 'true' | 'false' = false; + let autoplayModeValue: string | undefined = undefined; + + if (typeof autoplay === 'boolean' || autoplay === 'true' || autoplay === 'false') { + autoPlayValue = autoplay + } + + if (typeof autoplay === 'string' && autoplay !== 'true' && autoplay !== 'false') { + autoplayModeValue = autoplay; + } + + let playerOptions: CloudinaryVideoPlayerOptions = { + autoplayMode: autoplayModeValue, + autoplay: autoPlayValue, + cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, + controls, + fontFace: fontFace || '', + language, + languages, + loop, + muted, + publicId, + transformation: playerTransformations, + ...logoOptions, + ...otherCldVidPlayerOptions + }; + + if ( Array.isArray(sourceTypes) ) { + playerOptions.sourceTypes = sourceTypes; + } + + if ( typeof colors === 'object' ) { + playerOptions.colors = colors; + } + + if ( typeof poster === 'string' ) { + // If poster is a string, assume it's either a public ID + // or a remote URL, in either case pass to `publicId` + playerOptions.posterOptions = { + publicId: poster + }; + } else if ( typeof poster === 'object' ) { + // If poster is an object, we can either customize the + // automatically generated image from the video or generate + // a completely new image from a separate public ID, so look + // to see if the src is explicitly set to determine whether + // or not to use the video's ID or just pass things along + if ( typeof poster.src !== 'string' ) { + playerOptions.posterOptions = { + publicId: getCldVideoUrl({ + ...poster, + src: publicId, + format: 'auto:image', + }) + }; + } else { + playerOptions.posterOptions = { + publicId: getCldImageUrl(poster) + }; + } + } + /** * handleEvent * @description Event handler for all player events @@ -117,106 +202,86 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { //Check if Cloud Name exists checkForCloudName(process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME); - /** - * handleOnLoad - * @description Stores the Cloudinary window instance to a ref when the widget script loads - */ + // Determine what value to use when loading the player script via Next Script + // Really the only time we'll be setting a value here is if we're using idle + // in which we'll use the Script tag's method instead of trying to run our own - function handleOnLoad() { - if ( 'cloudinary' in window ) { - cloudinaryRef.current = window.cloudinary; - let logoOptions: CloudinaryVideoPlayerOptionsLogo = {}; - - if ( typeof logo === 'boolean' ) { - logoOptions.showLogo = logo; - } else if ( typeof logo === 'object' ) { - logoOptions = { - ...logoOptions, - showLogo: true, - logoImageUrl: logo.imageUrl, - logoOnclickUrl: logo.onClickUrl - } - } + let scriptStrategy: ScriptProps['strategy'] | undefined = undefined; - // Parse the value passed to 'autoplay'; - // if its a boolean or a boolean passed as string ("true") set it directly to browser standard prop autoplay else fallback to default; - // if its a string and not a boolean passed as string ("true") set it to cloudinary video player autoplayMode prop else fallback to undefined; + // We're choosing a different definition of lazyloading here, where lazy loading + // will work more similarly to how images and iframes work, where we'll initialize + // the player closer to when it comes into view, so if we're defining loading + // as idle, we'll use lazyOnload which is on idle - let autoPlayValue: boolean | 'true' | 'false' = false; - let autoplayModeValue: string | undefined = undefined; + if ( ['idle', 'lazy'].includes(loading) ) { + scriptStrategy = 'lazyOnload'; + } - if (typeof autoplay === 'boolean' || autoplay === 'true' || autoplay === 'false') { - autoPlayValue = autoplay - } + useEffect(() => { + if ( !videoRef.current || loading !== 'lazy' ) return; - if (typeof autoplay === 'string' && autoplay !== 'true' && autoplay !== 'false') { - autoplayModeValue = autoplay; - } + const video = videoRef.current as Element; + const observer = new IntersectionObserver((entries, observer) => { + if ( entries[0].isIntersecting ) { + shouldInitializeRef.current = true; - let playerOptions: CloudinaryVideoPlayerOptions = { - autoplayMode: autoplayModeValue, - autoplay: autoPlayValue, - cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, - controls, - fontFace: fontFace || '', - language, - languages, - loop, - muted, - publicId, - transformation: playerTransformations, - ...logoOptions, - ...otherCldVidPlayerOptions - }; + if ( cloudinaryRef.current ) { + initializePlayer(); + } - if ( Array.isArray(sourceTypes) ) { - playerOptions.sourceTypes = sourceTypes; + observer.unobserve(video); } + }); + + observer.observe(video); - if ( typeof colors === 'object' ) { - playerOptions.colors = colors; - } + return () => { + observer.unobserve(video); + } + }, [videoRef.current, loading]); + + /** + * handleOnLoad + * @description Stores the Cloudinary window instance to a ref when the widget script loads + */ - if ( typeof poster === 'string' ) { - // If poster is a string, assume it's either a public ID - // or a remote URL, in either case pass to `publicId` - playerOptions.posterOptions = { - publicId: poster - }; - } else if ( typeof poster === 'object' ) { - // If poster is an object, we can either customize the - // automatically generated image from the video or generate - // a completely new image from a separate public ID, so look - // to see if the src is explicitly set to determine whether - // or not to use the video's ID or just pass things along - if ( typeof poster.src !== 'string' ) { - playerOptions.posterOptions = { - publicId: getCldVideoUrl({ - ...poster, - src: publicId, - format: 'auto:image', - }) - }; + function handleOnLoad() { + if ( 'cloudinary' in window ) { + cloudinaryRef.current = window.cloudinary; + + if ( loading === 'eager' || ( loading === 'lazy' && shouldInitializeRef.current) ) { + initializePlayer(); + } else if ( loading === 'idle' ) { + if ( 'requestIdleCallback' in window ) { + requestIdleCallback(() => initializePlayer()); } else { - playerOptions.posterOptions = { - publicId: getCldImageUrl(poster) - }; + setTimeout(() => initializePlayer(), 1); } } - playerRef.current = cloudinaryRef.current.videoPlayer(videoRef.current, playerOptions); - - Object.keys(events).forEach((key) => { - if ( typeof events[key] === 'function' ) { - playerRef.current?.on(key, handleEvent); - } - }); + shouldInitializeRef.current = true; } } - useEffect(() => { + /** + * initializePlayer + * @description Creates new player instance and sets player events + */ + + function initializePlayer() { + if ( !cloudinaryRef.current ) return; + + playerRef.current = cloudinaryRef.current.videoPlayer(videoRef.current, playerOptions); + + Object.keys(events).forEach((key) => { + if ( typeof events[key] === 'function' ) { + playerRef.current?.on(key, handleEvent); + } + }); + } + useEffect(() => { return () => { //@ts-ignore playerRef.current?.videojs.cloudinary.dispose(); @@ -234,7 +299,6 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { video: videoRef.current } } - return ( <> @@ -249,10 +313,11 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { height={height} />