teklog

인턴 회고록 pt.2

2022/09/06

n°10

category : Recap

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


*이전 블로그에서 옮겨온 글입니다.