Skip to content

Commit

Permalink
Merge pull request #15 from boostksm/feat/#14/song-fetching
Browse files Browse the repository at this point in the history
오디오 소스 fetching 관리 및 로딩 UI 처리
  • Loading branch information
boostksm authored Jul 12, 2023
2 parents 76e7c44 + b281f88 commit 641faea
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 70 deletions.
3 changes: 3 additions & 0 deletions public/data/album1.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"imageUrl":
"/samples/track1.jpg",
"description": "1번 트랙",
"duration": 254.537143,
"highlightTime": 108,
"audioSrc": "/samples/터널-코너레코즈.mp3",
"lyrics": ["가고 싶은 곳이 생긴 열일곱 소녀는", "들뜬 마음으로 떠날 채비를 하고", "부풀어 오른 가슴을 간신히 부여잡고", "낯선 곳을 향해 한 발을 내딛었어", "가도 가도 끝이 보이지 않아", "벌써 몇 번이나 주저앉으려 했지", "희미하게 보이는 저기 저곳에", "여전히 지금도 가고 있는 걸", "앞으로 나아가 지쳐 힘들어도 다시 일어나", "끝이 없는 터널 속에 버려진 나", "보이지 않는 닿을 수 없는 꿈 속의 그 곳", "쉬지 않는 발걸음에 지쳐가며", "걸어보자 걸어보자 걸어보자", "앞으로 나아가 지쳐 힘들어도 다시 일어나", "끝이 없는 터널 속에 버려진 나", "꺼지지 않는 불씨 속에 바람이 일어", "보이지 않는 버릴 수 없는 가고 싶은 그 곳", "오늘도 난 캄캄한 밤 외로운 길을", "걸어간다 걸어간다 걸어간다"],
Expand All @@ -41,6 +42,7 @@
"imageUrl":
"/samples/track2.jpg",
"description": "2번 트랙",
"duration" : 223.007325,
"highlightTime": 135,
"audioSrc": "/samples/봄-earbut.mp3",
"lyrics": ["가사 업로드 예정"],
Expand All @@ -56,6 +58,7 @@
"imageUrl":
"/samples/track3.jpg",
"description": "3번 트랙",
"duration": 203.568,
"highlightTime": 60,
"audioSrc": "/samples/Be As You Are (JordanXL Remix)-Mike Posner.mp3",
"lyrics": [
Expand Down
102 changes: 66 additions & 36 deletions src/components/SongPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import timeFormatter from "../utils/timeFormatter";
import { PlayerIcons } from "../utils/icons";
import { css, styled } from "styled-components";
import useSongPlayer from "../hooks/useSongPlayer";
import { moveFromLeftToRight } from "../styles/keyframes";

const loopDescriptions = [
"반복 재생 꺼짐",
Expand Down Expand Up @@ -30,30 +31,53 @@ const SongPlayerLayout = styled.section`
display: flex;
justify-content: space-between;
}
.progressBarBox {
.progressBarInput {
-webkit-appearance: none;
appearance: none;
margin: 0 0;
width: 100%;
outline: none;
overflow: hidden;
border-radius: 5px;
}
.progressBarInput::-webkit-slider-runnable-track {
height: 10px;
background: lightgray;
}
.progressBarInput::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
.progressBox {
overflow: hidden;
.barBox {
display: flex;
height: 10px;
width: 10px;
border-radius: 50%;
background-color: white;
border: 2px solid darkgray;
box-shadow: -407px 0 0 400px darkgray;
.progressBarInput {
-webkit-appearance: none;
appearance: none;
margin: 0 0;
width: 100%;
height: 10px;
outline: none;
overflow: hidden;
border-radius: 5px;
}
.progressBarInput::-webkit-slider-runnable-track {
height: 10px;
background-color: lightgray;
}
.progressBarInput::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 10px;
width: 10px;
border-radius: 50%;
background-color: white;
border: 2px solid darkgray;
box-shadow: -407px 0 0 400px darkgray;
}
.loadingBar {
width: 100%;
height: 10px;
border-radius: 5px;
background-color: lightgray;
height: 10px;
border: 1px lightgray solid;
.loadingBarMoving {
position: relative;
width: 50%;
height: 100%;
border-radius: 5px;
background-color: white;
animation: ${moveFromLeftToRight} 0.8s ease-in-out infinite;
}
}
}
.playingSongTimeBox {
display: flex;
justify-content: space-between;
Expand Down Expand Up @@ -93,16 +117,15 @@ const SongPlayer = ({
playPrevSong,
playNextSong,
currentTime,
duration,
changeCurrentTime,
updateDuration,
updateCurrentTime,
isPlaying,
isRandom,
loopOptionIdx,
toggleIsPlaying,
toggleIsRandom,
setLoop,
isLoading,
} = useSongPlayer({ albumData, setPlayingId, playingSong, isFromHighlight });

return (
Expand All @@ -112,9 +135,6 @@ const SongPlayer = ({
ref={audioRef}
id="player"
loop={loopOptionIdx === 2}
src={playingSong?.audioSrc}
preload="auto"
onLoadedMetadata={updateDuration}
onTimeUpdate={updateCurrentTime}
></audio>
<div className="playingSongHeadingBox">
Expand Down Expand Up @@ -161,17 +181,27 @@ const SongPlayer = ({
{loopOptionIdx === 2 && <span>1</span>}
</Button>
</div>
<div className="progressBarBox">
<input
className="progressBarInput"
type="range"
value={(currentTime / duration) * 100 || 0}
onInput={changeCurrentTime}
aria-label="재생바"
/>
<div className="progressBox">
<div className="barBox">
{isLoading ? (
<div className="loadingBar">
<div className="loadingBarMoving"></div>
</div>
) : (
<input
className="progressBarInput"
type="range"
value={
playingSong ? (currentTime / playingSong.duration) * 100 : 0
}
onInput={changeCurrentTime}
aria-label="재생바"
/>
)}
</div>
<div className="playingSongTimeBox">
<div>{timeFormatter.getString(currentTime)}</div>
<div>{timeFormatter.getString(duration)}</div>
<div>{timeFormatter.getString(playingSong?.duration || 0)}</div>
</div>
</div>
</SongPlayerLayout>
Expand Down
93 changes: 60 additions & 33 deletions src/hooks/useSongPlayer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import getRandomNumber from "../utils/getRandomNumber";
import AudioFetchingManager from "../utils/AudioFetchingManager";

export default function useSongPlayer({
playingSong,
Expand All @@ -10,9 +11,62 @@ export default function useSongPlayer({
const [isPlaying, setIsPlaying] = useState(false);
const [isRandom, setIsRandom] = useState(false);
const [loopOptionIdx, setLoopOptionIdx] = useState(0);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const audioRef = useRef(null);
const audioSrcManager = useRef(new AudioFetchingManager(10));
const [curObjectUrl, setCurObjectUrl] = useState(null);
const isLoading = useMemo(() => curObjectUrl === null, [curObjectUrl]);

useEffect(() => {
if (!playingSong) return;
(async () => {
try {
let objectUrl = audioSrcManager.current.getObjectUrl(
playingSong.audioSrc
);
if (!objectUrl) {
setCurObjectUrl(null);
objectUrl = await audioSrcManager.current.getNewObjectUrl(
playingSong.audioSrc
);
}
setCurObjectUrl(objectUrl);
} catch (err) {
console.log(err);
}
})();
}, [playingSong]);

useEffect(() => {
audioRef.current.src = curObjectUrl;
if (isFromHighlight) {
moveToHighlight();
} else {
moveTo(0);
}
if (isPlaying) {
audioRef.current.play().catch(console.log);
}
}, [curObjectUrl]);

useEffect(() => {
if (isFromHighlight) moveToHighlight();
}, [isFromHighlight]);

useEffect(() => {
if (isPlaying) audioRef.current.play().catch(console.log);
else audioRef.current.pause();
}, [isPlaying]);

const moveTo = useCallback((time) => {
audioRef.current.currentTime = time;
setCurrentTime(time);
}, []);

const moveToHighlight = useCallback(() => {
fadeIn();
moveTo(playingSong.highlightTime);
}, [playingSong]);

const playPrevSong = useCallback(() => {
if (!playingSong) return;
Expand Down Expand Up @@ -45,22 +99,6 @@ export default function useSongPlayer({
setIsPlaying((prev) => !prev);
}, []);

useEffect(() => {
if (!playingSong || !audioRef.current) return;
async function playAudio() {
try {
await audioRef.current.play();
} catch (err) {
console.log(err);
alert(err);
// audioRef.current.src = "/samples/Be As You Are (JordanXL Remix)-Mike Posner.mp3";
// audioRef.current.play();
}
}
if (isPlaying) playAudio();
else audioRef.current.pause();
}, [isPlaying, playingSong]);

const fadeIn = useCallback(() => {
audioRef.current.volume = 0;
const intervalId = setInterval(() => {
Expand All @@ -75,45 +113,34 @@ export default function useSongPlayer({
}, 100);
}, []);

useEffect(() => {
if (playingSong && isFromHighlight) {
if (isPlaying) fadeIn();
audioRef.current.currentTime = playingSong.highlightTime;
setCurrentTime(playingSong.highlightTime);
}
}, [isFromHighlight, playingSong]);

const updateDuration = useCallback((e) => {
setDuration(e.currentTarget.duration);
}, []);
const updateCurrentTime = useCallback((e) => {
const { currentTime } = e.currentTarget;
setCurrentTime(currentTime);
}, []);

const changeCurrentTime = useCallback(
(e) => {
if (!playingSong) return;
const { value } = e.currentTarget;
const nextTime = (value / 100) * duration;
const nextTime = (value / 100) * playingSong.duration;
audioRef.current.currentTime = nextTime;
setCurrentTime(nextTime);
},
[duration]
[playingSong]
);
return {
audioRef,
playPrevSong,
playNextSong,
currentTime,
duration,
changeCurrentTime,
updateDuration,
updateCurrentTime,
isPlaying,
isRandom,
loopOptionIdx,
toggleIsPlaying,
toggleIsRandom,
setLoop,
isLoading,
};
}
11 changes: 10 additions & 1 deletion src/styles/keyframes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,13 @@ const shine = keyframes`
}
`;

export { rotate, rotateFaster, shine };
const moveFromLeftToRight = keyframes`
0%{
left: -100%;
}
100% {
left: 100%;
}
`;

export { rotate, rotateFaster, shine, moveFromLeftToRight };
34 changes: 34 additions & 0 deletions src/utils/AudioFetchingManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import axios from "axios";

export default class AudioFetchingManager {
constructor(limit = 10) {
this.objectUrls = new Map();
this.limit = limit;
this.abortController = null;
}

fifo() {
if (this.objectUrls.size > this.limit) {
const [[firstKey, firstObjectUrl]] = this.objectUrls.entries();
URL.revokeObjectURL(firstObjectUrl);
this.objectUrls.delete(firstKey);
}
}

async getNewObjectUrl(src) {
this.abortController = new AbortController();
const { data: blob } = await axios.get(src, {
responseType: "blob",
signal: this.abortController.signal,
});
const objectUrl = URL.createObjectURL(blob);
this.objectUrls.set(src, objectUrl);
this.fifo();
return objectUrl;
}

getObjectUrl(src) {
this.abortController?.abort();
return this.objectUrls.get(src);
}
}

0 comments on commit 641faea

Please sign in to comment.