React 프로젝트에서 유튜브 url을 갖고 커스텀한 비디오 플레이어를 구현하면서 react-player
라이브러리를 사용하게 되었다.
영상 요소로 플레이어를 구현할 때 사용하는 라이브러리 중 하나이다.
다른 플레이어 라이브러리들 중 고민했는데,
react-player는 타입 정의 스크립트를 제공한다고 하여 이후 TypeScript로의 마이그레이션을 고려해 react-player를 선택했다.
그리고 위 사진은 react-player Demo 페이지인데 Demo 페이지 덕에 구현 가능한 옵션이랑 State 확인이 명확해서 좋았던 점도 있다ㅎ
무엇보다 재생 컨트롤바를 따로 커스텀해 만들어야 했는데 Seek 기능과 같이 구현이 가능해서 선택하게 되었다.
- 아래 동그란 재생 버튼을 통해 재생을 관리
- 영상 재생이 끝나면 다음 영상으로 자동 넘김
- 영상에 띄워지는 progress bar 만들기 - 재생시간 표시 및 재생위치 조절하기
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
컴포넌트 삽입, 옵션 설정.player
)을 지정해 스타일 적용onEnded
호출 시 다음 영상으로 넘어갈 수 있도록 설정url
props에는 영상 링크 문자열로 이뤄진 배열 삽입도 가능하다. 배열 삽입 시에는 onEnded 설정 없이도 자동으로 영상이 넘어가는 기능이 적용되어 있어 이후 API 데이터 적용하면서 추가할 예정이다.)이외에도 적용 가능한 옵션들은 react-player 깃허브에서 확인할 수 있다.
react-player Github 바로가기
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
옵션이 제어되도록 했다.
재생 버튼까지 잘 작동되었다면 이제 마지막으로 재생바(ProgressBar)를 만들어 보자!
ProgressBar에는 이미지와 같이 영상 재생위치를 확인 및 조절하는 1) 재생 막대와, 영상 2) 재생 시간을 표시해 주려 한다.
먼저 ReactPlayer 옵션 중 유튜브 재생 컨트롤바 노출 여부 옵션인 controls
를 false
로 설정 후 진행한다.
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, 현재 재생 시간 / 총 재생 시간)을 저장해 재생 막대의 재생 포인트를 제어한다. (이후 현재 재생 시간을 영상에 표시하는 데에도 사용한다)min
속성과 같이 0으로 설정한 후,input
에서 onChange 이벤트 발생 마다 setPlayed
로 현재 시간 값(value
)을 업데이트한다. 그러면 played
값(현재 재생 시간)은 이렇게 실시간으로 변경된다.playerRef
: 실제 영상의 재생중인 위치를 변경하기 위해 만든 참조 변수이다.input
에서 onChange 이벤트 발생마다 playerRef.current. seekTo()
로 현재 영상에 해당하는 ReactPlayer의 인스턴스에 접근한 다음, ReactPlayer 컴포넌트 내 존재하는seekTo()
재생 위치 변경 메소드를 사용해 현재 시간 값(value
)을 적용해 준다.다음으로는 재생 막대의 양 옆에 위치하는 '현재 재생 시간'과 '총 재생 시간'값을 표시해 주자.
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 한다.
기능 구현 끝! 다음과 같이 스타일을 적용해 준다.
position:relative
속성을 준 다음, 재생 시간과 재생바를 감싼 태그에 position:absolute
를 주어 영상 위에 재생바를 띄운다.::-webkit-slider-thumb
선택자로 스타일 적용이 가능하다. background
스타일을 조정해 적용해 주었다.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%; /* 재생 포인트를 원형으로 만듬 */
}
}
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%;
}
}
`;
안녕하세요.
재생바 만드는데 많은 도움이 되었습니다.
감사합니다 ㅎㅎ