두번째 인턴쉽 과제 회고

이택우·2022년 5월 31일
1

프로젝트 회고

목록 보기
4/4

Internship - 과제 2

커스텀 비디오 플레이어

프로젝트 깃허브 링크

기능 구현 상세

컨트롤 바

  • 컨트롤 바 on/off
  • 영상 영역에서 마우스 이동시 컨트롤바 보이도록 구현
  • 영상 영역에서 마우스가 사라질시 컨트롤바 제거
  • 영상 영역에서 3초 동안 마우스가 아무 반응이 없을시 컨트롤바 제거

비디오 제어

  • 재생/ 정지 기능
  • 전체화면 ,축소 기능
  • 영상 전체길이 표기
  • 현재 보고있는 영상 시간 표기
  • 프로그레스바 클릭시 해당 시간으로 넘기기
    • 넘기는 도중 영상이 load 중이라면 로딩UI 표시
  • keyDown event 이용해서 키보드 이벤트 구현
    • 왼쪽 방향키 event - 영상 시간 -5초
    • 오른쪽 방향키 event - 영상 시간 +5초
    • 스페이스바 evnet - 영상 재생/정지 toggle
  • 볼륨 조절 기능
  • 볼륨 음소거 on/off기능
  • 볼륨 아이콘 hover 시 볼륨 조절 기능 표시

광고 기능

  • 특정 시간경과시 광고영상으로 교체되고 광고가 끝날 시 다시 원래 영상보던 시간으로 돌아오기

Challenging Points :

  • forwardRef의 Props 타입을 지정
  • 비디오의 정보가 업데이트될 때마다 전체 페이지가 리렌더링되지 않도록 컴포넌트 분리-설계 해야했던 점
  • useFoarwardRef와 useImperativeHandle을 사용하여 자식 컴포넌트의 state를 형제 컴포넌트인 비디오 태그의 이벤트 핸들러가 제어하도록 구현

코드

프로젝트 전체 컴포넌트 구조

player.tsx - 부모 컴포넌트

Video.tsx 바로가기 - 자식 컴포넌트

Controls.tsx 바로가기 - 자식 컴포넌트

비디오 태그의 이벤트 핸들러 처리

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;
  }
`;

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]);

0개의 댓글