0

I just deployed an updated page in my React + Typescript SPA. This page has a carousel showing 5 out of a number of videos and autoscrolls the array of videos every 5 seconds unless the user is playing one. The page works well, or at least seems to do so. But since I deployed the page to production, the media server containing media files has crashed.

The production page gets a lot of real traffic, I guess that's why in test environment we didn't notice the problem.

My guess is that the carousel doesn't correctly unmount the video components so network requests stay "open" even the video is not showing in the carousel at that moment. But I'm not a "network guy".

This is my LandingPage component:

import { FC, useEffect, useState } from 'react';

//types
import { Testimonial } from './LandingPage.d';

// components
import SocietyInfoMobile from 'components/SocietyInfoMobile/SocietyInfoMobile';
import CtaBlock from './CtaBlock/CtaBlock.component';
import TestimonialCard from './TestimonialCard/TestimonialCard.component';
import TestimonialsCarousel from './TestimonialsCarousel';
import TestimonialsCarouselMobile from './TestimonialsCarousel/TestimonialsCarouselMobile'; 

// hooks
import useAppSelector from 'hooks/useAppSelector';
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";

//utils
import { getTestimonials } from './services/LandingPage.services';

//assets
import { ReactComponent as BulletIcon } from './assets/bulletIcon.svg';

//components
import LoadingMask from 'components/ui/LoadingMask';

// style
import { Page, TopCardsRow, ProfitsCard, BulletsCard, BulletsItem, BulletsBox, AllTestimonialsBox, AllTestimonialsTitle, AllTestimonials } from './style/LandingPage.style';


const LandingPage: FC<{}> = () => {
    const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
    const [isLoading, setIsLoading] = useState(true);

    const { isAppMobile } = useAppSelector( state => state.ui);
    const { executeRecaptcha } = useGoogleReCaptcha();

    const filterSortTestimonials = (testimonials: Testimonial[]) => {
        return [...testimonials
                .filter(t => t.visible && t.video)
            ]
    }

    const getAllTestimonials = async () => {
        try {
            if (!executeRecaptcha || typeof window === 'undefined') return;
            setTimeout(async () => {
                const reCaptchaResponse = await executeRecaptcha('testimonials');
                const response: Testimonial[] = await getTestimonials(reCaptchaResponse);
                console.log('response', response);
                setTestimonials(filterSortTestimonials(response));
                setIsLoading(false);
            }, 100); // Add a delay of 100ms or adjust accordingly
        } catch (error) {
            console.log(error);
        }
    }

    useEffect(() => {
        if (executeRecaptcha) {
            getAllTestimonials();
        }
    }, [executeRecaptcha]);

    return (
        <Page>
            <TopCardsRow>
                {/* Other stuff */}
            {isLoading && (
                    <LoadingMask
                        isCenter
                        size='big'
                    />
            )}

            {isAppMobile && !isLoading && (
                <TestimonialsCarouselMobile testimonials={testimonials}/>
            )}

            {!isAppMobile && !isLoading && (
                <TestimonialsCarousel testimonials={testimonials}/>
            )}
            
            {/* Other Stuff */}
        </Page>
    );
}

export default LandingPage;

which renders the TestimonialsCarousel component:

import { FC, useEffect, useRef, useState } from "react";

//types
import { TestimonialsCarouselProps} from './TestimonialsCarousel.component.d'

//components
import TestimonialCard from '../TestimonialCard/TestimonialCard.component'

// assets
import { ReactComponent as ArrowLeftSVG } from "./assets/arrow-left-icon.svg";
import { ReactComponent as ArrowRightSVG } from "./assets/arrow-right-icon.svg";

//style
import { CardBack, CardFront, CardWrapper, Carousel, Dot, Dots, NextButton, PrevButton, SliderWrapper, TestimonialsCarouselBox } from "./style/TestimonialsCarousel.component.style";
import { getCenteredSubArray } from "./TestimonialsCarousel.helper";

const TestimonialsCarousel: FC<TestimonialsCarouselProps> = ({ testimonials }) => {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [isVideoPaused, setIsVideoPaused] = useState(true);

  const intervalRef = useRef<NodeJS.Timeout>();

  const startInterval = () => {
    intervalRef.current = setInterval(() => {  
      handleNextClick();
    }, 5000);
  }

  const stopInterval = () => {
    clearInterval(intervalRef.current);
  }

  const handlePrevClick = () => {
    setCurrentSlide(currentSlide => currentSlide === 0 ? testimonials.length - 1 : currentSlide - 1);
    stopInterval();
    startInterval();
    setIsVideoPaused(true)
  };

  const handleNextClick = () => {  
    setCurrentSlide(currentSlide => currentSlide === testimonials.length - 1 ? 0 : currentSlide + 1);
    stopInterval();
    startInterval();
    setIsVideoPaused(true)
  };

  const handleClickCard = (title: string, cIndex: number) => { 
      cIndex > 0 ? handleNextClick() : handlePrevClick();
  }


  useEffect(() => {
    if (isVideoPaused) startInterval();
    if (!isVideoPaused) stopInterval();
  }, [isVideoPaused]);

  // const handleMouseEnter = () => {
  //   setIsHovering(true);
  // }
  
  // const handleMouseLeave = () => {
  //   setIsHovering(false);
  // }


  return (
    <TestimonialsCarouselBox >
        <Carousel>
            <PrevButton onClick={handlePrevClick}>
                <ArrowLeftSVG/>
            </PrevButton>

            <CardWrapper>
                {getCenteredSubArray(testimonials, currentSlide, 5).map((testimonial, index) => {
                return (
                    <TestimonialCard 
                      key={testimonial.description} 
                      testimonial={testimonial} 
                      cIndex={index-2} 
                      inCarousel={true} 
                      handleClickCard={handleClickCard}
                      setIsVideoPaused={setIsVideoPaused}
                    />
                );
                })}
            </CardWrapper>
            
            <NextButton onClick={handleNextClick}>
                <ArrowRightSVG/>
            </NextButton>
        </Carousel>
        <Dots>
            {getCenteredSubArray(testimonials, currentSlide, 7).map((testimonial, index) => {
                    return (
                        <Dot key={testimonial.description} cIndex={index-3} />
                    );
                    })}
        </Dots> 
    </TestimonialsCarouselBox>
  );
};



  export default TestimonialsCarousel;

which, finally, maps 5 testimonials array to TestimonialCard components:

import { FC, useEffect, useState } from 'react';

//hooks
import useAppSelector from 'hooks/useAppSelector';
import {useSwipeable} from 'react-swipeable'

// types
import { TestimonialCardProps } from './TestimonialCard.d';

//components
import VideoPlayerLight from 'components/VideoPlayerLight';

// style
import {
    TestimonialWrapper,
    VideoCard,
    InfoCard,
    TestimonialTitle,
    TestimonialTag,
    TestimonialInfo,
    TestimonialLabel,
    DarkOverlay,
    TestimonialProfilePic,
    TestimonialTagBox,
    TestimonialProfilePicBackground
} from "./style/TestimonialCard.style";


const TestimonialCard: FC<TestimonialCardProps> = ({
    testimonial,
    cIndex,
    inCarousel,
    handleClickCard,
    onSwipe,
    setIsVideoPaused
}) => {

    const [opacity, setOpacity] = useState(onSwipe !== undefined ? 0 : 1);

    // Handle null/undefined properties using optional chaining and default values
    const videoUrl = testimonial?.video?.url ?? '';
    const coverUrl = testimonial?.videoCover?.url ?? '';
    const profilePicUrl = testimonial?.profilePicture?.url ?? '';
    const title = testimonial?.title ?? '';
    const description = testimonial?.description ?? '';

    const { isAppMobile } = useAppSelector( state => state.ui);

    const onClickHandler = () => {
        if (handleClickCard && title && cIndex) handleClickCard(title, cIndex)
    }

    const swipeHandlers = useSwipeable({
        onSwipedLeft: () => {
            if (onSwipe)onSwipe('left');
        },
        onSwipedRight: () => {
            if (onSwipe) onSwipe('right');
        }});

    
    useEffect(() => {        
        setTimeout(() => {
            setOpacity(1);
          }, 50);
        return () => setOpacity(0);
    }, []);


    return (
        <TestimonialWrapper
            key={description}
            opacity={opacity}
            inCarousel={inCarousel} 
            cIndex={cIndex} 
            onClick={onClickHandler}
            {...swipeHandlers} // add swipe handlers
        >
            {cIndex !== undefined && !isAppMobile && <DarkOverlay cIndex={cIndex} /> }
            <VideoCard inCarousel={inCarousel}>
                <VideoPlayerLight videoUrl={videoUrl} posterUrl={coverUrl} setIsVideoPaused={setIsVideoPaused}/>
            </VideoCard>

            {/* Other stuff */}
           
        </TestimonialWrapper>
    );


};

export default TestimonialCard;

in each TestimonialCard, among other stuff, there is a VideoPlayerLight with a videoUrl and a posterUrl:

import { FC, useState, useRef } from 'react';

//types
import {VideoPlayerLightProps} from './VideoPlayerLight.component.d'
import { PlayButton, VideoElement, VideoPlayerContainer } from './style/VideoPlayerLight.component.style';

//assets
import { ReactComponent as PlayOverlayButtonSVG } from './assets/play-overlay-button.svg';


const VideoPlayerLight: FC<VideoPlayerLightProps> = ({ videoUrl, posterUrl, setIsVideoPaused }) => {
    const videoRef = useRef<HTMLVideoElement>(null);
    const [playing, setPlaying] = useState(false);
    const [showControls, setShowControls] = useState(false);
  
    const handlePlayButtonClick = () => {
      if (videoRef.current) {
        videoRef.current.play();
        setPlaying(true);
        setShowControls(true);        
        if (setIsVideoPaused) setIsVideoPaused(false);
      }
    };
  
    const handleVideoClick = () => {
      if (videoRef.current && !playing) {
        videoRef.current.play();
        setPlaying(true);
        setShowControls(true);
        if (setIsVideoPaused) setIsVideoPaused(false);
      }
    };
  
    return (
      <VideoPlayerContainer>
        <VideoElement
          ref={videoRef}
          src={videoUrl}
          poster={posterUrl}
          onClick={handleVideoClick}
          controls={showControls}
        />
        {!playing && (
            <PlayButton onClick={handlePlayButtonClick}>
                <PlayOverlayButtonSVG/>
            </PlayButton>
        )}
      </VideoPlayerContainer>
    );
  };

export default VideoPlayerLight;

Do you see any potential netowrk or memory leak problem with this set up?

Thanks in advance to any of you.

marcob8986
  • 153
  • 4
  • 12
  • Have you flicked through the slides and observed the network tab in the browser dev tools to see if it's true that they are still downloading data that it shouldn't be? Your theory is possible, but that's an easy way to prove/disprove it. If it it does not, it may well be the case your server cant handle the 1 video per client. If it does, then we can dig deeper as to why, Are the videos well-compressed? How big is each one in terms of file size? – adsy May 05 '23 at 19:03
  • Another aspect to consider is that if a video is started and then paused, it may continue to buffer. See https://stackoverflow.com/questions/4071872/html5-video-force-abort-of-buffering for a solution to that. – adsy May 05 '23 at 19:08
  • 1
    In addition, set `preload="none" ` on each video element. This will ensure they don't buffer before being played. Some (desktop) browsers do this, so you'd have a bit of data downloaded for each video (all 5) which would effectively 5x the bandwidth impact of every visitor before they clicked play, when compared before/after. – adsy May 05 '23 at 19:11
  • Id also encourage you to do the network pane test on a real phone in case that client is the problem. Mobiles typically have an issue with manipulating video controls as they are impose quite harsh restrictions intended to prevent malicious websites blasting imagery and sounds through your phone automatically. Repeat the test on iOS or Android using remote debugger. – adsy May 05 '23 at 19:17
  • Thanks @adsy. Using Firefox as a browser, I noticed that every time one new card gets added in the carousel two GET requests are made: one for the cover image (img initiator status 200) and one for the mp4 video (media initiator, status undefinite, 0B transferred). Then if click play on the video a new request on the same video is made. It stays in status undefinite but if I click on the timeline ahead the status become 206 PARTIAL CONTENT – marcob8986 May 06 '23 at 08:24
  • 1
    Preload none may help you here i think – adsy May 07 '23 at 01:42

0 Answers0