Internship - 과제 2
커스텀 비디오 플레이어
프로젝트 깃허브 링크
기능 구현 상세
컨트롤 바
- 컨트롤 바 on/off
- 영상 영역에서 마우스 이동시 컨트롤바 보이도록 구현
- 영상 영역에서 마우스가 사라질시 컨트롤바 제거
- 영상 영역에서 3초 동안 마우스가 아무 반응이 없을시 컨트롤바 제거
비디오 제어
- 재생/ 정지 기능
- 전체화면 ,축소 기능
- 영상 전체길이 표기
- 현재 보고있는 영상 시간 표기
- 프로그레스바 클릭시 해당 시간으로 넘기기
- 넘기는 도중 영상이 load 중이라면 로딩UI 표시
- keyDown event 이용해서 키보드 이벤트 구현
- 왼쪽 방향키 event - 영상 시간 -5초
- 오른쪽 방향키 event - 영상 시간 +5초
- 스페이스바 evnet - 영상 재생/정지 toggle
- 볼륨 조절 기능
- 볼륨 음소거 on/off기능
- 볼륨 아이콘 hover 시 볼륨 조절 기능 표시
광고 기능
- 특정 시간경과시 광고영상으로 교체되고 광고가 끝날 시 다시 원래 영상보던 시간으로 돌아오기
<hr/>
Challenging Points :
- forwardRef의 Props 타입 지정! 프랍으로 타입을 넘겨주는 것이 처음이라 낯설었던듯.
- 비디오의 정보가 업데이트될 때마다 전체 페이지가 리렌더링되지 않도록 컴포넌트 분리-설계 해야했던 점
- useFoarwardRef와 useImperativeHandle을 사용하여 자식 컴포넌트의 state를 형제 컴포넌트인 비디오 태그의 이벤트 핸들러가 제어하도록 구현
- 기본적으로 이벤트 핸들링이 많았다. onChange, onMouseEnter, onClick, onKeyPress 등등. 서로 충돌하는 이벤트 핸들러가 있어서 조금 고생했다.
공유하고 싶은 코드 :
1. 프로젝트 전체 컴포넌트 구조
player.tsx - 부모 컴포넌트
Video.tsx 바로가기 - 자식 컴포넌트
Controls.tsx 바로가기 - 자식 컴포넌트
useForwardRef를 설명하기 위해서는 필요한 내용이라서 공유
2.비디오 태그의 이벤트 핸들러 처리
const Video = () => {
const controllerRef = useRef<ControllerInterface>(null);
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const srcRef = useRef<HTMLSourceElement>(null);
// video source 링크
const srcOrigin =
"<http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4>";
const srcAd =
"<http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4>";
// container Props & handlers
const containerProps = {
ref: containerRef,
tabIndex: 0,
onKeyDown: (e: React.KeyboardEvent) => {
if (controllerRef.current) controllerRef.current.handleKeyDown(e);
},
onMouseEnter: () => {
if (controllerRef.current) controllerRef.current.handleMouseIn();
},
onMouseLeave: () => {
if (controllerRef.current) controllerRef.current.handleMouseLeave();
},
onMouseMove: (e: React.MouseEvent) => {
if (controllerRef.current) controllerRef.current.handleMouseMove(e);
},
};
// video Prop & handlers
const videoProps = {
ref: videoRef,
width: "100%",
controls: false,
onTimeUpdate: () => {
if (controllerRef.current) controllerRef.current.handleTimeUpdate();
},
onClick: () => {
if (controllerRef.current) controllerRef.current.handleVideoClick();
},
};
// Controls Props
const controlProps = {
ref: controllerRef,
containerRef: containerRef,
videoRef: videoRef,
srcRef: srcRef,
srcOrigin: srcOrigin,
srcAd: srcAd,
};
return (
<Layout>
<Container {...containerProps}>
<VideoWrapper {...videoProps}>
<source ref={srcRef} src={srcOrigin} type="video/mp4" />
</VideoWrapper>
<Controls {...controlProps} />
</Container>
</Layout>
);
};
export default Video;
const Container = styled.div`
position: relative;
&:focus {
border: none;
outline: none;
}
`;
const VideoWrapper = styled.video`
&::-webkit-media-controls {
display: none !important;
}
`;
3.Video 컴포넌트의 자식 컴포넌트인 Controls
핸들러 함수들과 useImperativeHandle 함수로 부모컴포넌트의 ref에 함수 올리기
// 비디오 재생 키보드 이벤트 핸들러
const handleKeyDown = (e: React.KeyboardEvent): void => {
switch (e.code) {
case "ArrowLeft":
videoElement!.currentTime -= 5;
break;
case "ArrowRight":
videoElement!.currentTime += 5;
break;
case "Space":
if (videoElement!.paused) {
videoElement!.play();
setIsPlaying(true);
} else {
videoElement!.pause();
setIsPlaying(false);
}
break;
default:
return;
}
};
// 비디오 클릭 시 재생/정지 핸들러
const handleVideoClick = () => {
if (videoElement) {
if (videoElement.paused) {
videoElement.play();
setIsPlaying(true);
} else {
videoElement.pause();
setIsPlaying(false);
}
}
};
// mouse event handlers - 마우스가 비디오 위에서 움직일 때 컨트롤바 보이게.
const handleMouseMove = (e: React.MouseEvent) => {
setShowControl(true);
setHideCursor(false);
setCoords({ x: e.screenX });
};
const handleMouseIn = () => {
setShowControl(true);
};
const handleMouseLeave = () => {
setShowControl(false);
};
// 동영상 시간 업데이트 핸들러
const handleTimeUpdate = () => {
setCurrent(videoElement?.currentTime || 0);
};
// 마우스 3초 이상 호버 시 컨트롤 바 안보이도록 타임아웃 useEffect
useEffect(() => {
const timeOut = setTimeout(() => {
setShowControl(false);
setHideCursor(true);
}, 3000);
return () => clearTimeout(timeOut);
}, [coords]);
// volume toggle에 현재 volume 업데이트 useEffect
useEffect(() => {
if (volumeRef && volumeRef.current) {
volumeRef.current.value = String(volume);
}
}, [isVolume]);
// 부모 컴포넌트로 핸들러 함수 올리기
useImperativeHandle(ref, () => ({
handleVideoClick,
handleKeyDown,
handleMouseIn,
handleMouseLeave,
handleMouseMove,
handleTimeUpdate,
}));
광고 기능
///// 광고 기능 /////
// 30초 이후 광고 불러오도록 트리거
useEffect(() => {
if (isAdPlayed) return;
if (30 < current) {
setAdTime({ ...adTime, originTime: current + 5, adLoaded: true });
}
}, [current]);
// 광고 안내 문구 마운트 & 5초 후 광고 로드
useEffect(() => {
if (srcElement && videoElement) {
setAdTime({ ...adTime, adPopUp: true });
let countdown = setInterval(
() => setAdCountDown((prev) => prev - 1),
1000
);
setTimeout(() => {
setAdTime({ ...adTime, adPopUp: false });
srcElement!.src = srcAd;
videoElement!.load();
videoElement!.play();
clearInterval(countdown);
}, 5000);
}
}, [adTime.adLoaded]);
// 광고 종료 이후 기존 비디오의 위치로 돌아가기
useEffect(() => {
if (isAdPlayed) return;
if (srcElement?.src === srcAd && current === totalTime) {
srcElement!.src = srcOrigin;
videoElement!.load();
videoElement!.currentTime = adTime.originTime;
videoElement!.play();
setIsAdPlayed(true);
}
}, [current]);
*이전 블로그에서 옮겨온 글입니다.