이번에 맡게된 외주 개발 작업에서
1. 사용자가 mediaRecorder로 녹음한거 재생
2. mp3 파일 재생
을 할 오디오 플레이어를 구현해야했다
Figma상 디자인은 이랬다
따라서 대충 슥 봤을 때
크게 구현해야하는 기능은
1. audioURL 생성해서 audio component에서 재생가능하게하기
2. play, pause 할 때마다 아이콘 바뀌고
audio 재생했다가 멈췄다가해주기
3. audio 전체 길이와 현재 재생 중인 시간
계산해서 progress bar 만들어주기
였다
처음에는 audioBlob이나 URL을
생성하지 않고
바로 audioRef를 넣어서
재생하게 해줬는데
이유는 모르겠지만 재생에 필요한
셋업을 하는 시간이 너무 오래 걸려서..
audioRef -> audioBlob -> URL
로 변경한 뒤
그걸 audio에 넣어주기로 했다
그러니 화면이 렌더링되자마자
바로 재생이 가능해졌다
나는 CustomAudioPlayer라는
컴포넌트를 따로 생성해줬다
우선 audioBlob까지만 만들어서
파라미터로 넘겨주면
CustomAudioPlayer
함수 내부에서
blob을 URL로 만들어주기로했다
const CustomAudioPlayer = ({audioBlob }) => {
const [audioURL, setAudioURL] = useState(null);
useEffect(() => {
if (audioBlob) {
const url = URL.createObjectURL(audioBlob);
setAudioURL(url);
return () => URL.revokeObjectURL(url);
}
}, [audioBlob]);
return(
<div className="flex flex-col items-center bg-transparent max-w-md min-w-[500px] w-full mt-5 mb-5">
<audio id="audio-player" src={audioURL} preload="metadata"/>
</div>
)
useEffect로
audioBlob을 받아오면
URL을 생성해
audioURL 변수에 넣어주었다
그런다음 <audio>를 생성해서
src에 URL을 넣어주면 된다
preload = "metadata"는
audio component 생성과 함께
metadata를 셋팅하게해달라는?
그런 역할인 듯 했다
이제 그 다음은 play와 pause 기능을
구현해보자
우선 현재 play인지 아닌지를
체크해줄 flag가 필요하다
그래서 위에
const [isPlaying, setIsPlaying] = useState(false);
로 설정해준다
처음에 audioPlayer가 렌더링 됐을 때는
재생이 되면 안되므로 처음 값은 false로 해준다
그다음 재생할 때 실행되는 함수를
구현해준다
const handlePlay = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.play();
setIsPlaying(true);
}
};
나는 audio의 id를 'audio-player'로 해놓았다
그런다음 audioElement가 존재하면
이를 play하게 해준다
그런 다음 isPlaying flag를
현재 재생이 되고 있으므로 true로 바꿔준다
const handlePause = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.pause();
setIsPlaying(false);
}
};
pause도 동일하게
audioElement를 찾아서 pause하게 해주고
isPlaying을 false로 바꿔주게한다
이렇게 handlePlay와 handlePause 함수를
정의해줬으면
재생버튼을 만들어서
isPlaying에 따라 함수들이 실행되게 구현해주자
<div className="flex items-center bg-transparent w-full">
<button
onClick={isPlaying ? handlePause : handlePlay}
className="relative z-10"
style={{ cursor: 'pointer' }}
>
<img
src={isPlaying ? pauseIcon : playIcon}
alt={isPlaying ? 'Pause' : 'Play'}
className="object-contain"
style={{ maxHeight: '32px', maxWidth: '32px' }}
/>
</button>
</div>
위와 같이 button 부분을 구현해줬다
우선 위에 icon들의 경로를 각각
playIcon, pauseIcon로 선언해줬고
Figma UI상
재생과 일시정지는 한 개의 버튼으로
실행이 되므로
button을 1개만 만들었다
그리고 만약 isPlaying이 true라면
버튼을 눌렀을 때 handlePause가 실행되도록
isPlaying false라면 버튼을 눌렀을 때
handlePlay가 실행되도록 구현해줬다
그다음 아이콘의 src도 동일하게
isPlaying이 true면
일시정지 모양의 img가 뜨도록
isPlaying이 false면
재생 모양의 img가 뜨도록 해줬다
이 부분을 구현한 전체 코드는
아래와 같다
const CustomAudioPlayer = ({audioBlob, duration }) => {
const [audioURL, setAudioURL] = useState(null);
// playing, pause 체크할 flag
const [isPlaying, setIsPlaying] = useState(false);
// icon 가져올 경로
const playIcon = IconPaths.CUSTOM_AUDIO_PLAYER_PLAY;
const pauseIcon = IconPaths.CUSTOM_AUDIO_PLAYER_PAUSE;
useEffect(() => {
if (audioBlob) {
const url = URL.createObjectURL(audioBlob);
setAudioURL(url);
return () => URL.revokeObjectURL(url);
}
}, [audioBlob]);
// play 구현
const handlePlay = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.play();
setIsPlaying(true);
}
};
// Pause 구현
const handlePause = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.pause();
setIsPlaying(false);
}
};
return (
<div className="flex flex-col items-center bg-transparent max-w-md min-w-[500px] w-full mt-5 mb-5">
<audio id="audio-player" src={audioURL} preload="metadata"/>
<div className="flex items-center bg-transparent w-full">
<button
onClick={isPlaying ? handlePause : handlePlay}
className="relative z-10"
style={{ cursor: 'pointer' }}
>
<img
src={isPlaying ? pauseIcon : playIcon}
alt={isPlaying ? 'Pause' : 'Play'}
className="object-contain"
style={{ maxHeight: '32px', maxWidth: '32px' }}
/>
</button>
</div>
</div>
);
};
이제 재생이 진행되는 만큼
검은색 progress bar가 진행되도록
progress를 구현해줘야한다
우선 progress는 width를 조정하는
방식으로 진행했는데
progress = (현재재생시간 / 전체길이) * 100
에 맞게 현재재생시간이 변할때마다 실시간으로
progress bar의 길이를 변경하도록
구현해야겠다고 생각했다
이를 위해서는
audioURL의 전체 길이를 알아야했는데
audioElement.duration으로
손쉽게 받아올 수 있을줄 알았으나..
우리는 user가 MediaRecorder로 녹음한
파일을 재생해줘야했는데
계속 이 duration이 Infinity로 뜨는 것이다
미췬듯이 구글링을 한 결과
MediaRecorder는 duration을
받아올 수 없다고 한다..
MediaRecorder에서 duration을 해주려면
ffmpeg로 다시 설정해주거나
다른 recorder를 사용해줘야했는데
이제와서 구현해놨던 녹음 기능을
전부다 바꾸기에는 일이 너무 커져서
결국 user가 녹음을 시작하고 끝내는
시각을 받아와 그 차이를 구해서
duration을 따로 계산해줬다 ㅠ
그래서 CustomAudioPlayer의
파라미터로 duration을 추가해줬다
그렇다면 이제 전체 길이인 duration은
해결이 되었으니까
현재 재생시간만 받아올 수 있으면 된다
이는 audioElement.currentTime으로
손쉽게 가져올 수 있다
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
const onTimeUpdate = () => {
setCurrentTime(audioElement.currentTime);
};
audioElement.addEventListener('timeupdate', onTimeUpdate);
return () => {
audioElement.removeEventListener('timeupdate', onTimeUpdate);
};
}
}, [audioURL]);
우선 위에 currentTime을
useState로 설정해주었다
그 다음 audio component에 있는
timeupdate 이벤트리스너를 이용하기로했다
시간이 변경되면 호출되는 이벤트리스터 같았다
따라서 onTimeUpdate 메소드로
시간이 변경될 때마다
currentTime에 현재 audioElement가
재생이 되고있는 시간이 담기도록 해줬다
useEffect를 사용해서
audioURL이 셋팅되면
이 이벤트리스너를 설정하도록 해주고
끝나면 다시 이벤트리스너를 remove해줘서
clean up하도록 해줬다
const progress = currentTime <= duration ? (currentTime / duration) * 100 : 100;
그 다음 넓이를 지정해줄
progress를 위와 같이 구현해줬는데
앞에 currentTime <= duration
조건문을 달아준 이유는
나는 정확한 audio의 duration이 아닌
user가 녹음을 시작하고 끝내는 길이를
계산해서 넣어준 duration 값이었으므로
혹시나 현재 재생 시간이
duration을 넘었을 때는 그냥
100으로 가도록 설정해준 것이다
아무튼 위와 같이 progress를 선언해준 뒤
아래와 같이 width를 %로 progress 값을 넣어줬다
그리고 progress bar와 같이 따라가는
동그란 아이콘은
왼쪽 위치를 progress만큼 옮겨서
같이 따라가게 해줬다
// 전체 길이를 나타내는 회색 bar
<div className="flex-1 h-2 bg-gray-200 rounded ml-4 relative">
// audio가 재생됨에 따라 변하는 검은색 bar
<div
className="h-full bg-black rounded"
style={{ width: `${progress}%` }}
/>
// 검은색 bar와 함께 따라가는 동그란 아이콘
<img
src={IconPaths.CUSTOM_AUDIO_PLAYER_PROGRESS_CIRCLE}
alt="Progress"
className="absolute transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${progress}%`,
top: '50%',
width: '25px',
height: '25px',
}}
/>
</div>
이정도까지만 해줬더니
생각한대로 CustomAudioPlayer가
구현이 되었다
처음 렌더링 되었을 때 모습
재생 중일 때 모습
아이콘이 정상적으로 변하고
progress bar도
제대로 따라가는 것을 확인할 수 있다
(아이콘에 파란 border는
그냥 클릭한 채로 캡처해서 뜨는거
코드와는 아무상관없다)
이대로 해줬더니 생긴 문제는
audio재생이 모두 끝났을 때
다시 처음 모습으로 돌아가게해주고싶은데
progress bar가 끝까지 진행된 그상태로
그냥 멈춰버린다는 점이었다
따라서 audio가 play를 끝냈을 때
다시 처음모습으로 돌아가도록
로직을 추가했다
const handleEnded = () => {
const audioElement = document.getElementById('audio-player');
setIsPlaying(false);
audioElement.currentTime = 0;
};
audioElement.addEventListener('ended', handleEnded);
아래와 같이
audio가 끝이 났을 때
isPlaying이 false가 되도록 해주고
currentTime을 0으로 다시 바꿔주었다
그런 다음 audio 컴포넌트의
ended 이벤트 리스너를 사용해서
handleEnded를 추가해주었다
이렇게 해주면 audio의 재생이 끝났을 때
handleEnded 함수가 실행되게된다
이렇게 구현해준다음
재생해주니
audio가 끝나면 정상적으로
처음 상태로 돌아가는 것을 확인했다
아래는 CustomAudioPlayer의
전체코드이다
import React, { useState, useEffect } from 'react';
import { IconPaths } from '../statics';
const CustomAudioPlayer = ({audioBlob, duration }) => {
const [audioURL, setAudioURL] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const playIcon = IconPaths.CUSTOM_AUDIO_PLAYER_PLAY;
const pauseIcon = IconPaths.CUSTOM_AUDIO_PLAYER_PAUSE;
useEffect(() => {
if (audioBlob) {
const url = URL.createObjectURL(audioBlob);
setAudioURL(url);
return () => URL.revokeObjectURL(url);
}
}, [audioBlob]);
useEffect(() => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
const onTimeUpdate = () => {
setCurrentTime(audioElement.currentTime);
};
const handleEnded = () => {
const audioElement = document.getElementById('audio-player');
setIsPlaying(false);
audioElement.currentTime = 0;
};
audioElement.addEventListener('timeupdate', onTimeUpdate);
audioElement.addEventListener('ended', handleEnded);
return () => {
audioElement.removeEventListener('timeupdate', onTimeUpdate);
audioElement.removeEventListener('ended', handleEnded);
};
}
}, [audioURL]);
const handlePlay = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.play();
setIsPlaying(true);
}
};
const handlePause = () => {
const audioElement = document.getElementById('audio-player');
if (audioElement) {
audioElement.pause();
setIsPlaying(false);
}
};
const progress = currentTime <= duration ? (currentTime / duration) * 100 : 100;
return (
<div className="flex flex-col items-center bg-transparent max-w-md w-full mt-5 mb-5">
<audio id="audio-player" src={audioURL} preload="metadata"/>
<div className="flex items-center bg-transparent w-full">
<button
onClick={isPlaying ? handlePause : handlePlay}
className="relative z-10"
style={{ cursor: 'pointer' }}
>
<img
src={isPlaying ? pauseIcon : playIcon}
alt={isPlaying ? 'Pause' : 'Play'}
className="object-contain"
style={{ maxHeight: '32px', maxWidth: '32px' }}
/>
</button>
<div className="flex-1 h-2 bg-gray-200 rounded ml-4 relative">
<div
className="h-full bg-black rounded"
style={{ width: `${progress}%` }}
/>
<img
src={IconPaths.CUSTOM_AUDIO_PLAYER_PROGRESS_CIRCLE}
alt="Progress"
className="absolute transform -translate-x-1/2 -translate-y-1/2"
style={{
left: `${progress}%`,
top: '50%',
width: '25px',
height: '25px',
}}
/>
</div>
</div>
</div>
);
};
export default CustomAudioPlayer;
위와 같이 CustomAudioPlayer을 구현해줬고
렌더링하는 js파일에서는
아래와 같이 작성해줬다
const AnswerContainer = ({ userRecordingAudioBlob, userRecordingDuration }) => {
return (
<ContentContainerBox>
<CustomAudioPlayer audioBlob={userRecordingAudioBlob} duration={userRecordingDuration} />
</ContentContainerBox>
);
};
사실 user가 클릭한 버튼에 따라
다른 audioBlob을 재생하도록 해줬는데
그부분까지 포함하니 코드가 너무 길어져서
우선 이런식으로 데이터를 주고받아서
CustomAudioPlayer를 구현했다는 것만
가져왔다
아무튼 나름 재밌지만 힘들었던
CustomAudioPlayer 구현하기 끝-!
'기술 > 웹 개발' 카테고리의 다른 글
[react] Google Cloud text-to-speech API 이용해서 frontend에서 영어단어별 발음 재생 구현하기 (7) | 2024.10.10 |
---|---|
[react] client단에서 마이크를 이용해 녹음한 파일 API로 전송하기(multipart 전송) (2) | 2024.09.05 |
[FastAPI/python] 파이썬 FastAPI로 정말 간단하게 API 만들기(CORS) (0) | 2024.08.31 |
[PortOne/react] 통합 결제 연동 솔루션 포트원 react 프로젝트에 이식하기 (2) | 2024.08.30 |
[node.js/express/react] 새로운 endpoint로 새로운 화면 띄우기 (1) | 2024.08.29 |