티스토리 뷰
이번에는 유저들의 면접 영상을 재생할 때 구현했던 플레이어 제작 과정을 소개하고자 한다.
플레이어를 제작하기 위한 사용한 라이브러리는 React Player이다.
다양한 API를 제공하고, 많은 개발자가 사용하며 최근까지도 업데이트가 활발이 되고 있어 선택하게 되었다.
추가적으로 플레이어에서 재생바 역할을 하는 부분만 스타일 잡는데 줘야 할 값들이 너무 많아 시간이 오래 소요될 것을 우려하여 mui의 Slider를 가져와서 사용하였다.
플레이어에 있는 기능은 Youtube와 Vimeo에 있는 기능을 넣으려고 노력하였다.
구현한 기능은 아래와 같다.
1. 재생, 멈춤
2. 3초 앞, 3초전
3. 소리 조절 + 음소거
4. 배속 (0.5배속, 1배속, 1.5배속, 2배속)
5. 전체화면
6. 재생시간 툴팁으로 표시
7. 좋아요, 스크랩 동영상 내에서 처리
8. 하이라이트 Top 3 시간 누르면 해당 시간으로 이동
+ TroubleShooting
초기 세팅
전체 코드는 여기서 확인할 수 있다. > 링크
필요한 기능을 하나 둘 넣다 보니 Player에서 제공하는 모든 API를 다 쓴 것 같다. 우선 Video 폴더에서 파일 관리는 아래와 같이 이뤄진다.
- Video.jsx : Player state 관리, 필요한 함수 생성 및 전반적 레이아웃 배치
- VideoControl.jsx : Video안쪽에 absolute로 위치한 Player controller 하는 기능 모음
- Bubble.jsx : 좋아요 시 발생하는 애니메이션
Video.jsx 에서는 전반적으로 비디오가 배치될 레이아웃을 잡고 Player에 들어갈 state를 선언 및 관리한다. 또 state를 이용해 필요한 기능의 함수를 만들고 그 함수들은 VideoControl component에 전달하는 역할을 하는 곳이다.
state 관리
const [state, setState] = useState({
playing: true, // 재생중인지
muted: false, // 음소거인지
controls: false, // 기본으로 제공되는 컨트롤러 사용할건지
volume: 0.5, // 볼륨크기
playbackRate: 1.0, // 배속
played: 0, // 재생의 정도 (value)
seeking: false, // 재생바를 움직이고 있는지
duration: 0, // 전체 시간
});
ReactPlayer
<ReactPlayer
ref={videoRef}
url={video} // 서버에서 받아온 video url
playing={playing} // true = 재생중 / false = 멈춤
controls={false} // 기본 컨트롤러 사용 x
muted={muted} // 음소거인지
volume={volume} // 소리조절 기능
playbackRate={playbackRate} // 배속기능
onProgress={progressHandler} // 재생 및 로드된 시점을 반환
width="100%"
height="100%"
/>;
1. 비디오 재생 / 멈춤
state를 전달하여 재생과 멈춤이 간단하게 구현된다.
Props | Description | Default |
playing | Set to true or false to pause or play the media | false |
함수
const playPauseHandler = () => {
setState({ ...state, playing: !state.playing });
};
적용
Video의 body와 footer부분 두 군데에 적용시켰다.
<button>
{!playing ? (
<PlayIcon className="play_icon" onClick={onPlayPause} />
) : (
<PauseIcon className="play_icon" onClick={onPlayPause} />
)}
</button>;
2. 재생 기준 3초 앞으로, 3초 전으로
React Player에서 제공하는 메서드인 getCurrentTime()을 이용하였다. 현재시간을 초로 반환해 +-3을 해주면 된다.
Method | Description |
getCurrentTime() | Returns the number of seconds that have been played ◦ Returns null if unavailable |
함수
const rewindHandler = () => {
videoRef.current.seekTo(videoRef.current.getCurrentTime() - 3);
};
const forwardHandler = () => {
videoRef.current.seekTo(videoRef.current.getCurrentTime() + 3);
};
적용 코드
<div className="body">
<button onClick={onRewind}>
<RewindIcon />
</button>
<button onClick={onForward}>
<ForwardIcon />
</button>
</div>;
3. 소리 조절 + 음소거
Props | Description | Defualt |
volume | Set the volume of the player, between 0 and 1 ◦ null uses default volume on all players #357 |
null |
muted | Mutes the player ◦ Only works if volume is set |
false |
함수
volume을 관리하는 함수에서 parseFloat()을 사용한 이유는 React Player에서 volume의 범위를 0에서 1까지 측정하고
Slider에서 제공하는 min, max는 0에서 100까지 측정하기 때문이다.
// 음소거 유무 함수
const muteHandler = () => {
setState({ ...state, muted: !state.muted });
};
// Slider onChange에 적용시킬 함수
const volumeChangeHandler = (e, newValue) => {
setState({
...state,
volume: parseFloat(newValue / 100),
muted: newValue === 0 ? true : false,
});
};
// Slider onChangeCommitted에 적용시킬 함수: onchange가 끝났을 시점의 볼륨을 찾아준다.
const volumeSeekUpHandler = (e, newValue) => {
setState({
...state,
volume: parseFloat(newValue / 100),
muted: newValue === 0 ? true : false,
});
};
mui Slider에서 제공하는 onChangeCommitted이라는 함수가 있다. 이 기능은 slider에서 mouseup 했을 때 발생하는 함수이다.
이때 그 시점의 볼륨을 value로 받아 찾아주면 된다. 그때의 value가 0이라면 muted 또한 true가 돼야 하므로 muted도 같이 관리해준다.
onChangeCommitted | func |
Callback function that is fired when the mouseup is triggered.
Signature: function(event: React.SyntheticEvent | Event, value: number | Array<number>) => void event: The event source of the callback. Warning: This is a generic event not a change event. |
Tiny trouble shooting
소리 조절을 구현한 Slider을 설명하자면 음소거 상태인 최솟값을 0 , 최댓값을 100으로 설정하고, value로 볼륨을 조절하는 식이다. 재생 바를 구현한 것과 크게 다르지 않다.
다만 여기서 임의로 소리를 0으로 줄였을 시에 자동으로 음소거 처리를 해서 다시 음소거 해제하는 버튼을 눌렀을 때에도 이전의 볼륨 value가 0이어서 변화가 없는 문제가 있었다.
사실 유저가 임의로 0으로 줄였기 때문에 이 현상은 자연스러울 수 있다고 생각했는데, 유튜브의 경우에는 이런 경우에 그냥 볼륨을 0 이상인 값으로 찾아줘 소리가 재생되게끔 해주었다.
다시 생각해보니 개발자 입장에서는 자연스러운 현상이라 생각할 수 있지만, 유저 입장에서는 자칫 오류라는 인식을 줄 수 도 있어 나도 코드를 조금 수정해 유저가 임의로 볼륨을 0으로 줄였더라도 음소거 해제 버튼을 누르면 볼륨의 value를 50으로 찾아주는 조건을 추가했다.
value={muted ? 0 : !muted && volume === 0 ? 50 : volume * 100}
적용 코드
<div className="volume_box">
<button onClick={onMute}>
{muted ? (
<HiVolumeOff className="volume_icon" />
) : (
<HiVolumeUp className="volume_icon" />
)}
</button>
<Slider
min={0}
max={100}
value={muted ? 0 : !muted && volume === 0 ? 50 : volume * 100}
onChange={onVolumeChange}
aria-label="Default"
onMouseDown={onSeekMouseDown}
onChangeCommitted={onVolumeSeekUp}
className="volume_slider"
valueLabelDisplay="off"
/>
</div>;
4. 배속 (0.5배속, 1배속, 1.5배속, 2배속)
Props | Description | Defualt |
playbackRate | Set the playback rate of the player ◦ Only supported by YouTube, Wistia, and file paths |
1 |
함수
받아오는 rate라는 인자를 playbackRate에 넣어주면 돼서 간단히 구현 가능했다.
const playBackChangeHandler = (rate) => {
setState({
...state,
playbackRate: rate,
});
};
적용 코드
Popover component의 안에서 배속의 값 rate를 배열에 넣어 map()을 돌렸다. 해당 rate를 클릭하면 rate에 맞는 배속에 맞게 움직이게 된다.
배속 버튼을 눌릴 때마다 버튼이 그대로 있는 현상이 있어서 open이라는 state를 만들어서 클릭 시 popup 되는 버튼들이 닫히도록 하였다.
const [open, setOpen] = useState(false);
const handlePopover = (event) => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Popover
id={open ? "playbackrate-popover" : undefined;}
open={open}
onClose={handleClose}
>
{[0.5, 1, 1.5, 2].map((rate) => (
<div className="speed_box" key={rate} onClick={() => setOpen(false)}>
<button
onClick={() => onPlaybackRateChange(rate)}
className="speed_btn"
style={{
color: rate === playbackRate ? "#EA617A" : "",
}}
>
<h1>{rate}</h1>
</button>
</div>
))}
</Popover>
);
5. 전체 화면 fullScreen
전체 화면을 구현하는 기능은 따로 제공하지 않아서 간단한 API를 설치했다 -> screenfull API
React player와 별개의 기능이라 함수를 생성하고 fullscreen icon에 버튼으로 적용시키면 된다.
함수
const toggleFullScreenHandler = () => {
screenfull.toggle(videoControllerRef.current);
};
적용 코드
<button className="fullscreen" onClick={onToggleFullScreen}>
<BiFullscreen />
</button>;
짧은 회고
여기까지 플레이어에 기본적으로 들어있는 기능 구현은 다 했다. 아래의 세 가지 기능만 남은 상태인데
6. 재생시간 툴팁으로 표시
7. 좋아요, 스크랩 동영상 내에서 처리
8. 하이라이트 Top 3 시간 누르면 해당 시간으로 이동
이 부분은 프로젝트를 위해 추가한 기능이라 따로 포스팅을 하는 게 나을 것 같다.
기본 기능만 구현하는 것도 쉽지 않았지만, 확실히 글을 쓰면서 발견한 불필요했던 코드도 있었고, 자연스럽게 리팩터링 과정도 조금은 거쳤던 것 같다.
Reference
'항해99 > 실전프로젝트' 카테고리의 다른 글
[WillBe 화상 면접 커뮤니티] React Player를 통한 custom player만들기 (2) (0) | 2022.05.24 |
---|---|
[WillBe 화상 면접 커뮤니티] S3 Pre-signed URL 를 통한 비디오 주고받기 (0) | 2022.05.20 |
[WillBe 화상 면접 커뮤니티] WebRTC, codec 조사 및 적용 (+Trouble Shooting) (0) | 2022.05.18 |
[WillBe 화상 면접 커뮤니티] 프로젝트 주제, 컨셉, 기술챌린지 소개 (0) | 2022.05.15 |
- Total
- Today
- Yesterday
- React Query
- 실전프로젝트
- 프로그래머스
- GIT
- React
- 무한스크롤
- 모두를위한컴퓨터과학
- javascript
- 자바스크립트 비동기 처리
- 리액트
- 모두를 위한 컴퓨터 과학
- 프로그래머스 자바스크립트
- 자바스크립트
- github
- 자바스크립트 클로저
- 클로저
- network
- 프로그래머스 베스트앨범 자바스크립트
- css
- python
- cs50
- reactquery
- 네트워크
- html
- 자바스크립트알고리즘
- 백준
- 리액트네이티브
- 타입스크립트
- 항해99
- 알고리즘자바스크립트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |