Search code examples
javascripthtmlreactjsbrowservimeo-player

Vimeo, detect fullscreen to block fetching of new player (dynamic width changes)


I want to dynamically change the width of my vimeo player adapting to the window width. You can see the full code of the component at the end of the question and maybe there is already a way simpler approach to dynamically change the width of the player so I don't have to deal with the issues at all (The responsive option provided by vimeo does not work for me).

The issue I am having with my current solution: I don't want to trigger a change if the player goes into fullscreen or rotates the phone on fullscreen since vimeo already handles those changes automatically but I have a hard time to detect if the player is in fullscreen.

    const isFullscreen = (document.fullscreenElement
      || document.webkitFullscreenElement
      || document.mozFullScreenElement
      || document.msFullscreenElement
      || playerWidth === delayedWidth)

This solution breaks for iphone 11 pro and probably all Safari browsers since fullScreen is not fully implemented there.

import React, {
  useEffect, useContext, useState, useRef,
} from 'react';
import PropTypes from 'prop-types';
import Player from '@vimeo/player';

import STYLES from '../../enums/styles';
import { BrowserContext } from '../../contexts/BrowserContext';

const TAG = 'player';

/**
 * remove event listeners
 * @param {object} playerRef
 */
function removeEventListeners(playerRef) {
  if (!playerRef.current) return;
  playerRef.current.off('ended');
  playerRef.current.off('pause');
  playerRef.current.off('play');
}

/**
 * remove interval
 * @param {object} intervalRef
 */
function removeInterval(intervalRef) {
  if (!intervalRef.current) return;
  window.clearInterval(intervalRef.current);
}

/**
 * 640×480, 800×600, 960×720, 1024×768, 1280×960,
 * 1400×1050, 1440×1080 , 1600×1200, 1856×1392, 1920×1440, and 2048×1536
 * @param {} width
 */
function computeRatio(delayedWidth) {
  const height = window.innerHeight;
  const width = delayedWidth - delayedWidth * 0.1;

  if (height <= 480) {
    return width > 640 ? 640 : width;
  }
  if (height <= 600) {
    return width > 800 ? 800 : width;
  }
  if (height <= 720) {
    return width > 960 ? 960 : width;
  }
  if (height <= 768) {
    return width > 1024 ? 1024 : width;
  }
  if (height <= 960) {
    return width > 1280 ? 1280 : width;
  }
  if (height <= 1050) {
    return width > 1400 ? 1400 : width;
  }
  if (height <= 1080) {
    return width > 1440 ? 1440 : width;
  }
  if (height <= 1200) {
    return width > 1600 ? 1600 : width;
  }
  if (height <= 1392) {
    return width > 1856 ? 1856 : width;
  }
  if (height <= 1440) {
    return width > 1920 ? 1920 : width;
  }
  if (height <= 1536) {
    return width > 2048 ? 2048 : width;
  }
  return width;
}

const VideoPlayer = ({
  index, title, description, link, onProgress, latestProgress,
}) => {
  const { delayedWidth } = useContext(BrowserContext);
  const [time, setTime] = useState(latestProgress < 1 ? latestProgress : 0);
  const playerRef = useRef(null);
  const intervalRef = useRef(null);

  useEffect(() => {
    console.tag(TAG).debug('changing delayed width', delayedWidth);

    const asyncEffect = async () => {
      const player = playerRef.current;
      if (player) {
        const playerWidth = await player.getVideoWidth();
        const isFullscreen = document.fullscreenElement
        || document.webkitFullscreenElement
        || document.mozFullScreenElement
        || document.msFullscreenElement
        || playerWidth === delayedWidth;
        console.tag(TAG).debug('fullscreen detected', isFullscreen);

        const isMobile = window.innerWidth <= STYLES.breakpoints.phoneWidth;
        const isLandscape = window.innerWidth > window.innerHeight;

        if (isFullscreen || (isLandscape && isMobile)) {
          console.tag(TAG).debug('isLandscape, isMobile', isLandscape, isMobile);
          return;
        }

        removeEventListeners(playerRef);
        playerRef.current = null;
        player.pause();
        player.destroy();
      }

      if (intervalRef.current) {
        removeInterval(intervalRef);
        intervalRef.current = null;
      }

      const options = { id: link, width: computeRatio(delayedWidth) };
      const newPlayer = new Player(`frame-${title}-${index}`, options);
      playerRef.current = newPlayer;

      if (time) {
        newPlayer.getDuration().then((duration) => {
          const seconds = duration * time;
          newPlayer.setCurrentTime(seconds);
        });
      }

      const keepTrackProgress = async () => {
        // gets duration of video in seconds
        const duration = await newPlayer.getDuration();

        intervalRef.current = window.setInterval(() => {
          newPlayer.getCurrentTime().then((seconds) => {
            // `seconds` indicates the current playback position of the video
            const progress = seconds / duration;
            console.tag(TAG).debug(`progress: ${progress}, duration ${duration}, seconds ${seconds}`);
            onProgress(progress);
            setTime(progress);
          });
          // track every next 10 seconds of progress
        }, 10000);
      };

      newPlayer.on('ended', () => {
        removeInterval(intervalRef);
        intervalRef.current = null;
        onProgress(1);
        setTime(1);
      });

      newPlayer.on('pause', ({ duration, seconds }) => {
        removeInterval(intervalRef);
        intervalRef.current = null;
        const progress = seconds / duration;
        console.tag(TAG).debug(`progress at paused: ${progress}, duration ${duration}, seconds ${seconds}`);
        onProgress(progress);
        setTime(progress);
      });

      newPlayer.on('play', () => {
        keepTrackProgress();
      });
    };

    asyncEffect();
    return () => {
      removeInterval(intervalRef);
      removeEventListeners(playerRef);
    };
  }, [delayedWidth]);

  return (
    <div className="video-player">
      <div id={`frame-${title}-${index}`} className="frame-wrapper" />
      <div className="details">
        <h1>{title}</h1>
        <p>{description}</p>
      </div>
    </div>
  );
};


VideoPlayer.propTypes = {
  index: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired,
  link: PropTypes.string.isRequired,
  onProgress: PropTypes.func.isRequired,
  latestProgress: PropTypes.number.isRequired,
};

export default VideoPlayer;


Solution

  • The team behind the @vimeo/player package released new fullscreen methods! It works like a charm and solves my issue.

    If you are running into similar issues, have a look at the updated @vimeo/player documentation: https://www.npmjs.com/package/@vimeo/player#getfullscreen-promiseboolean-error

    My updated and tested code looks like this:

    import React, {
      useEffect, useContext, useState, useRef,
    } from 'react';
    import PropTypes from 'prop-types';
    import Player from '@vimeo/player';
    
    import { BrowserContext } from '../../contexts/BrowserContext';
    
    const TAG = 'player';
    
    /**
     * remove event listeners
     * @param {object} player
     */
    function removeEventListeners(player) {
      if (!player) return;
      player.off('ended');
      player.off('pause');
      player.off('play');
    }
    /**
     * remove interval
     * @param {number} interval
     */
    function removeInterval(interval) {
      console.tag(TAG).debug('removeInterval called');
      window.clearInterval(interval);
    }
    
    /**
     * 640×480, 800×600, 960×720, 1024×768, 1280×960,
     * 1400×1050, 1440×1080 , 1600×1200, 1856×1392, 1920×1440, and 2048×1536
     * @param {number} width
     */
    function computeRatio(delayedWidth, percentage = 0.9) {
      const height = window.innerHeight;
      const width = delayedWidth - (delayedWidth * (1 - percentage));
    
      if (height <= 480) {
        return width > 640 ? 640 : width;
      }
      if (height <= 600) {
        return width > 800 ? 800 : width;
      }
      if (height <= 720) {
        return width > 960 ? 960 : width;
      }
      if (height <= 768) {
        return width > 1024 ? 1024 : width;
      }
      if (height <= 960) {
        return width > 1280 ? 1280 : width;
      }
      if (height <= 1050) {
        return width > 1400 ? 1400 : width;
      }
      if (height <= 1080) {
        return width > 1440 ? 1440 : width;
      }
      if (height <= 1200) {
        return width > 1600 ? 1600 : width;
      }
      if (height <= 1392) {
        return width > 1856 ? 1856 : width;
      }
      if (height <= 1440) {
        return width > 1920 ? 1920 : width;
      }
      if (height <= 1536) {
        return width > 2048 ? 2048 : width;
      }
      return width;
    }
    
    const VideoPlayer = ({
      index, link, onProgress, latestProgress, widthPercentage, onVideoEnded,
    }) => {
      const { delayedWidth } = useContext(BrowserContext);
      const [progress, setProgress] = useState(latestProgress < 1 ? latestProgress : 0);
      const playerRef = useRef(null);
      const intervalRef = useRef(null);
    
      useEffect(() => {
        console.tag(TAG).debug('changing delayed width', delayedWidth);
    
        const asyncEffect = async () => {
          const player = playerRef.current;
          if (player) {
            console.tag(TAG).debug('player detected, checking fullscreen');
            const isFullscreen = await player.getFullscreen();
            console.tag(TAG).debug('fullscreen detected', isFullscreen);
    
            if (isFullscreen) {
              return;
            }
    
            removeEventListeners(player);
            playerRef.current = null;
            player.pause(); // gets rid of interval
            player.destroy();
          }
    
          const options = { id: link, width: computeRatio(delayedWidth, widthPercentage) };
          const newPlayer = new Player(`frame-${index}`, options);
          playerRef.current = newPlayer;
    
          if (progress) {
            newPlayer.getDuration().then((duration) => {
              const seconds = duration * progress;
              newPlayer.setCurrentTime(seconds);
            });
          }
    
          const keepTrackProgress = async () => {
            // gets duration of video in seconds
            const duration = await newPlayer.getDuration();
    
            intervalRef.current = window.setInterval(() => {
              const currentPlayer = playerRef.current;
              if (!currentPlayer) {
                return;
              }
              currentPlayer.getCurrentTime().then((seconds) => {
                // `seconds` indicates the current playback position of the video
                const newProgress = seconds / duration;
                console.tag(TAG).debug(`progress: ${newProgress}, duration ${duration}, seconds ${seconds}`);
                onProgress(newProgress);
                setProgress(newProgress);
              });
              // track every next 10 seconds of progress
            }, 10000);
          };
    
          newPlayer.on('ended', () => {
            console.tag(TAG).debug('player onEnded');
            removeInterval(intervalRef.current);
            intervalRef.current = null;
            onProgress(1);
            setProgress(1);
            onVideoEnded();
          });
    
          newPlayer.on('pause', ({ duration, seconds }) => {
            console.tag(TAG).debug('player onPause');
            removeInterval(intervalRef.current);
            intervalRef.current = null;
            const newProgress = seconds / duration;
            console.tag(TAG).debug(`progress at paused: ${newProgress}, duration ${duration}, seconds ${seconds}`);
            onProgress(newProgress);
            setProgress(newProgress);
          });
    
          newPlayer.on('play', () => {
            console.tag(TAG).debug('player onPlay');
            keepTrackProgress();
          });
        };
    
        asyncEffect();
      }, [delayedWidth]);
    
      useEffect(() => () => {
        removeInterval(intervalRef.current);
        removeEventListeners(playerRef.current);
        if (playerRef.current) {
          playerRef.current.destroy();
        }
      }, []);
    
      return (
        <div id={`frame-${index}`} className="frame-wrapper" />
      );
    };
    
    
    VideoPlayer.propTypes = {
      index: PropTypes.number.isRequired,
      link: PropTypes.string.isRequired,
      onProgress: PropTypes.func.isRequired,
      onVideoEnded: PropTypes.func,
      latestProgress: PropTypes.number.isRequired,
      widthPercentage: PropTypes.number,
    };
    
    VideoPlayer.defaultProps = {
      widthPercentage: 0.9,
      onVideoEnded: () => {},
    };
    
    export default VideoPlayer;