⚛️React | React-Player 라이브러리를 활용한 비디오 플레이어 커스텀 + 재생바 만들기

dayannne·2023년 12월 8일
5

React

목록 보기
7/13
post-thumbnail

React 프로젝트에서 유튜브 url을 갖고 커스텀한 비디오 플레이어를 구현하면서 react-player 라이브러리를 사용하게 되었다.

React-Player

영상 요소로 플레이어를 구현할 때 사용하는 라이브러리 중 하나이다.
다른 플레이어 라이브러리들 중 고민했는데,
react-player는 타입 정의 스크립트를 제공한다고 하여 이후 TypeScript로의 마이그레이션을 고려해 react-player를 선택했다.

그리고 위 사진은 react-player Demo 페이지인데 Demo 페이지 덕에 구현 가능한 옵션이랑 State 확인이 명확해서 좋았던 점도 있다ㅎ
무엇보다 재생 컨트롤바를 따로 커스텀해 만들어야 했는데 Seek 기능과 같이 구현이 가능해서 선택하게 되었다.

React Player Demo 페이지 바로가기

비디오 플레이어 커스텀 + 재생바 만들기

구현할 UI

구현할 기능

  1. 아래 동그란 재생 버튼을 통해 재생을 관리
  2. 영상 재생이 끝나면 다음 영상으로 자동 넘김
  3. 영상에 띄워지는 progress bar 만들기 - 재생시간 표시 및 재생위치 조절하기

1. React-Player 세팅하기

라이브러리 설치

npm install react-player

컴포넌트 세팅

import ReactPlayer from 'react-player';

// ...

export default function MusicPlayer(props) {
  const { playing, setPlaying, playlist } = props; // 상위 컴포넌트에 playing, setPlaying true로 정의
  const playerRef = useRef(null);
  const [ready, setReady] = useState(false);
  const [curr, setCurr] = useState(
    'https://youtu.be/sqgxcCjD04s?si=ePXJiYzUtjTZ7g_e',
  );

  const onEnded = () => {
    setCurr('https://youtu.be/ZXmoJu81e6A?si=cqMWOLxy-4PF0dxg');
    setPlaying(true);
  };

  return (
    <>
      <MusicPlayerWrap>
        <ReactPlayer
          url={curr} // 영상 url 삽입
          className='player' // 클래스 이름 지정하여 스타일 적용
          playing={playing} // 재생 상태, true = 재생중 / false = 일시 정지
          controls={false} // 유튜브 재생 컨트롤바 노출 여부
          onEnded={onEnded} // 현재 재생 중인 영상 종료 시 호출
          onReady={() => setReady(true)} // 영상이 로드되어 준비된 상태
        />
      </MusicPlayerWrap>
    </>
  );
}


const MusicPlayerWrap = styled.div`
  position: relative;
  border-radius: 10px;
  width: 328px;
  height: 180px;
  left: 50%;
  transform: translate(-50%, 0);
  margin: 80px 0 25px;
  z-index: 2;

  .player {
    position: absolute;
    top: 0%;
    left: 0px;
    width: 100%;
    height: 100%;
    border-radius: 10px;
    overflow: hidden;
  }
`;
  • import ReactPlayer from 'react-player';로 라이브러리를 불러온 후 ReactPlayer 컴포넌트 삽입, 옵션 설정
  • ReactPlayer를 div를 한번 감싸준 다음 클래스 이름(.player)을 지정해 스타일 적용
  • onEnded 호출 시 다음 영상으로 넘어갈 수 있도록 설정
    (url props에는 영상 링크 문자열로 이뤄진 배열 삽입도 가능하다. 배열 삽입 시에는 onEnded 설정 없이도 자동으로 영상이 넘어가는 기능이 적용되어 있어 이후 API 데이터 적용하면서 추가할 예정이다.)

이외에도 적용 가능한 옵션들은 react-player 깃허브에서 확인할 수 있다.
react-player Github 바로가기

2. 재생 버튼 만들기

ReactPlayer의 옵션 중 재생 상태를 관리하는 playing 옵션을 이용해서 재생버튼 클릭으로 영상 재생/일시중지 기능을 만들어 보았다.

우선 다른 버튼과 함께 재생 토글 버튼이 들어간 MusicPlayBar 컴포넌트를 만들어 주었다.

export default function MusicPlayBar(props) {
  const { playing, setPlaying } = props; // 상위 컴포넌트에 playing,setPlaying true로 정의
  const [isModalOpen, setIsModalOpen] = useState(false);

//...

  const handlePlayBtn = () => {
    if (playing === false) {
      setPlaying(true);
    } else {
      setPlaying(false);
    }
  };

  return (
    <PlayBarWrap>
      //...
      <PlayBtn onClick={handlePlayBtn}>
        <img
          src={playing === true ? PauseIcon : PlayIcon}
          alt='재생/멈춤 버튼'
        />
      </PlayBtn>
     //...
    </PlayBarWrap>
  );
}

MusicPlayer 컴포넌트와 마찬가지로 const { playing, setPlaying } = props;와 같이 props를 받아오고 있고,

export default function 상위컴포넌트(props) {
  const [playing,setPlaying] = useState(false)
  
 return(
   <>
    <MusicPlayer playing={playing} setPlaying={setPlaying} />
    <MusicPlayBar playing={playing} setPlaying={setPlaying} />
   </>
)
}

두 컴포넌트의 상위 컴포넌트에서 play 상태를 관리하는 playing이라는 useState 변수를 하나 만든 다음, 두 컴포넌트에 모두 props 설정을 해준 것이다.
그래서 MusicPlayBar에서의 재생 버튼 클릭 시 onClick 이벤트 함수가 호출되어setplaying 을통해 true/false값으로 변경시면서
MusicPlayer 컴포넌트 내 ReactPlayer의 playing 옵션이 제어되도록 했다.

3. 재생바(progressBar) 만들기

재생 버튼까지 잘 작동되었다면 이제 마지막으로 재생바(ProgressBar)를 만들어 보자!
ProgressBar에는 이미지와 같이 영상 재생위치를 확인 및 조절하는 1) 재생 막대와, 영상 2) 재생 시간을 표시해 주려 한다.

1) 재생바 - 재생 막대 + 재생위치 조절 기능 만들기

먼저 ReactPlayer 옵션 중 유튜브 재생 컨트롤바 노출 여부 옵션인 controlsfalse로 설정 후 진행한다.

Demo를 보면 'range' 타입의 input 태그로 재생 막대 기능이 구현되어 있다.
해당 input 태그를 props들까지 그대로 가져와 준다!

일단 'range' 타입인 input 태그 내 속성은 다음과 같다.

  • value : 현재 재생 위치(재생 시간)
  • min / max : 재생 위치의 최소 및 최대 값 (값은 가져온 그대로 사용할 것)
  • step : 재생 위치의 단계(증가 또는 감소)를 정의 (역시 그대로 사용)

사용자가 재생 포인트를 움직일 때마다 (재생 위치가 변경될 때) onChange 이벤트를 통해 영상(ReactPlayer)의 재생 위치재생 막대의 현재 재생 위치를 이동시키는 기능을 만들어 주자.

export default function MusicPlayer(props) {
  const { playing, setPlaying, playlist } = props;
  const [curr, setCurr] = useState(
    'https://youtu.be/sqgxcCjD04s?si=ePXJiYzUtjTZ7g_e',
  );
  const playerRef = useRef(null); // ReactPlayer의 ref 속성에 삽입해 메소드 제어 (변경된 재생 시간에 따른 실제 영상 재생 위치) 
  const [played, setPlayed] = useState(0); // 현재 재생 시간  (0부터 0.999999, 퍼센트로 표기된 총 재생 시간 대비 현재 시간 값)
  
  const onEnded = () => {
    setCurr('https://youtu.be/ZXmoJu81e6A?si=cqMWOLxy-4PF0dxg');
    setPlaying(true);
  };

  return (
    <>
      <MusicPlayerWrap>
        <ReactPlayer
          url={curr} 
          ref={playerRef} // 실제 영상 재생 위치 조절
          className='player'
          playing={playing} 
          controls={false} 
          width='100%'
          height='100%'
          onEnded={onEnded}
          onProgress={({ played }) => setPlayed(played)} // 현재 재생 시간
        />
        <input
          type='range'
          min='0'
          max='0.999999'
          step='any'
          value={played}
          style={{ '--progress': `${played * 100}%` }}
          onChange={(e) => {
            setPlayed(parseFloat(e.target.value)); // 재생 포인트 위치 실시간 변경
            playerRef.current.seekTo(parseFloat(e.target.value)); // 실제 영상 재생 위치 실시간 변경
          }}
        />
      </MusicPlayerWrap>
    </>
  );
}
  • played 값에 현재 재생 시간에 해당하는 값(float, 현재 재생 시간 / 총 재생 시간)을 저장해 재생 막대의 재생 포인트를 제어한다. (이후 현재 재생 시간을 영상에 표시하는 데에도 사용한다)
    초기값은 input의 min 속성과 같이 0으로 설정한 후,input 에서 onChange 이벤트 발생 마다 setPlayed로 현재 시간 값(value)을 업데이트한다. 그러면 played 값(현재 재생 시간)은 이렇게 실시간으로 변경된다.
  • playerRef : 실제 영상의 재생중인 위치를 변경하기 위해 만든 참조 변수이다.
    input에서 onChange 이벤트 발생마다 playerRef.current. seekTo() 로 현재 영상에 해당하는 ReactPlayer의 인스턴스에 접근한 다음, ReactPlayer 컴포넌트 내 존재하는seekTo() 재생 위치 변경 메소드를 사용해 현재 시간 값(value)을 적용해 준다.

2) 재생바 - 재생 시간 표시하기

다음으로는 재생 막대의 양 옆에 위치하는 '현재 재생 시간'과 '총 재생 시간'값을 표시해 주자.

  • '총 재생 시간' 값은 ReactPlayer 내 영상의 지속 시간을 반환해 주는 onDuration 함수로 가져온다.
  • '현재 재생 시간' 값은 played 값과 총 재생 시간 값을 활용한다.
  • 두 값이 예시 사진과 같이 분:초 형태로 보여지도록 하기 위해 시간 값을 분:초 형태로 리턴하는formatTime이라는 함수를 만들어 사용했다.
export default function MusicPlayer(props) {
  //...
  const [played, setPlayed] = useState(0); // 현재 재생 시간 (0부터 0.999999, 퍼센트로 표기된 총 재생 시간 대비 현재 시간 값)
  const [duration, setDuration] = useState(0); // 총 재생 시간
  
  // formatTime 함수 '분:초' 형태로 리턴
  function formatTime(seconds) {
    const minutes = Math.floor(seconds / 60);
    seconds = Math.floor(seconds % 60);
    return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
  }
  
  return (
    <>
      <MusicPlayerWrap>
        <ReactPlayer
          url={curr} 
          ref={playerRef}
          className='player'
          playing={playing} 
          controls={false} 
          width='100%'
          height='100%'
          onEnded={onEnded} 
          onDuration={setDuration} // 총 재생 시간
          o
          onProgress={({ played }) => setPlayed(played)} 
        />
        <ProgressBar> // 시간과 재생바를 감싸는 div 태그
          <time dateTime='P1S'>{formatTime(played * duration)}</time>
          <input
            type='range'
            min='0'
            max='0.999999'
            step='any'
            value={played}
            onChange={(e) => {
              setPlayed(parseFloat(e.target.value));
              playerRef.current.seekTo(parseFloat(e.target.value));
            }}
          />
          <time dateTime='P1S'>{formatTime(duration)}</time>
        </ProgressBar>
      </MusicPlayerWrap>
    </>
  );
}
  • 총 재생 시간 duration : onDuration을 통해 받아온 총 재생 시간 값은 아래처럼 '초' 단위의 정수로 되어 있다.

  • 현재 재생 시간 played * duration played 값은 0부터 0.9999까지의 float 형태이며 현재 재생 시간 / 총 재생 시간 에 해당한다. 현재 재생 시간 역시 초 단위로 가져오기 위해 played * duration로 사용한다.

  • 그리고 두 값을 format 함수에 넣어 분:초 형태로 return 한다.

3) 재생바 - 스타일링 (재생 막대, 재생 시간, 재생 포인트 버튼, 재생 진행도)

기능 구현 끝! 다음과 같이 스타일을 적용해 준다.

  • ReactPlayer에 position:relative속성을 준 다음, 재생 시간과 재생바를 감싼 태그에 position:absolute를 주어 영상 위에 재생바를 띄운다.
  • input 태그에 스타일을 적용해 재생 막대를 스타일링 한다.
  • 재생 포인트 버튼의 경우::-webkit-slider-thumb선택자로 스타일 적용이 가능하다.
  • 재생 진행도는 진행 시간 값을 이용하여 input의 background 스타일을 조정해 적용해 주었다.
  • 영상이 로드되기 전까지 재생바가 보이지 않도록 해주어야 한다. ReactPlayer 내 onReady 함수를 이용해 영상이 로드되었을 때 상태값을 true로 바꾸어 input의 disabled 를 처리한다.

스타일까지 적용한 코드는 다음과 같다.

export default function MusicPlayer(props) {
  //... 다른 변수들
const [ready, setReady] = useState(false); // onReady에서 영상이 로드된 상태값을 받아 사용

 //...
  
  return (
    <>
      <MusicPlayerWrap>
        <ReactPlayer
          url={curr} 
          ref={playerRef} 
          className='player'
          playing={playing} 
          controls={false} 
          width='100%'
          height='100%'
          onEnded={onEnded} 
          onReady={() => setReady(true)} // 영상이 로드되어 준비된 상태
          onDuration={setDuration} 
          onProgress={({ played }) => setPlayed(played)} 
        />
        <ProgressBar>
          <time dateTime='P1S'>{formatTime(played * duration)}</time>
          <input
            type='range'
            min='0'
            max='0.999999'
            step='any'
            value={played}
            disabled={!ready}
            style={{ '--progress': `${played * 100}%` }}
            onChange={(e) => {
              setPlayed(parseFloat(e.target.value));
              playerRef.current.seekTo(parseFloat(e.target.value));
            }}
          />
          <time dateTime='P1S'>{formatTime(duration)}</time>
        </ProgressBar>
      </MusicPlayerWrap>
    </>
  );
}

const MusicPlayerWrap = `
// ...MusicPlayerWrap 스타일
`
 
const ProgressBar = styled.div`
  position: absolute; // ReactPlayer 안으로 위치 설정
  bottom: 5px;
  width: 100%;
  padding: 8px;
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: var(--font-sm);
  color: #fff;
  &:disabled {
    display: none; // disabled 시 보이지 않도록
  }
  // 재생바 (막대기)
  input {
    width: 100%;
    height: 3px;
    border-radius: 10px;
    background: linear-gradient(
      to right,
      #fff var(--progress),
      rgba(250, 250, 250, 0.5) 0
    );

	// 재생 포인트 버튼
    &::-webkit-slider-thumb { 
      -webkit-appearance: none; /* 브라우저의 기본 스타일을 제거 */
      width: 10px; /* 재생 포인트의 너비를 설정 */
      height: 10px; /* 재생 포인트의 높이를 설정 */
      background: #fff; /* 재생 포인트의 배경색을 설정 */
      border-radius: 50%; /* 재생 포인트를 원형으로 만듬 */
    }
  }

4) 완성, 전체 코드

  • 상위 컴포넌트
export default function 상위컴포넌트(props) {
  const [playing,setPlaying] = useState(false)
  
 return(
   <>
    <MusicPlayer playing={playing} setPlaying={setPlaying} />
    <MusicPlayBar playing={playing} setPlaying={setPlaying} />
   </>
)
}
  • 재생 버튼
import React, { useState } from 'react';
import styled from 'styled-components';

//... import 컴포넌트, 이미지 

export default function MusicPlayBar(props) {
  const { playing, setPlaying } = props; // 상위 컴포넌트에 playing,setPlaying true로 정의
  const [isModalOpen, setIsModalOpen] = useState(false);

//...

  const handlePlayBtn = () => {
    if (playing === false) {
      setPlaying(true);
    } else {
      setPlaying(false);
    }
  };

  return (
    <PlayBarWrap>
      //...
      <PlayBtn onClick={handlePlayBtn}>
        <img
          src={playing === true ? PauseIcon : PlayIcon}
          alt='재생/멈춤 버튼'
        />
      </PlayBtn>
     //...
    </PlayBarWrap>
  );
}

//... MusicPlayBar 컴포넌트 요소 포함 재생 버튼 스타일링
  • 재생바
import React, { useState, useRef } from 'react';
import ReactPlayer from 'react-player';
import styled from 'styled-components';

export default function MusicPlayer(props) {
  const { playing, setPlaying, playlist } = props; // 상위 컴포넌트에 playing, setPlaying true로 정의
  const playerRef = useRef(null); // ReactPlayer의 ref 속성에 삽입해 메소드 제어 (변경된 재생 시간에 따른 실제 영상 재생 위치)
  const [played, setPlayed] = useState(0); // 현재 재생 시간 (0부터 0.999999, 퍼센트로 표기된 총 재생 시간 대비 현재 시간 값)
  const [duration, setDuration] = useState(0); // 총 재생 시간
  const [ready, setReady] = useState(false); // onReady에서 영상이 로드된 상태값을 받아 사용
  const [curr, setCurr] = useState(
    'https://youtu.be/sqgxcCjD04s?si=ePXJiYzUtjTZ7g_e',
  );

  const onEnded = () => {
    setCurr('https://youtu.be/ZXmoJu81e6A?si=cqMWOLxy-4PF0dxg');
    setPlaying(true);
  };

  // formatTime 함수 '분:초' 형태로 리턴
  function formatTime(seconds) {
    const minutes = Math.floor(seconds / 60);
    seconds = Math.floor(seconds % 60);
    return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
  }

  return (
    <>
      <MusicPlayerWrap>
        <ReactPlayer
          url={curr} // 링크 배열로 삽입 가능(종료 시 onEnded없이도 자동으로 다음 인덱스의 링크 재생)
          ref={playerRef} // 실제 영상 재생 위치 조절
          className='player'
          playing={playing} // 재생 상태, true - 재생중 / false - 일시 중지
          controls={false} // 유튜브 재생바 노출 여부
          width='100%'
          height='100%'
          onEnded={onEnded} // 현재 영상 종료 시
          onReady={() => setReady(true)} // 영상이 로드되어 준비된 상태
          onDuration={setDuration} // 총 재생 시간
          onProgress={({ played }) => setPlayed(played)} // 현재 재생 시간
        />
        <ProgressBar>
          <time dateTime='P1S'>{formatTime(played * duration)}</time>
          <input
            type='range'
            min='0'
            max='0.999999'
            step='any'
            value={played}
            disabled={!ready}
            style={{ '--progress': `${played * 100}%` }}
            onChange={(e) => {
              setPlayed(parseFloat(e.target.value)); // 재생 포인트 위치 실시간 변경
              playerRef.current.seekTo(parseFloat(e.target.value)); // 실제 영상 재생 위치 실시간 변경
            }}
          />
          <time dateTime='P1S'>{formatTime(duration)}</time>
        </ProgressBar>
      </MusicPlayerWrap>
    </>
  );
}

const MusicPlayerWrap = styled.div`
  position: relative;
  border-radius: 10px;
  width: 328px;
  height: 180px;
  left: 50%;
  transform: translate(-50%, 20%);
  z-index: 2;
  .player {
    position: absolute;
    top: 0%;
    left: 0px;
    width: 100%;
    height: 100%;
    border-radius: 10px;
    overflow: hidden;
  }
`;

const ProgressBar = styled.div`
  position: absolute;
  bottom: 5px;
  width: 100%;
  padding: 8px;
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: var(--font-sm);
  color: #fff;
  &:disabled {
    display: none;
  }
  input {
    width: 100%;
    height: 3px;
    border-radius: 10px;
    background: linear-gradient(
      to right,
      #fff var(--progress),
      rgba(250, 250, 250, 0.5) 0
    );

    &::-webkit-slider-thumb {
      -webkit-appearance: none; 
      width: 10px;
      height: 10px;
      background: #fff; 
      border-radius: 50%; 
    }
  }
`;
profile
☁️

1개의 댓글

comment-user-thumbnail
2024년 5월 24일

안녕하세요.
재생바 만드는데 많은 도움이 되었습니다.
감사합니다 ㅎㅎ

답글 달기