yt-custom-player
Version:
> A library for styling and customizing the YouTube player in React.
503 lines (441 loc) • 14.5 kB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import './styles.css';
import InitialOverlay from './InitialOverlay';
import Controls from './Controls/index';
const YouTubeEmbed = ({
videoId,
start = 0,
end = 0,
autoplay = false,
muted = false,
repeat = false,
showInicialOverlay = true,
showPlayPauseBtn = true,
showStopBtn = false,
showMuteBtn = true,
showProgressBar = true,
aspectRatio = "16:9",
fullScreen = true,
live = false,
action = null,
onTimestampUpdate = null,
}) => {
// caso o CMS forneça autoplay true e muted false, o navegador não deixará o vídeo ser reproduzido
// essa validação força o muted para true quando autoplay for true para reproduzir o vídeo
if (autoplay) {
muted = true
}
// lives não suportam tão bem funções de seek
if (live) {
showPlayPauseBtn = false
showStopBtn = true
}
const aspectRatios = {
"4:3": 4 / 3,
"16:9": 16 / 9,
"21:9": 21 / 9,
"9:16": 9 / 16,
"1:1": 1 / 1,
};
const [isFullscreenSupported, setIsFullscreenSupported] = useState(false);
const aspect = aspectRatios[aspectRatio] || aspectRatios["16:9"];
const videoRef = useRef(null);
const [isMuted, setIsMuted] = useState(muted);
const [showControls, setShowControls] = useState(false);
const [showLiveControl, setShowLiveControl] = useState(false);
const [isMouseMoving, setIsMouseMoving] = useState(false);
const [isPlaying, setIsPlaying] = useState(autoplay);
const [volume, setVolume] = useState(75);
const playerRef = useRef(null);
const [previousVolume, setPreviousVolume] = useState(volume);
const [showInitialOverlay, setShowInitialOverlay] = useState(showInicialOverlay === true);
const [progress, setProgress] = useState(0);
const [videoDuration, setVideoDuration] = useState(null);
const [loading, setLoading] = useState(true);
const [iconType, setIconType] = useState(loading ? 2 : 0);
const [isFullscreen, setIsFullscreen] = useState(false);
const containerRef = useRef(null);
const hideTimeoutRef = useRef(null);
const [userInteracted, setUserInteracted] = useState(false);
useEffect(() => {
const onYouTubeIframeAPIReady = () => {
const player = new window.YT.Player(videoRef.current, {
videoId: videoId,
playerVars: {
autoplay: autoplay ? 1 : 0,
controls: 0,
disablekb: 1,
playsinline: 1,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
modestbranding: 1,
enablejsapi: 1,
start: start,
end: end || undefined,
},
events: {
onReady: (event) => {
playerRef.current = event.target;
setLoading(false);
if (muted) {
event.target.mute();
setIsMuted(true);
} else {
event.target.setVolume(volume);
setIsMuted(false);
}
event.target.setVolume(volume);
if (autoplay) {
event.target.playVideo();
}
const duration = event.target.getDuration();
setVideoDuration(duration);
if (autoplay && muted) {
setIconType(2);
} else {
setIconType(1);
}
},
onStateChange: (event) => {
if (event.data === window.YT.PlayerState.ENDED) {
if (repeat) {
event.target.seekTo(start);
event.target.playVideo();
}
}
if (event.data === window.YT.PlayerState.PAUSED) {
setIsPlaying(false);
}
if (event.data === window.YT.PlayerState.PLAYING) {
setIsPlaying(true);
setTimeout(() => {
setShowInitialOverlay(false);
}, 750);
}
if (end > 0 && event.target.getCurrentTime() >= end) {
event.target.pauseVideo();
event.target.seekTo(start);
}
},
},
});
const interval = setInterval(() => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime();
const duration = (end || videoDuration) - start;
if (duration > 0) {
const adjustedCurrentTime = currentTime - start;
if (adjustedCurrentTime >= 0) {
setProgress((adjustedCurrentTime / duration) * 100);
// Aqui, chamamos o callback onTimestampUpdate com o timestamp atual
if (onTimestampUpdate) {
onTimestampUpdate(adjustedCurrentTime); // Passa o timestamp atual
}
}
}
}
}, 1000);
return () => clearInterval(interval);
};
if (!window.YT) {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
} else {
onYouTubeIframeAPIReady();
}
return () => {
if (playerRef.current) {
playerRef.current.destroy();
}
};
}, [videoId, start, end, autoplay, muted, videoDuration]);
useEffect(() => {
if (playerRef.current) {
playerRef.current.setVolume(volume);
setIsMuted(volume === 0);
}
}, [volume]);
// Verifica suporte ao fullscreen apenas se a prop fullScreen for verdadeira
useEffect(() => {
if (fullScreen) {
const fullscreenSupported =
document.fullscreenEnabled ||
document.mozFullScreenEnabled ||
document.webkitFullscreenEnabled ||
document.msFullscreenEnabled;
if (!fullscreenSupported) {
console.warn("Fullscreen not supported on this browser. Hiding fullscreen button.");
setIsFullscreenSupported(false);
} else {
setIsFullscreenSupported(true);
}
} else {
setIsFullscreenSupported(false);
}
}, [fullScreen]);
// ações de controle dinâmicas via prop
useEffect(() => {
if (!action || !playerRef.current) return; // Se nenhuma ação for passada, não faz nada
const player = playerRef.current;
switch (action) {
case 'play':
if (live) {
handleSeekLive();
player.playVideo();
setIsPlaying(true);
} else {
player.playVideo();
setIsPlaying(true);
}
break;
case 'pause':
player.pauseVideo();
setIsPlaying(false);
break;
case 'mute':
player.mute();
setIsMuted(true);
break;
case 'unmute':
player.unMute();
setIsMuted(false);
break;
case 'togglePlayPause':
if (isPlaying) {
player.pauseVideo();
setIsPlaying(false);
} else {
player.playVideo();
setIsPlaying(true);
}
break;
case 'toggleMute':
if (isMuted) {
player.unMute();
setIsMuted(false);
} else {
player.mute();
setIsMuted(true);
}
break;
default:
console.warn(`Ação desconhecida: ${action}`);
break;
}
}, [action]);
const handleFullscreenToggle = () => {
const container = containerRef.current;
if (!container) {
console.error("Container not found!");
return;
}
if (isFullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
setIsFullscreen(false);
} else {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
setIsFullscreen(true);
}
};
// ouvintes de evento para atualizar o estado do fullscreen caso o usuário saia via ESC
useEffect(() => {
const handleFullscreenChange = () => {
const isCurrentlyFullscreen =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement;
setIsFullscreen(!!isCurrentlyFullscreen);
};
// Adiciona o listener para mudanças de fullscreen
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
// Limpeza do evento ao desmontar
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
};
}, []);
const toggleMute = () => {
const player = playerRef.current;
if (player) {
if (isMuted) {
player.unMute();
player.setVolume(previousVolume);
setVolume(previousVolume);
} else {
setPreviousVolume(volume);
player.mute();
setVolume(0);
}
setIsMuted(!isMuted);
}
};
const togglePlayPause = () => {
const player = playerRef.current;
if (player) {
setUserInteracted(true);
if (isPlaying) {
player.pauseVideo();
} else {
player.playVideo();
}
}
};
const handleToggleControls = () => {
setShowControls(true);
clearTimeout(window.hideControlsTimeout); // Cancela qualquer temporizador anterior
window.hideControlsTimeout = setTimeout(() => {
setShowControls(false);
}, 3000); // Oculta os controles após 3 segundos
};
const handleVolumeChange = (event, newValue) => {
const player = playerRef.current;
if (isMuted) {
player.unMute();
}
setVolume(newValue);
};
const stopVideo = () => {
if (playerRef.current) {
playerRef.current.pauseVideo();
setIsPlaying(false);
setShowInitialOverlay(true);
setIconType(1);
}
};
const handleSeekLive = () => {
if (live) {
const duration = playerRef.current.getDuration();
playerRef.current.seekTo((duration + 999), true);
}
}
const handlePlayInitial = () => {
if (playerRef.current) {
handleSeekLive();
setIconType(2);
playerRef.current.playVideo();
setIsPlaying(true);
}
};
const handleSeek = (event) => {
const { clientWidth } = event.currentTarget;
const clickX = event.nativeEvent.offsetX;
const newProgress = (clickX / clientWidth) * 100;
const duration = (end || videoDuration) - start;
const newTime = (newProgress / 100) * duration + start;
if (playerRef.current) {
playerRef.current.seekTo(newTime, true);
}
};
// Detecta movimento do mouse dentro do contêiner
const handleMouseMove = () => {
setShowControls(true);
setShowLiveControl(true); // Mostra o LiveControl quando o mouse se move
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setShowControls(false);
setShowLiveControl(false); // Oculta o LiveControl após 3 segundos sem movimento
}, 3000);
};
const handleMouseLeave = () => {
clearTimeout(hideTimeoutRef.current);
setShowControls(false);
setShowLiveControl(false); // Oculta o LiveControl quando o mouse sai
};
return (
<div
ref={containerRef}
className="yt-video-container"
onMouseMove={handleMouseMove} // Movimento do mouse
onMouseLeave={handleMouseLeave} // Saída do mouse
onClick={handleToggleControls} // Mobile e Desktop
onTouchStart={handleToggleControls} // Mobile
style={{ paddingBottom: `calc(100% / ${aspect})` }}
>
<div ref={videoRef} className="video"></div>
<div
className="overlay"
onClick={
showStopBtn
? stopVideo // Aqui acionamos a função stopVideo ao clicar no overlay
: showPlayPauseBtn === true && !loading
? togglePlayPause
: undefined
}
></div>
{showInitialOverlay && (
<InitialOverlay
thumbnailUrl={`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`}
onPlay={handlePlayInitial}
iconType={iconType}
/>
)}
{/* showControls */}
{showControls && (
<Controls
isPlaying={isPlaying}
onTogglePlayPause={togglePlayPause}
progress={progress}
progressStart={start || 0}
progressEnd={end || videoDuration}
volume={volume}
isMuted={isMuted}
onMuteToggle={toggleMute}
onVolumeChange={handleVolumeChange}
showPlayPause={showPlayPauseBtn === true}
showStop={showStopBtn === true}
onStop={stopVideo}
showProgressBar={showProgressBar === true}
showVolumeControl={showMuteBtn === true}
onProgressBarClick={handleSeek}
isLive={live}
isFullscreen={isFullscreen}
onFullscreenToggle={handleFullscreenToggle}
showFullscreenButton={isFullscreenSupported}
/>
)}
</div>
);
};
YouTubeEmbed.propTypes = {
videoId: PropTypes.string.isRequired,
start: PropTypes.number,
end: PropTypes.number,
autoplay: PropTypes.bool,
muted: PropTypes.bool,
repeat: PropTypes.bool,
showInicialOverlay: PropTypes.bool,
showPlayPauseBtn: PropTypes.bool,
showMuteBtn: PropTypes.bool,
aspectRatio: PropTypes.string,
fullScreen: PropTypes.bool,
live: PropTypes.bool,
action: PropTypes.string,
};
export default YouTubeEmbed;