Boky
Boky

Reputation: 12064

How to play youtube video in iframe in react and get correct state changes

I have react component as follows:

import React, { Fragment, useEffect, useRef, useState } from 'react';
import { hasWindow } from '../utils.js';
import { Helmet } from 'react-helmet';

const PLATFORMS = {
  YOUTUBE: 'platform',
  BUNNY: 'bunny',
  VIMEO: 'vimeo'
};

const VideoExternal = ({ video, isOnScreen, item, context }) => {
  const [isPlaying, setIsPlaying] = useState(video.autoplay);
  const [isPaused, setIsPaused] = useState(false);
  const [isMuted, setIsMuted] = useState(video.autoplay);
  const [isVideoLoaded, setIsVideoLoaded] = useState(false);
  const [isVideoHovered, setIsVideoHovered] = useState(false);
  const [showLastImage, setShowLastImage] = useState(false);

  const iframeId = `iframe-${video.url}-${video.externalUrl}`;

  const iframeRef = useRef(null);
  const playerRef = useRef(null);

  const getPlatform = (url) => {
    if (url.includes('youtube') || url.includes('youtu')) return PLATFORMS.YOUTUBE;
    if (url.includes('b-cdn') || url.includes('mediadelivery')) return PLATFORMS.BUNNY;
    if (url.includes('vimeo')) return PLATFORMS.VIMEO;

    return null;
  };

  const getBaseUrl = (url) => {
    const urlObj = new URL(url);
    return `${urlObj.origin}${urlObj.pathname}`;
  };

  const addQueryParams = (url, platform, autoplay, loop) => {
    const urlObj = new URL(url);
    switch (platform) {
      case PLATFORMS.YOUTUBE:
        if (autoplay) urlObj.searchParams.set('autoplay', '1');
        if (loop) urlObj.searchParams.set('loop', '1');
        isMuted ? urlObj.searchParams.set('mute', '1') : urlObj.searchParams.set('mute', '0');
        urlObj.searchParams.set('controls', '0');
        urlObj.searchParams.set('enablejsapi', '1');
        break;
      case PLATFORMS.VIMEO:
        if (autoplay) urlObj.searchParams.set('autoplay', '1');
        if (loop) urlObj.searchParams.set('loop', '1');
        isMuted ? urlObj.searchParams.set('muted', '1') : urlObj.searchParams.set('muted', '0');
        urlObj.searchParams.set('controls', '0');
        break;
      case PLATFORMS.BUNNY:
        if (autoplay) urlObj.searchParams.set('autoplay', 'true');
        if (loop) urlObj.searchParams.set('loop', 'true');
        urlObj.searchParams.set('mute', `${isMuted} ? 'true' : 'false'`);
        urlObj.searchParams.set('controls', 'false');
        break;
      default:
        break;
    }
    return urlObj.toString();
  };

  const platform = getPlatform(video.externalUrl);
  const baseUrl = getBaseUrl(video.externalUrl, platform);
  const iframeUrl = addQueryParams(baseUrl, platform, video.autoplay, video.loop);

  const initializePlayer = (iframe) => {
    if (!iframe) return null;

    if (platform === PLATFORMS.YOUTUBE) {
      playerRef.current = new YT.Player(iframeId, {
        events: {
          onReady: () => {
            console.log('onReady');          
          },
          onStateChange: event => {
            if (!video.loop && event.data === YT.PlayerState.ENDED) {
                setIsPlaying(false);
                setIsMuted(true);
                setShowLastImage(true);
                console.log('Video ended.');  
            } 
          }
        }
      });
    } else if (platform === PLATFORMS.VIMEO) {
      playerRef.current = new Vimeo.Player(iframe);
    }
  };

  useEffect(() => {
    if (isOnScreen && isVideoLoaded) {
      const iframe = iframeRef?.current;
      // Create new player instance
      initializePlayer(iframe);
    }
  }, [isVideoLoaded, isOnScreen]);

  // Effect to reset playing and showing the last image on slide change
  useEffect(() => {
    setIsPlaying(video.autoplay);
    setIsMuted(video.autoplay);
    setShowLastImage(false);
  }, [isOnScreen]);

  // Effect to check when video is ended
  useEffect(() => {
    if (isOnScreen && isVideoLoaded && hasWindow()) {
      const iframe = iframeRef?.current;

      if (platform === PLATFORMS.YOUTUBE) {
      } else if (platform === PLATFORMS.VIMEO) {
        playerRef?.current?.on('finish', function () {
          if (!video.loop) {
            setIsPlaying(false);
            setIsMuted(true);
            setShowLastImage(true);
          }
        });
      } else if (platform === PLATFORMS.BUNNY) {
      }
    }
  }, [video.loop, isOnScreen, isVideoLoaded]);

  const handlePlayClick = () => {
    const iframe = iframeRef?.current;
    const iframeWindow = iframe?.contentWindow;

    if (iframe && isVideoLoaded) {

      if (platform === PLATFORMS.YOUTUBE)
        playerRef?.current?.playVideo();
      } else if (platform === PLATFORMS.VIMEO) {
        playerRef?.current?.play();
      } else if (platform === PLATFORMS.BUNNY && iframeWindow) {
        iframeWindow?.postMessage('{"event":"command","func":"play"}', '*');
      }

      setIsPaused(false);
      setIsPlaying(true);
      setIsMuted(false);
      setShowLastImage(false);
    }
  };   

  const isFirstImageVisible = !isPlaying && !isPaused && !showLastImage && video.firstImage !== '';
  const isLastImageVisible = !isPlaying && !isPaused && showLastImage && video.lastImage !== '';
  const isPlayButtonVisible = !isPlaying && video.playButtonVisible;      

  const renderFirstImage = () => {
    return <img src={video.firstImage} alt='Start of Video' className='rounded-16 h-full w-full absolute inset-0 object-cover' />;
  };

  const renderLastImage = () => {
    return <img src={video.lastImage} alt='End of Video' className='rounded-16 h-full w-full absolute inset-0 object-cover' />;
  };

  const renderPlayButton = () => {
    return <i className='fad fa-play-circle text-4xl md:text-6xl text-gray-200 hover:text-gray-100 cursor-pointer absolute z-1' onClick={handlePlayClick} />;
  };    
  

  const handleIframeLoad = () => {
    setIsVideoLoaded(true);
  };

  return (
    <Fragment>
      <Helmet>
        <script src='https://player.vimeo.com/api/player.js'></script>
        <script src='https://www.youtube.com/iframe_api'></script>
      </Helmet>
      <div className='rounded-16 h-full w-full relative flex items-center justify-center' onMouseEnter={() => setIsVideoHovered(true)} onMouseLeave={() => setIsVideoHovered(false)}>
        {isFirstImageVisible && renderFirstImage()}
        <iframe id={iframeId} ref={iframeRef} src={iframeUrl} allow='autoplay; fullscreen; picture-in-picture' title='Video' className='h-full w-full rounded-16' onLoad={handleIframeLoad}></iframe>
        {isPlayButtonVisible && renderPlayButton()}
        {isLastImageVisible && renderLastImage()}
      </div>
    </Fragment>
  );
};

export default VideoExternal;

When the component loads the video starts playing (video.autoplay is true), and when video is ended I see console.log('Video ended.');

When I then click on play button and (executes handlePlayClick function) and thus playerRef?.current?.playVideo(); is executed, the video starts playing, but when it ends I do not see console.log('isEnded: ', true).

Any idea why? Do I need to reinitialize the iframe or is there something else that I'm missing?

Thanks.

Upvotes: 0

Views: 102

Answers (1)

Siddhu Bhai
Siddhu Bhai

Reputation: 1

The issue seems to be related to the way the YouTube player instance is being initialized and handled. This is what you should try:

  1. Initialize the YouTube player instance outside the useEffect hook: Instead of initializing the YouTube player instance inside the useEffect hook, initialize it in the component itself. This ensures that the player instance is not recreated on every render, which might be causing the issue.
const VideoExternal = ({ video, isOnScreen, item, context }) => {
  // ...

  const [player, setPlayer] = useState(null);

  useEffect(() => {
    if (isOnScreen && isVideoLoaded && platform === PLATFORMS.YOUTUBE) {
      const iframe = iframeRef.current;
      if (iframe) {
        const newPlayer = new YT.Player(iframeId, {
          events: {
            onReady: () => {
              console.log('onReady');
            },
            onStateChange: (event) => {
              if (!video.loop && event.data === YT.PlayerState.ENDED) {
                setIsPlaying(false);
                setIsMuted(true);
                setShowLastImage(true);
                console.log('Video ended.');
              }
            },
          },
        });
        setPlayer(newPlayer);
      }
    }
  }, [isOnScreen, isVideoLoaded]);

  // ...
};
  1. Use the player state to control the video playback: Instead of using playerRef.current, use the player state to control the video playback.
const handlePlayClick = () => {
  const iframe = iframeRef?.current;
  const iframeWindow = iframe?.contentWindow;

  if (iframe && isVideoLoaded) {
    if (platform === PLATFORMS.YOUTUBE) {
      player?.playVideo();
    } else if (platform === PLATFORMS.VIMEO) {
      playerRef?.current?.play();
    } else if (platform === PLATFORMS.BUNNY && iframeWindow) {
      iframeWindow?.postMessage('{"event":"command","func":"play"}', '*');
    }

    setIsPaused(false);
    setIsPlaying(true);
    setIsMuted(false);
    setShowLastImage(false);
  }
};
  1. Clean up the player instance on component unmount: Add a useEffect hook with an empty dependency array to clean up the player instance when the component unmounts.
useEffect(() => {
  return () => {
    if (player) {
      player.destroy();
    }
  };
}, []);

Upvotes: 0

Related Questions