
클론 코딩 기반의 오디오 시각화 웹사이트
Original Codepen:https://codepen.io/dilums/pen/MWjEqaa
SnowGlow는 YouTube 링크를 입력하면 서버가 mp3로 변환하고, 클라이언트는 job 상태를 폴링하며 진행률(%)을 표시한 뒤, 완성된 mp3를 HTTP Range 스트리밍으로 재생/시크(시간 이동) 하는 구조로 만들었다.
초기엔 “YouTube URL → 서버가 변환 → mp3를 내려받아 decodeAudioData로 디코딩” 방식이 가장 단순해 보였다.
하지만 실제로는 아래 문제가 반복적으로 발생했다.
decodeAudioData 실패decodeAudioData failed가 발생하거나 메모리 이슈가 생길 수 있다.<audio> 기반 스트리밍 + Range가 훨씬 자연스럽다.그래서 최종적으로 Job 기반 변환 + 진행률 폴링 + <audio> 스트리밍(HTTP Range) 로 구조를 바꿨다.
아래가 현재 구조의 핵심 플로우다.
POST /api/youtube/jobs{ "url": "<youtubeUrl>" }{ "jobId": "<uuid>" }클라이언트는 jobId를 받아서 “진행률 폴링”을 시작한다.
GET /api/youtube/jobs/:jobId{ status, percent, stage, error? }클라이언트는 일정 주기(예: 550ms)로 계속 호출한다.
UI 예시:
YouTube 변환 중… 84%YouTube 변환 중… 99%GET /api/youtube/jobs/:jobId/audio클라이언트는 이 mp3 URL을 <audio src="...">로 재생한다.
이 방식은 전체 다운로드/디코딩 없이도 브라우저가 알아서 버퍼링/재생을 처리한다.

아래는 “형태”를 보여주기 위한 예시입니다.
Request
POST /api/youtube/jobs
Content-Type: application/json
{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}
Response
{
"jobId": "114abf53-1956-409b-ab20-da06acecfe74"
}
Request
GET /api/youtube/jobs/114abf53-1956-409b-ab20-da06acecfe74
Response (running)
{
"status": "running",
"percent": 84,
"stage": "download"
}
Response (done)
{
"status": "done",
"percent": 100,
"stage": "complete"
}
Response (error)
{
"status": "error",
"percent": 0,
"stage": "starting",
"error": "yt-dlp exited with code 1"
}
Request (seek가 발생하면 브라우저가 Range를 보냄)
GET /api/youtube/jobs/114abf53-1956-409b-ab20-da06acecfe74/audio
Range: bytes=0-1048575
Response
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: audio/mpeg
Content-Range: bytes 0-1048575/123456789
Content-Length: 1048576
현재는 단일 서버 기준으로 job 상태를 메모리 Map에 저장한다.
{ status, percent, stage, error, outputPath, cleanupAt }확장(멀티 인스턴스/오토스케일)이 필요해지면 Redis/DynamoDB 같은 외부 저장소로 빼야 한다.
yt-dlp의 --progress-template을 이용해 stdout/stderr에서 파싱 가능한 라인을 뽑고, percent 값을 숫자로 추출해 job.percent를 계속 갱신한다.
/youtube/jobs/:jobId/audio는 Range 요청을 처리한다.
Range: bytes=start-end 헤더가 오면 206 Partial ContentAccept-Ranges, Content-Range, Content-Length 설정fs.createReadStream(file, { start, end })로 부분 스트리밍변환된 mp3는 서버 temp/에 저장된다.
decodeAudioData 대신 <audio> 스트리밍YouTube/업로드 mp3는 <audio>로 재생하고, WebAudio로 분석만 연결한다.
<audio>가 실제 재생 담당AudioContext.createMediaElementSource(audioEl)로 analyser 연결장점:
input[type="range"]로 시간을 이동<audio> 재생 경로는 audio.currentTime을 바꾸면 즉시 이동audio.play() / audio.pause()예시:
트래픽이 늘면 아래 구조가 안정적이다.
https://codepen.io/dilums/pen/MWjEqaa