MUXIC.js

mp3를 받아 오는
라이브러리.

URL을 넣으면 이미 열린 mp3 오디오 스트림을 돌려주는 TypeScript 라이브러리입니다. music.player가 그대로 가져다 쓸 수 있도록, URL 해석과 실제 추출을 두 층으로 나눠 다시 설계했습니다.

TypeScript, Bun, MIT

요약

514줄
TypeScript 소스
14개
bun test 테스트 케이스
2개
추출 엔진. 기본 하나, 선택 하나
0단계
빌드 과정. TypeScript 소스를 그대로 배포

사용법

import { openAudioStream } from "muxic";

const { stream, contentType, abort } = await openAudioStream(
  "https://www.youtube.com/watch?v=...",
);
// stream: ReadableStream<Uint8Array>, contentType: "audio/mpeg"

반환값은 주소 목록이 아니라 이미 열린 바이트 스트림입니다. 호출하는 쪽은 이걸 HTTP 응답 본문에 바로 꽂거나 .tee()로 나눠 쓰면 됩니다.

구조

openAudioStream(url) URLyoutube.com/… resolver순수 함수네트워크 없음 { site, id }정규화된 식별자 engine부수효과는 전부 여기동시 실행 슬롯으로 제한 ytdlp-ffmpeg기본 · mp3 스트림 innertube선택 { stream, contentType, abort } ExtractionError사용자 문구 대신 기계용 코드BOT_CHECK · PRIVATE · TIMEOUT …
resolver와 engine의 두 층 구조

resolver

URL을 정규화된 식별자 { site, id }로 바꿉니다. 네트워크를 쓰지 않는 순수 함수라서 테스트하기 쉽습니다.

engine

식별자를 받아 바이트 스트림을 엽니다. 외부 프로세스 실행 같은 부수효과는 전부 여기에 모았습니다.

  • ytdlp-ffmpeg (기본): yt-dlp의 출력을 ffmpeg로 넘겨 mp3 스트림을 만듭니다. 두 프로그램이 PATH에 있어야 합니다.
  • innertube (선택): youtubei.js로 프로세스 없이 JavaScript만으로 가져오는 빠른 경로입니다. 기본으로 등록돼 있지 않아서 직접 등록해야 씁니다.
import { openAudioStream, registerEngine, innertubeEngine } from "muxic";

registerEngine("innertube", innertubeEngine);
await openAudioStream(url, { engine: "innertube" });

다시 설계한 이유

처음 버전은 JavaScript만으로 직접 주소를 뽑아 넘기는 방식이었습니다. music.player에 붙이려고 보니 세 가지가 맞지 않았습니다.

  1. 뽑아 온 주소는 요청한 서버의 IP에 묶여 있어서 브라우저에 그대로 넘길 수 없습니다. 결국 서버가 중계해야 합니다.
  2. 주소가 가리키는 파일은 대개 webm/opus입니다. music.player는 Safari에서도 재생되도록 mp3 192k로 맞추고 있어서 이 보장이 깨집니다.
  3. download()가 파일 전체를 메모리에 올렸습니다. music.player가 쓰는 점진적 스트리밍과 정반대였습니다.

그래서 인터페이스 모양(엔진 레지스트리)은 살리고, 엔진은 music.player에서 이미 검증한 yt-dlp + ffmpeg를 옮겨 왔습니다. JavaScript만 쓰는 경로는 기본이 아니라 선택용으로 남겼습니다. 함께 TypeScript로 옮겼습니다.

에러

라이브러리에는 한국어든 영어든 사용자에게 보여 줄 문구를 넣지 않았습니다. 대신 기계가 읽는 코드를 던지고, 코드를 화면 문구로 바꾸는 일은 앱이 맡습니다. yt-dlp의 에러 출력을 읽어 코드로 분류합니다.

ExtractionError 코드
코드뜻
BOT_CHECK봇 확인 요구
RELOAD_REQUIRED페이지 새로고침 필요
PRIVATE비공개 영상
UNAVAILABLE볼 수 없는 영상
GEO_BLOCKED지역 제한
TIMEOUT시간 초과
SPAWN_FAILED외부 프로세스 실행 실패
UNKNOWN분류되지 않은 실패

동시에 너무 많은 추출이 돌지 않도록 슬롯을 두었고, 한도를 넘으면 TooManyExtractionsError를 던집니다.

export function createSlots(limit: number): Slots {
  let active = 0;
  return {
    acquire() {
      if (active >= limit) throw new TooManyExtractionsError(limit);
      active++;
      let released = false;
      return () => {              // 여러 번 불러도 한 번만 반납
        if (released) return;
        released = true;
        active--;
      };
    },
    get active() { return active; },
  };
}

코드 구성

src/ 파일별 줄 수 (합계 514줄) engines/ytdlp-ffmpeg.ts 246줄 engines/innertube.ts 93줄 errors.ts 47줄 index.ts 44줄 types.ts 29줄 resolvers/youtube.ts 28줄 concurrency.ts 27줄
외부 프로세스를 다루는 기본 엔진이 코드의 절반 가까이를 차지합니다.

기록

  1. 저장소 생성, 첫 JavaScript 버전
  2. FIXES.md 계획대로 resolver/engine 구조로 재설계, TypeScript 전환

music.player에 붙이려고 만든 라이브러리지만, music.player는 2026년 9월 7일 서버 추출을 Invidious Companion으로 바꾸면서 yt-dlp와 ffmpeg를 걷어 냈습니다. 그래서 지금 music.player는 이 라이브러리를 쓰지 않습니다.