# RemoCue Player Open Protocol Specification

> RemoCue Player를 원격 제어하기 위한 통신 규격서
> Version: 3.9 | Last Updated: 2026-04-25

---

## Introduction

이 문서는 **RemoCue Player**의 원격 제어 통신 규격서입니다.

RemoCue Player는 피치 조절, 배속 변경, 구간 반복, 대기열 관리 등 고급 재생 기능을 제공하는 범용 미디어 플레이어입니다. 이 규격서는 RemoCue Player를 원격에서 제어하고자 하는 **모든 소프트웨어 개발자**에게 제공됩니다.

이 규격을 구현하면 어떤 소프트웨어든 RemoCue Player의 리모컨이 될 수 있습니다.

### 이 규격서의 목적

RemoCue Player는 **미디어 URL을 수신하여 재생하는 역할**에 집중합니다. 콘텐츠 검색, URL 추출, 카탈로그 관리 등은 리모컨 측의 책임입니다.

이러한 역할 분리에는 명확한 이유가 있습니다:

- 영상 플랫폼 정책, 콘텐츠 제공자의 이해관계, 법적 제약으로 인해 하나의 앱이 콘텐츠 소스와 재생을 모두 담당하기 어렵습니다
- 플레이어를 콘텐츠 소스에 독립시킴으로써, **어떤 미디어 소스와도 연동**할 수 있는 유연한 구조를 실현합니다
- 미래에 새로운 합법적 콘텐츠 소스나 스트리밍 방식이 등장하더라도, RemoCue Player는 수정 없이 그대로 사용할 수 있습니다

리모컨과 플레이어를 분리된 규격으로 정의함으로써, 다양한 리모컨 구현체가 RemoCue Player 생태계에 자유롭게 참여할 수 있습니다.

### 누가 이 규격을 사용할 수 있는가

- 노래방 리모컨 앱 개발자
- 스마트 홈 자동화 시스템
- 음악/영상 큐레이션 서비스
- 교육용 미디어 재생 관리 도구
- 커스텀 DJ/이벤트 관리 소프트웨어
- 그 밖에 원격 미디어 제어가 필요한 모든 소프트웨어

---

## 1. Architecture Overview

```
+-------------------+        HTTP REST (LAN)         +---------------------+
|   Remote Client   |  <---------------------------->  |   RemoCue Player    |
| (App, Web, CLI,   |     Wi-Fi / Same Network        |   (Media Player +   |
|  IoT Device, ...) |                                  |    HTTP Proxy)      |
+-------------------+                                  +---------------------+
```

- **RemoCue Player** — 미디어 재생 서버. HTTP API를 제공하며 수신한 명령에 따라 재생을 수행합니다. 또한 외부 URL에 대한 **HTTP 프록시** 기능을 제공하여, 브라우저 기반 Remote Client가 CORS 제한 없이 외부 서비스에 접근할 수 있도록 지원합니다.
- **Remote Client** — RemoCue Player를 제어하는 소프트웨어. 이 규격의 HTTP API를 호출하는 모든 것이 Remote Client가 될 수 있습니다.

### 통신 기본 사양

| 항목 | 사양 |
|------|------|
| 전송 프로토콜 | HTTP REST (JSON) |
| 상태 동기화 | Remote Client가 `GET /api/state`를 1초 간격으로 폴링 |
| 네트워크 | 동일 LAN (Wi-Fi) 내 통신 |
| 인증 | URL 경로 내 세션 토큰 |

---

## 2. Connection Flow

### 2.1 개요 — Player는 단일 QR, Landing Page가 카탈로그를 제공

RemoCue Player v1.4부터의 연결 흐름은 **역할 분리**가 명확합니다:

| 주체 | 역할 |
|---|---|
| **Player UI** | 단 하나의 QR만 표시 — `https://<ip>:8443/<token>` 형태의 raw server URL |
| **Landing Page** (browser-side) | QR을 스캔한 폰에서 열리는 HTML 페이지. **카탈로그** 기반 리모컨 선택 UI를 제공하고, 사용자가 고른 리모컨으로 연결 파라미터와 함께 navigate |

이 분리의 장점:
- Player UI는 극단적으로 단순 — 어떤 리모컨이 추가되어도 UI 변경이 필요 없음
- 카탈로그 갱신이 앱 릴리스 없이 가능 (live JSON 교체만으로 즉시 반영)
- 사용자 관점에서도 "폰 카메라로 찍기 → 브라우저에서 고르기"의 두 단계가 직관적
- 외부 호환 리모컨 개발자는 자기 URL을 카탈로그에 등록하는 것만으로 player 사용자들에게 노출됨

전체 흐름:

```
┌─ Player ───────────────┐
│  [QR: https://ip:8443  │
│       /a1b2c3d4]       │
│  wifi/url/연결수        │
└────────────────────────┘
          │
          ▼ 폰 카메라 스캔
          ▼
  https://ip:8443/a1b2c3d4
  (self-signed cert 수락 필요 시 1회)
          │
          ▼
┌─ Player Landing Page ──┐
│ 사용할 리모컨 선택      │
│ [📷 기본 리모컨]        │
│ [🎤 노래방 리모컨]      │
│ ...                    │
└────────────────────────┘
          │
          ▼ 사용자가 카드 탭
          ▼
  https://remote-host/path
   ?ip=&port=&token=&scheme=
          │
          ▼
┌─ 선택된 리모컨 사이트 ──┐
│  (자동 연결 완료)       │
│  Gallery / Queue / ...  │
└────────────────────────┘
```

### 2.2 Remote Catalog

카탈로그는 player가 알고 있는 호환 리모컨들의 메타데이터 목록으로, 공개 호스팅된
JSON 파일 또는 앱 번들 에셋에서 읽어옵니다. Player는 서버 시작 시 이를 로드하고,
브라우저가 root path(`/<token>/`)에 접근하면 랜딩 페이지 HTML에 카탈로그 리스트를
렌더링합니다.

**카탈로그 파일 스키마** (`schemaVersion: 1`):

```json
{
  "schemaVersion": 1,
  "updated": "YYYY-MM-DD",
  "remotes": [
    {
      "id": "unique-id",
      "name": { "ko": "...", "en": "..." },
      "description": { "ko": "...", "en": "..." },
      "url": "https://<host>/<path>",
      "icon": "📷",
      "category": "gallery",
      "features": ["queue", "pitch-control"],
      "protocolVersion": "3.3",
      "requiresHttpsPlayer": true,
      "official": true,
      "author": "...",
      "version": "1.2.0"
    }
  ]
}
```

| 필드 | 필수 | 설명 |
|---|---|---|
| `id` | ✓ | 안정된 고유 식별자. 사용자 선호도 저장의 키로 사용 |
| `name` / `description` | ✓ | 로케일 맵 (`ko`/`en` 등). 리더가 현재 locale에 맞춰 선택 |
| `url` | ✓ | 리모컨의 canonical URL (HTTPS 필수) |
| `icon` | | 단일 이모지 또는 짧은 심볼 |
| `category` | | `gallery`, `karaoke`, `music`, `video`, `utility`, `custom` 등 |
| `features` | | 기능 태그 배열 — 필터링·표시용 |
| `protocolVersion` | | 해당 리모컨이 따르는 최소 프로토콜 버전 |
| `requiresHttpsPlayer` | | `true`면 player가 HTTPS(8443)로 떠 있어야 함 |
| `official` | | RemoCue 팀이 관리하는 공식 리모컨 표시 |
| `author` | | 표시용 저작자 이름 |
| `version` | | 리모컨 **번들 버전** (semver). 설정 시 해당 리모컨은 "Bundleable" 로 간주되어 Controller 유형의 클라이언트가 오프라인 캐싱과 업데이트 판단에 사용. **섹션 2.6 참조**. Player 유형 클라이언트는 무시. |

Player 구현체는 카탈로그 로딩에 대해 **network-first, asset-fallback** 전략을
사용할 것을 권장합니다. 공개 URL(예: `https://sntsoft.co.kr/remocueplayerweb/remotes.json`)을
먼저 시도하고, 네트워크 실패 시 앱 번들된 JSON을 fallback으로 사용합니다. 이렇게
하면 카탈로그 갱신이 앱 릴리스 없이 즉시 배포 가능하면서도, 오프라인 환경에서도
최소 하나의 공식 리모컨은 항상 보장됩니다.

`_`로 시작하는 필드(예: `_description`, `_fieldGuide`)는 문서 전용이며 리더는
무시해야 합니다. 공식 카탈로그 파일에는 스키마 설명과 필드 가이드가 인라인으로
담겨 있어 파일 자체가 레퍼런스가 됩니다.

### 2.3 Landing Page

사용자가 QR을 스캔하면 폰 브라우저가 `https://<ip>:8443/<token>`을 엽니다. Player는
이 요청에 대해 **content negotiation**으로 응답합니다:

| 요청 조건 | 응답 |
|---|---|
| `X-RemoCue-Remote: true` 헤더 포함 | JSON API info (`{"ok": true, "type": "remocue", ...}`) |
| 헤더 없음 (일반 브라우저 방문) | 카탈로그 기반 HTML 랜딩 페이지 |

이 분기는 API 클라이언트와 사람 사용자를 자연스럽게 구분합니다. Remote Client
자바스크립트는 fetch 시 항상 헤더를 전송하므로 JSON만 받으며, 사람은 브라우저
주소창으로 접속하므로 HTML 랜딩 페이지를 보게 됩니다.

랜딩 페이지 HTML의 요구사항:

1. Player의 세션 정보(`ip`/`port`/`token`/`scheme`)를 요청의 `requestedUri`에서 추출
2. 로드된 카탈로그의 `remotes` 배열을 순회하며 각 엔트리를 **카드**로 렌더링
3. 각 카드의 `<a href>`는 해당 엔트리의 `url`에 `ip`/`port`/`token`/`scheme` 쿼리 파라미터를 머지한 URL
4. 사용자가 카드를 탭하면 브라우저가 해당 URL로 이동하며, 목표 리모컨은 쿼리 파라미터를 읽어 즉시 player에 자동 연결
5. 카탈로그 로딩이 실패한 경우에도 최소 하나의 fallback 엔트리(공식 기본 리모컨)는 항상 렌더링

### 2.4 Remote Site Auto-Connect

랜딩 페이지에서 선택된 리모컨 사이트는 다음 형식의 URL로 열립니다:

```
https://<remote-host>/<remote-path>?ip=<ip>&port=<port>&token=<token>&scheme=<http|https>&lang=<ko|en|…>
```

| 쿼리 파라미터 | 필수 | 설명 |
|---|---|---|
| `ip` | ✓ | 플레이어 기기의 LAN IP (예: `192.168.0.13`) |
| `port` | ✓ | `8081`(HTTP) 또는 `8443`(HTTPS) |
| `token` | ✓ | 8자리 HEX 세션 토큰 (예: `a1b2c3d4`) |
| `scheme` | ✓ | `http` 또는 `https` — 웹 페이지가 fetch에 사용할 프로토콜 |
| `lang` | | 리모컨 UI 의 초기 locale 힌트 (BCP 47 primary subtag, 예: `ko`, `en`). Opener(Player 랜딩 페이지, Controller 앱 등)의 현재 언어 선택을 리모컨에 전달해 **앱 ↔ 리모컨 간 언어 일관성** 을 유지하게 한다. 리모컨 구현체는 이 값을 지원 언어에 매핑하여 초기 렌더링에 반영하고, 사용자가 페이지 내에서 언어를 바꾸면 그 선택을 우선한다. 알 수 없는 값은 무시하고 리모컨 자체의 기본 동작(localStorage / navigator.language 등)으로 폴백한다. |

> `scheme` 파라미터는 **Mixed Content 정책 대응**을 위해 필수적입니다. HTTPS 페이지는
> HTTP player에 fetch할 수 없으므로, 공개 웹 리모컨이 HTTPS로 호스팅된다면
> `scheme=https`가 전달되어야 하며 player도 HTTPS 엔드포인트(8443)를 열어 두어야 합니다.

Remote Client(리모컨 웹페이지) 구현체의 연결 절차:

1. 페이지 로드 시 `window.location.search`에서 `ip`/`port`/`token`/`scheme` 파싱
2. 모두 존재하면 `BASE = scheme://ip:port/token`으로 재구성
3. `GET <BASE>/api/ping` → 응답의 `"type": "remocue"` 확인
4. `POST <BASE>/api/connect`로 연결 통지 → 본격적인 폴링/명령 시작
5. 파라미터가 없거나 불완전하면 사용자에게 수동 연결 화면(QR 스캔 / URL 입력) 노출

### 2.5 수동 연결

카메라를 사용할 수 없는 환경에서는 사용자가 server URL을 직접 입력하여 player에
접속할 수 있습니다. 형식은 `http(s)://<ip>:<port>/<session-token>` 입니다. 브라우저
주소창에 이 URL을 입력하면 랜딩 페이지가 열리므로 이후는 섹션 2.3과 동일합니다.

### 2.6 Bundleable Remotes

이 규격의 기본 사용 시나리오는 사용자가 **웹 브라우저** 에서 리모컨 사이트를
방문하는 것입니다. 그러나 별도의 **Controller 앱** (리모컨 전용 네이티브 앱) 은
리모컨 UI 를 앱 내부 WebView 로 실행하고자 하며, 이를 위해 리모컨 사이트의 정적
자산을 로컬에 **캐싱** 할 필요가 있습니다. 본 절은 그 캐싱·업데이트 규약을 정의합니다.

카탈로그 엔트리에 `version` 필드가 있는 리모컨을 **Bundleable Remote** 라 하며,
아래 규약을 따라야 합니다. `version` 이 없는 리모컨은 Controller 가 **외부 브라우저
fallback** 으로 처리합니다 (즉, 시스템 브라우저로 `url` 을 열어 넘김).

#### 2.6.1 Manifest 파일 규약

Bundleable Remote 의 배포 사이트는 리모컨 엔트리 `url` 과 **같은 디렉터리** 에
`bundle-manifest.json` 파일을 호스팅해야 합니다. 예를 들어 카탈로그 엔트리의
`url` 이 `https://example.com/remote/index.html` 이면, 매니페스트는
`https://example.com/remote/bundle-manifest.json` 이어야 합니다.

**매니페스트 스키마** (`schemaVersion: 1`):

```json
{
  "schemaVersion": 1,
  "version": "1.2.0",
  "entry": "index.html",
  "files": [
    "index.html",
    "app.js",
    "chunks/vendor-a1b2c3.js",
    "style.css",
    "assets/logo.svg",
    "fonts/inter-var.woff2"
  ],
  "sha256": {
    "app.js": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b"
  }
}
```

| 필드 | 필수 | 설명 |
|---|---|---|
| `schemaVersion` | ✓ | 매니페스트 스키마 버전. 현재는 `1` |
| `version` | ✓ | 이 번들의 semver 버전. **카탈로그 엔트리의 `version` 과 일치해야 함** |
| `entry` | ✓ | Controller 가 WebView 에 로드할 최초 HTML 파일. 일반적으로 `"index.html"` |
| `files` | ✓ | 번들에 포함되는 **모든** 파일의 상대 경로 목록. 매니페스트 자신은 포함하지 않음 |
| `sha256` | | 파일별 SHA-256 해시 (hex). Controller 는 선언된 파일에 대해 다운로드 후 검증 |

**중요한 제약**:

- `files` 목록은 **완전해야** 합니다. 런타임에 동적으로 참조되는 모든 JS 청크,
  이미지, 폰트, WASM 등이 명시되어야 합니다. 매니페스트에 없는 파일은 오프라인에서
  로드되지 않습니다.
- 경로는 매니페스트 파일 기준 **상대 경로** 이며 `./` 또는 절대 경로(`/`) 없이
  기술합니다. 디렉터리 구조는 `/` 구분자를 사용합니다.
- HTML/JS/CSS 내 자산 참조는 **상대 경로** 여야 합니다 (예: `<script src="app.js">`,
  `url(fonts/inter-var.woff2)`). 루트 절대 경로(`/app.js`) 는 WebView 의 `file://`
  로딩 시 깨집니다.
- 외부 CDN (`https://cdnjs.cloudflare.com/...`) 참조는 허용되나, **오프라인에서
  동작하지 않게** 됩니다. 오프라인 지원이 필요한 번들은 모든 자산을 동일 디렉터리에
  포함해야 합니다.

#### 2.6.2 Controller 측 다운로드·캐싱 흐름

1. 카탈로그 로드 후, `version` 필드가 있는 각 엔트리에 대해:
   - 로컬 캐시된 번들의 버전과 카탈로그의 `version` 을 비교
   - 캐시 미존재 또는 캐시 버전 < 카탈로그 버전 인 경우 **업데이트 필요**
2. 업데이트 필요 시:
   - `{url_directory}/bundle-manifest.json` fetch
   - 매니페스트의 `version` 이 카탈로그 `version` 과 일치하는지 검증 (불일치 시 배포
     동기화 실패로 간주, 외부 브라우저 fallback)
   - `files` 목록의 각 파일을 병렬 다운로드, `sha256` 이 선언된 파일은 검증
   - 모든 파일이 성공적으로 받아진 경우에만 **원자적** 으로 기존 캐시를 교체
     (temp 디렉터리 → rename)
3. 사용자가 리모컨 선택 시:
   - 로컬 캐시의 `entry` 파일을 WebView 로 로드 (`file://` 스킴)
   - 쿼리 파라미터 `ip` / `port` / `token` / `scheme` 을 URL 에 부가하여 자동 연결
     (섹션 2.4 의 auto-connect 규약과 동일)

#### 2.6.3 리모컨 개발자 권장 빌드 스텝

대부분의 프론트엔드 번들러 (Vite, Webpack, Parcel, esbuild) 에서 빌드 산출물
디렉터리 (`dist/`, `build/`) 를 순회하여 매니페스트를 자동 생성하는 스크립트를
추가할 것을 권장합니다. 의사코드 예시:

```javascript
// build.post-step.js
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
import { createHash } from 'crypto';
import { relative, join } from 'path';

const DIST = 'dist';
const VERSION = process.env.npm_package_version;

function walk(dir) {
  return readdirSync(dir).flatMap((name) => {
    const p = join(dir, name);
    return statSync(p).isDirectory() ? walk(p) : [p];
  });
}

const files = walk(DIST)
  .map((p) => relative(DIST, p).replaceAll('\\', '/'))
  .filter((p) => p !== 'bundle-manifest.json');

const sha256 = Object.fromEntries(
  files.map((p) => [
    p,
    createHash('sha256').update(readFileSync(join(DIST, p))).digest('hex'),
  ]),
);

writeFileSync(
  join(DIST, 'bundle-manifest.json'),
  JSON.stringify(
    { schemaVersion: 1, version: VERSION, entry: 'index.html', files, sha256 },
    null,
    2,
  ),
);
```

#### 2.6.4 Backward Compatibility

- `version` 이 없는 기존 리모컨 → Controller 는 외부 브라우저로 fallback. 기존
  Player 랜딩 페이지 방식은 어떤 경우에도 영향 없음.
- 매니페스트 fetch 실패 / 파일 검증 실패 → Controller 는 외부 브라우저 fallback.
  조용한 실패 지양, 사용자에게 "이 리모컨은 앱 내 실행이 불가하여 브라우저로
  엽니다" 고지 권장.
- Player 유형 클라이언트는 `version` 필드와 매니페스트를 완전히 **무시** 합니다.
  기존 브라우저 리모컨의 동작은 어떤 변경도 없습니다.

---

## 3. Server Specification

### 3.1 RemoCue Player가 구현하는 사항

| 항목 | 설명 |
|------|------|
| HTTP 서버 | LAN 내에서 접근 가능한 HTTP(S) 서버 |
| 세션 토큰 | URL 경로 접두사로 사용되는 랜덤 토큰 |
| QR 코드 | 서버 URL을 인코딩한 QR 코드 표시 |
| CORS | `Access-Control-Allow-Origin: *` 및 관련 헤더 필수 (상세: 5.9 참조) |
| JSON | 모든 API 응답은 `Content-Type: application/json` |
| HTTP 프록시 | 외부 URL에 대한 GET/POST 프록시 지원 (CORS 우회용) |

### 3.2 기본 포트

RemoCue Player는 **HTTP와 HTTPS를 동시에** 서비스합니다.

| 포트 | 프로토콜 | 용도 |
|------|---------|------|
| 8081 | HTTP  | Remote Client 연결용 (기존 클라이언트 호환) |
| 8443 | HTTPS | Remote Client 연결용 (브라우저 Secure Context 필요 시) |

두 서버는 동일한 라우팅과 엔드포인트를 공유하며, 세션 토큰도 동일합니다.
어느 한 쪽 연결이 실패해도 다른 쪽으로 폴백할 수 있습니다.

### 3.3 HTTPS

HTTPS 지원은 **브라우저 기반 리모컨에서 사실상 필수**입니다. 다음 상황에서 HTTPS가 요구됩니다:

1. **Mixed Content 차단 회피** — HTTPS로 호스팅된 웹 리모컨(예: GitHub Pages)은 HTTP 플레이어에 fetch할 수 없습니다. 브라우저가 요청을 차단하므로 player가 HTTPS 엔드포인트를 노출해야 합니다.
2. **Secure Context API 요구사항** — 브라우저의 `getUserMedia()`(카메라/QR 스캔), Clipboard API, Service Worker 등은 HTTPS 또는 localhost에서만 동작합니다.
3. **Private Network Access (PNA)** — 최신 Chrome은 공개 사이트가 사설망(192.168.x.x 등)에 접근할 때 추가 제약을 걸 수 있으며 HTTPS 페어링이 통과율을 높입니다.

#### 자체 서명 인증서

RemoCue Player는 최초 실행 시 **자체 서명 X.509 인증서**를 생성해 앱 로컬 저장소에 영속화합니다.

| 항목 | 값 |
|------|-----|
| 키 길이 | RSA 2048 bit |
| 유효 기간 | 10년 |
| Common Name | `RemoCuePlayer` |
| 저장 위치 | 앱 전용 저장소 (Application Documents) |

인증서가 신뢰되지 않은 상태로 fetch를 시도하면 브라우저가 네트워크 레벨에서 요청을 거부하므로(CORS 프리플라이트 이전), **사용자가 HTTPS 엔드포인트를 한 번 직접 방문해서 인증서를 수락**해야 합니다.

#### 인증서 수락 흐름

웹 리모컨이 HTTPS fetch 실패(`ERR_CERT_AUTHORITY_INVALID` 등)를 감지하면 다음
흐름으로 사용자를 가이드할 것을 권장합니다:

1. 인증서 수락 안내 모달 표시
2. 사용자가 "새 탭에서 인증서 수락" 링크 클릭 → `https://<ip>:8443/<token>` 열림
3. 브라우저가 인증서 경고 표시 → 사용자가 "고급 → 진행" 선택
4. Player가 **카탈로그 기반 랜딩 페이지**(섹션 2.3)를 반환 — 사용자는 여기서 원하는 리모컨 카드를 다시 클릭해 해당 리모컨 페이지로 이동 가능
5. 또는 사용자가 원래 탭으로 돌아가서 재시도 (이제 인증서가 수락된 상태이므로 fetch 성공)

---

## 4. Authentication

| 항목 | 설명 |
|------|------|
| 세션 토큰 | 모든 API 경로의 prefix (예: `/<token>/api/state`) |
| 토큰 형식 | 8자리 HEX 문자열 (4바이트 랜덤) |
| 토큰 수명 | 서버 시작 시마다 새로 생성 |
| 클라이언트 식별 | Remote Client는 모든 API 요청에 `X-RemoCue-Remote: true` 헤더를 전송해야 합니다 |
| 보안 모델 | 동일 Wi-Fi 네트워크 접근 + QR 코드 토큰 (별도 계정/비밀번호 없음) |

> 잘못된 토큰으로의 요청은 `404`를 반환합니다.

---

## 5. API Endpoints

> 모든 엔드포인트는 `/<session-token>` 접두사가 필요합니다.
> 예: `GET /<token>/api/state`

---

### 5.1 `GET /api/ping`

서버 감지. Remote Client가 연결 시 가장 먼저 호출합니다.

**Response:**
```json
{
  "ok": true,
  "type": "remocue"
}
```

---

### 5.2 `GET /api/state`

플레이어 전체 상태 조회. Remote Client가 **1초 간격**으로 폴링합니다.

**Response:**
```json
{
  "currentSong": {
    "title": "제목",
    "artist": "아티스트",
    "videoId": "unique-media-id",
    "songNumber": "12345",
    "source": "provider-name"
  },
  "status": "playing",
  "currentTime": 123456,
  "duration": 234567,
  "volume": 1.0,
  "playbackRate": 1.0,
  "pitchSemitones": 0,
  "queueCount": 3,
  "queueIndex": 0,
  "nextSong": {
    "title": "다음곡 제목",
    "artist": "아티스트",
    "videoId": "abc123"
  },
  "queue": [
    {
      "queueId": "uuid-string",
      "title": "곡 제목",
      "artist": "아티스트",
      "videoId": "abc123",
      "source": "provider-name"
    }
  ],
  "abRepeat": {
    "enabled": false,
    "pointA": null,
    "pointB": null
  },
  "isFullscreen": false
}
```

**필드 상세:**

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `currentSong` | object \| null | Yes | 현재 재생 중인 미디어 정보 |
| `currentSong.title` | string | Yes | 제목 |
| `currentSong.artist` | string | Yes | 아티스트/제작자 |
| `currentSong.videoId` | string | Yes | 미디어 고유 ID |
| `currentSong.songNumber` | string | No | 곡 번호 (노래방 등) |
| `currentSong.source` | string | No | 콘텐츠 출처 |
| `status` | string | Yes | `"playing"` \| `"paused"` \| `"buffering"` \| `"idle"` \| `"ended"` \| `"error"` |
| `currentTime` | int | Yes | 현재 재생 위치 (밀리초) |
| `duration` | int | Yes | 전체 길이 (밀리초) |
| `volume` | double | Yes | 볼륨 (0.0 ~ 1.0) |
| `playbackRate` | double | Yes | 재생 속도 배율 (1.0 = 보통) |
| `pitchSemitones` | int | Yes | 키 조절 (-6 ~ +6 반음) |
| `queueCount` | int | Yes | 대기열 곡 수 |
| `queueIndex` | int | Yes | 현재 재생 인덱스 |
| `nextSong` | object \| null | No | 다음 곡 정보 |
| `queue` | array | Yes | 대기열 목록 |
| `queue[].queueId` | string | Yes | 대기열 항목 고유 ID (UUID) |
| `abRepeat` | object | Yes | A-B 구간 반복 상태 |
| `isFullscreen` | bool | Yes | 전체화면 상태 |

> **참고**: 재생 히스토리와 즐겨찾기는 Remote Client에서 자체 관리합니다 (예: localStorage).
> Player는 재생과 대기열 관리에만 집중합니다.

---

### 5.3 `POST /api/command`

플레이어 제어 명령

**Request:**
```json
{
  "command": "commandName",
  "params": {}
}
```

**Response:**
```json
{
  "ok": true
}
```

#### 재생 제어

| Command | Params | 설명 |
|---------|--------|------|
| `play` | - | 재생. 현재 곡이 없으면 대기열 첫 곡 시작 |
| `pause` | - | 일시정지 |
| `togglePlayPause` | - | 재생/일시정지 토글. 현재 곡이 없으면 대기열 첫 곡 시작 |
| `next` | - | 다음 곡 |
| `previous` | - | 현재 곡 처음으로 |
| `seek` | `{"position": int}` | 특정 위치로 이동 (밀리초) |

#### 볼륨 / 속도 / 키

| Command | Params | 설명 |
|---------|--------|------|
| `setVolume` | `{"volume": double}` | 볼륨 설정 (0.0 ~ 1.0) |
| `setSpeed` | `{"rate": double}` | 재생 속도 설정 |
| `pitchUp` | - | 키 +1 반음 (최대 +6) |
| `pitchDown` | - | 키 -1 반음 (최소 -6) |
| `pitchReset` | - | 키 초기화 (0) |

#### A-B 구간 반복

| Command | Params | 설명 |
|---------|--------|------|
| `setPointA` | - | 현재 위치를 A 지점으로 설정 |
| `setPointB` | - | 현재 위치를 B 지점으로 설정 |
| `clearABRepeat` | - | A-B 구간 해제 |
| `toggleABRepeat` | - | A-B 반복 토글 |

#### 대기열(Queue) 관리

| Command | Params | 설명 |
|---------|--------|------|
| `playFromQueue` | `{"index": int}` | 대기열에서 해당 인덱스 곡 재생 |
| `removeFromQueue` | `{"queueId": "string"}` | 대기열에서 곡 제거 (UUID) |
| `reorderQueue` | `{"from": int, "to": int}` | 대기열 순서 변경 |
| `shuffle` | - | 대기열 셔플 |
| `clearQueue` | - | 대기열 전체 삭제 |

#### 즉시 재생

| Command | Params | 설명 |
|---------|--------|------|
| `playNow` | `{"videoId", "title"?, "artist"?, "source"?, "streamUrl", "streamHeaders"?, "queueLoop"?}` | 즉시 재생. **`streamUrl` 필수**. `streamHeaders`로 재생 시 사용할 HTTP 헤더 전달 가능. `queueLoop`로 대기열 반복 모드 설정 가능 |

> **중요**: `playNow` 명령에는 `streamUrl`이 반드시 포함되어야 합니다.
> RemoCue Player는 콘텐츠 소스에 독립적이며, 미디어 URL을 직접 추출하지 않습니다.
> URL 추출은 Remote Client의 책임입니다.

> **streamHeaders** (선택): 스트림 URL 재생 시 Player의 미디어 엔진에 전달할 HTTP 헤더입니다.
> 콘텐츠 제공자가 특정 User-Agent, Referer 등을 요구하는 경우 사용합니다.
> ```json
> "streamHeaders": {
>   "User-Agent": "Mozilla/5.0 ...",
>   "Referer": "https://www.example.com/",
>   "Origin": "https://www.example.com"
> }
> ```

> **queueLoop** (선택, boolean): 대기열 반복 모드를 설정합니다.
> - `true`: 재생이 끝난 항목이 대기열의 마지막으로 이동하여 전체 목록이 무한 반복됩니다
> - `false`: 재생이 끝난 항목이 대기열에서 제거됩니다 (기본 동작)
> - 미지정: 현재 모드를 그대로 유지합니다 (변경 없음)
>
> 이 파라미터는 한 번 설정되면 명시적으로 변경할 때까지 유지되는 모드성 설정입니다.
> 매 재생 명령마다 다시 보낼 수도 있고, 한 번만 보내도 됩니다.

#### 대기열 추가

| Command | Params | 설명 |
|---------|--------|------|
| `addToQueue` | `{"videoId", "title"?, "artist"?, "source"?, "streamUrl", "streamHeaders"?, "queueLoop"?}` | 대기열에 추가. 재생 중인 곡이 없으면 자동 시작. `playNow`와 동일한 params 지원 |

#### 전체화면

| Command | Params | 설명 |
|---------|--------|------|
| `enterFullscreen` | - | Player를 전체화면 모드로 전환 |
| `exitFullscreen` | - | Player를 일반 화면 모드로 전환 |

> Player의 전체화면 상태는 `GET /api/state`의 `isFullscreen` 필드로 확인할 수 있습니다.

---

### 5.4 `POST /api/queue/add`

대기열에 미디어 추가

**Request:**
```json
{
  "videoId": "unique-media-id",
  "title": "제목",
  "artist": "아티스트",
  "source": "provider-name",
  "streamUrl": "https://media-stream-url...",
  "streamHeaders": {
    "User-Agent": "...",
    "Referer": "https://..."
  },
  "queueLoop": true
}
```

> `streamUrl`은 플레이어가 재생할 실제 미디어 스트림 URL입니다.
> `streamHeaders`는 선택사항이며, 재생 시 미디어 엔진에 전달할 HTTP 헤더입니다.
> `queueLoop`는 선택사항이며, 대기열 반복 모드를 설정합니다 (자세한 내용은 5.3 `playNow` 섹션 참조).
> 현재 재생 중인 곡이 없으면 자동으로 재생을 시작합니다.

**Response:**
```json
{
  "ok": true
}
```

---

### 5.5 `POST /api/connect`

Remote Client 연결 알림

**Response:**
```json
{
  "ok": true
}
```

---

### 5.6 `POST /api/disconnect`

Remote Client 연결 해제 알림

**Response:**
```json
{
  "ok": true
}
```

---

### 5.7 `GET /api/resolve`

**외부 URL에 대한 GET 프록시**. 브라우저 기반 Remote Client가 CORS 제한을 우회하여 외부 리소스에 접근할 수 있도록 합니다.

**Request:**
```
GET /<token>/api/resolve?url=https://www.example.com/resource
```

**Response:** 프록시된 원본 응답 (상태 코드, Content-Type 포함)

**응답 헤더:**

| 헤더 | 설명 |
|------|------|
| `X-Original-Set-Cookie` | 원본 응답의 `Set-Cookie` 헤더 (브라우저가 직접 Set-Cookie를 차단하므로 커스텀 헤더로 전달) |
| `X-Original-Content-Length` | 원본 응답의 Content-Length |

> Player는 영속 HttpClient를 사용하여 프록시 요청 간 쿠키/세션을 유지합니다.

---

### 5.8 `POST /api/resolvepost`

**외부 URL에 대한 POST 프록시**. GET/POST/HEAD 등 다양한 HTTP 메서드를 지원합니다.

**Request:**
```json
{
  "url": "https://www.example.com/api/endpoint",
  "method": "POST",
  "body": { "key": "value" },
  "headers": {
    "Content-Type": "application/json",
    "Custom-Header": "value"
  }
}
```

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `url` | string | Yes | 프록시할 대상 URL |
| `method` | string | No | HTTP 메서드 (기본: `POST`). `GET`, `POST`, `HEAD` 지원 |
| `body` | any | No | 요청 바디 (POST 시). 객체 또는 문자열 |
| `headers` | object | No | 커스텀 요청 헤더. 지정 시 이 헤더로 요청, 미지정 시 Player 기본 헤더 사용 |

**Player 기본 헤더** (`headers` 미지정 시):
```
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.18 Safari/537.36
Accept: text/html,application/xhtml+xml,...
Accept-Language: en-US,en;q=0.5
Cookie: CONSENT=YES+cb
```

**Response:** 프록시된 원본 응답 (상태 코드, Content-Type 포함)

> `headers`를 지정하면 Player가 해당 헤더를 그대로 대상 서버에 전달합니다.
> `headers`를 생략하면 Player의 기본 헤더를 사용합니다. 기본 헤더는 일관성을 위해 고정되어 있으며, 외부 서비스와의 세션 연속성을 보장합니다.

---

### 5.9 `OPTIONS /api/*`

CORS Preflight 응답

**Required Headers:**
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-RemoCue-Remote
Access-Control-Expose-Headers: X-Original-Set-Cookie, X-Original-Content-Length
```

---

### 5.10 Gallery Management API

갤러리 탐색과 파일 관리를 위한 엔드포인트. 이 섹션의 엔드포인트는 player 구현에
포함되어 있으며 리모컨 웹 UI에서 갤러리 브라우저·업로드·다운로드·삭제 기능을
구현하는 데 사용됩니다.

#### `GET /api/gallery/albums`

앨범(폴더) 목록을 반환합니다. 시스템 갤러리 앨범 + 가상 "Uploads" 앨범이 포함됩니다.

**Query parameters:**
- `type` (선택): `image` 또는 `video` — 해당 유형의 미디어만 포함하는 앨범을 반환

**Response:**
```json
{
  "ok": true,
  "albums": [
    { "id": "...", "name": "All", "count": 150, "isAll": true },
    { "id": "__uploads__", "name": "Uploads", "count": 3, "isAll": false }
  ]
}
```

#### `GET /api/gallery/media`

앨범 내 미디어 목록 (페이지네이션).

**Query parameters:**
- `albumId` (필수): 앨범 ID (`__uploads__` 포함)
- `page` (선택, 기본 0): 페이지 번호
- `size` (선택, 기본 50): 페이지당 항목 수

**Response:**
```json
{
  "media": [
    { "id": "...", "type": "image", "filename": "IMG_001.jpg", "width": 1920, "height": 1080, "duration": 0, "createDate": "..." }
  ]
}
```

#### `GET /media/<id>/thumb`

200×200 JPEG 썸네일. `X-RemoCue-Remote` 헤더 불필요 (`<img src>` 직접 사용 가능).

#### `GET /media/<id>/file`

원본 파일 스트리밍. HTTP Range 요청 지원.

**Query parameters:**
- `download` (선택): `1`이면 `Content-Disposition: attachment; filename="원본이름"` 헤더 추가 → 브라우저 다운로드 트리거

#### `POST /api/gallery/upload`

리모컨 브라우저에서 player 기기로 파일 업로드.

**Content-Type:** `multipart/form-data`
**Form field:** `file` — 업로드할 파일 (허용: jpg, jpeg, png, gif, webp, heic, mp4, mov, mkv, webm, avi 등, 최대 500MB)

**Response:**
```json
{
  "ok": true,
  "media": { "id": "upload_uuid", "type": "image", "filename": "photo.jpg", "width": 0, "height": 0, "duration": 0, "createDate": "..." }
}
```

#### `POST /api/gallery/delete`

갤러리 항목 삭제. 업로드된 파일(`upload_` prefix)은 즉시 삭제. 시스템 갤러리 파일은 직접 `File.delete()` 시도 후 실패 시 `PhotoManager.editor.deleteWithIds` fallback.

**Request:**
```json
{ "ids": ["upload_abc123", "system-asset-id"] }
```

**Response:**
```json
{ "ok": true, "deleted": ["upload_abc123"], "failed": ["system-asset-id"] }
```

---

## 6. Error Handling

| HTTP Status | 의미 |
|-------------|------|
| 200 | 성공 |
| 400 | 필수 파라미터 누락 또는 잘못된 요청 |
| 404 | 잘못된 세션 토큰 또는 존재하지 않는 경로 |
| 500 | 서버 내부 오류 |

**에러 응답 형식:**
```json
{
  "ok": false,
  "error": "에러 메시지"
}
```

---

## 7. Sequence Diagram

### 7.1 기본 재생 흐름

```
Remote Client                        RemoCue Player
  |                                       |
  |--- GET /api/ping ------------------->|  1. 서버 감지
  |<-- {"ok":true, "type":"remocue"} ----|
  |                                       |
  |--- POST /api/connect --------------->|  2. 연결 알림
  |<-- {"ok": true} ---------------------|
  |                                       |
  |=== 연결 유지 (1초 간격 폴링) ==========|
  |                                       |
  |--- GET /api/state ------------------>|  3. 상태 조회
  |<-- {currentSong, queue, ...} --------|
  |                                       |
  |--- POST /api/command --------------->|  4. 재생 명령
  |    {"command":"playNow",             |
  |     "params":{"streamUrl":"...",     |
  |       "streamHeaders":{...}}}        |
  |<-- {"ok": true} ---------------------|
  |                                       |
  |--- POST /api/queue/add ------------->|  5. 대기열 추가
  |    {"streamUrl":"...", "title":"..."}|
  |<-- {"ok": true} ---------------------|
  |                                       |
  |--- POST /api/command --------------->|  6. 제어 (피치, 배속 등)
  |    {"command":"pitchUp"}             |
  |<-- {"ok": true} ---------------------|
  |                                       |
  |=== 연결 종료 ==========================|
  |                                       |
  |--- POST /api/disconnect ------------>|  7. 연결 해제
  |<-- {"ok": true} ---------------------|
```

### 7.2 프록시를 활용한 스트림 URL 추출 흐름

Remote Client가 브라우저 환경에서 외부 서비스의 스트림 URL을 추출할 때, Player의 HTTP 프록시를 활용하여 CORS 제한을 우회할 수 있습니다.

```
Remote Client (Browser)              RemoCue Player              External Service
  |                                       |                           |
  |--- POST /api/resolvepost ----------->|                           |
  |    {"url":"https://ext/api",         |--- HTTP Request -------->|
  |     "method":"POST",                 |                           |
  |     "body":{...}}                    |<-- Response --------------|
  |<-- Proxied Response -----------------|                           |
  |                                       |                           |
  | [Client-side: extract streamUrl      |                           |
  |  from response]                      |                           |
  |                                       |                           |
  |--- POST /api/command --------------->|                           |
  |    {"command":"playNow",             |                           |
  |     "params":{"streamUrl":"...",     |--- Play stream URL ----->|
  |       "streamHeaders":{...}}}        |   (with matching headers) |
  |<-- {"ok": true} ---------------------|                           |
```

> **핵심**: Player의 영속 HttpClient가 외부 서비스와의 세션/쿠키를 유지하므로,
> Remote Client가 프록시를 통해 여러 단계의 요청을 수행할 때도 세션 연속성이 보장됩니다.
> 스트림 URL 재생 시 `streamHeaders`로 추출 단계와 동일한 HTTP 헤더를 전달하면
> 외부 서비스의 헤더 검증을 통과할 수 있습니다.

---

## 8. Implementation Guide for Remote Client Developers

RemoCue Player를 제어하는 리모컨을 구현할 때 참고하세요:

### 연결

1. QR 코드 또는 수동 입력으로 `http(s)://<ip>:<port>/<token>` 획득
2. `GET /<token>/api/ping` → `"type": "remocue"` 확인
3. `POST /<token>/api/connect` 호출
4. 1초 간격으로 `GET /<token>/api/state` 폴링 시작

### 미디어 재생

Remote Client가 미디어를 재생하려면 **streamUrl**(직접 재생 가능한 미디어 URL)을 함께 전송해야 합니다:

```json
POST /api/command
{
  "command": "playNow",
  "params": {
    "videoId": "unique-id",
    "title": "곡 제목",
    "artist": "아티스트",
    "streamUrl": "https://actual-media-stream-url...",
    "streamHeaders": {
      "User-Agent": "Mozilla/5.0 ...",
      "Referer": "https://www.example.com/"
    }
  }
}
```

### streamHeaders 활용

일부 콘텐츠 제공자는 스트림 URL 접근 시 특정 HTTP 헤더를 요구합니다. 이 경우 `streamHeaders`를 통해 Player의 미디어 엔진에 헤더를 전달할 수 있습니다:

- **User-Agent**: 스트림 URL 추출 시 사용한 것과 동일한 값 사용
- **Referer / Origin**: 콘텐츠 제공자의 도메인

### HTTP 프록시 활용

브라우저 기반 Remote Client는 CORS 제한으로 외부 서비스에 직접 접근할 수 없습니다. Player의 프록시 엔드포인트를 활용하세요:

- `GET /api/resolve?url=<encoded-url>` — 외부 URL GET 요청
- `POST /api/resolvepost` — 외부 URL POST/GET/HEAD 요청 (메서드, 헤더, 바디 지정 가능)

프록시는 영속 HttpClient를 사용하므로:
- 외부 서비스와의 쿠키/세션이 요청 간 유지됩니다
- 기본 헤더가 일관되게 적용됩니다
- gzip 응답이 자동으로 해제됩니다

### 지원 미디어 포맷

RemoCue Player는 media_kit(mpv) 기반이므로 다음 포맷을 지원합니다:
- **컨테이너**: MP4, MKV, WebM, FLV, AVI, MOV
- **오디오**: MP3, AAC, FLAC, OGG, WAV, M4A, OPUS
- **스트리밍**: HLS (m3u8), DASH, HTTP Progressive
- **코덱**: H.264, H.265, VP8, VP9, AV1

### 히스토리/즐겨찾기 관리

**히스토리와 즐겨찾기는 Remote Client에서 자체 관리합니다.** Player는 이 데이터를 저장하거나 제공하지 않습니다.

권장 구현:
- 브라우저 기반: `localStorage`에 JSON 배열로 저장
- 네이티브 앱: SQLite 또는 로컬 파일

저장 항목:
```json
{
  "videoId": "abc123",
  "title": "곡 제목",
  "artist": "아티스트",
  "source": "media",
  "streamUrl": "https://...",
  "streamUrlCreatedAt": 1775061343000,
  "playedAt": "2026-04-02T19:30:00Z"
}
```

**streamUrl 캐시**: 콘텐츠 제공자의 스트림 URL은 시간이 지나면 만료됩니다 (예: YouTube는 약 6시간).
`streamUrlCreatedAt`을 저장하여 만료 여부를 판단하고, 만료 시 재추출합니다.

### 연결 해제

앱 종료 또는 연결 해제 시 `POST /<token>/api/disconnect`를 호출하여 정리합니다.

---

## 9. Compatible Remote Registration

이 섹션은 완성된 호환 리모컨을 **RemoCue Player 공식 카탈로그**(`remotes.json`)에 등록 신청하는 절차를 설명합니다. 등록이 승인되면 해당 리모컨은 전 세계 RemoCue Player 사용자의 landing page(섹션 2.3)와 내장 리모컨 선택 UI에 **자동으로 노출**됩니다. 사용자는 별도의 URL 입력 없이 카드 한 번 탭으로 당신이 만든 리모컨에 접근할 수 있게 됩니다.

등록 신청은 의무가 아닙니다. 로컬/사내 배포만 원한다면 사용자가 직접 URL을 입력하는 방식으로도 충분히 사용 가능합니다. 이 섹션은 공개 배포를 원하는 개발자를 위한 안내입니다.

### 9.1 사전 요구사항

등록 신청 전에 다음 조건을 모두 충족해야 합니다:

1. **HTTPS 호스팅 필수** — 리모컨 웹페이지는 반드시 HTTPS로 제공되어야 합니다. HTTP로 호스팅된 페이지는 Mixed Content 정책 때문에 HTTPS player에 fetch할 수 없어 Player 사용자의 기기에서 동작하지 않습니다. GitHub Pages, Netlify, Vercel, Cloudflare Pages, Surge.sh 등 무료 HTTPS 호스팅을 활용할 수 있습니다.
2. **자동 연결 구현** — 페이지 로드 시 `window.location.search`에서 `ip`/`port`/`token`/`scheme` 쿼리 파라미터를 읽어 섹션 2.4 절차에 따라 player에 자동 연결해야 합니다. 사용자가 URL을 수동 입력하는 fallback은 선택 구현입니다.
3. **최소 프로토콜 준수** — `GET /api/ping`으로 player 감지, `POST /api/connect`로 연결 통지, 핵심 명령(`playNow`, `pause`, `next` 등) 중 적어도 하나 이상을 실제로 사용할 수 있어야 합니다. 부분 구현도 카테고리에 따라 허용됩니다 (예: 컨트롤 전용 리모컨은 `playNow`를 구현하지 않아도 됨).
4. **자체 서명 인증서 수락 안내** — Player는 HTTPS(8443)를 자체 서명 인증서로 제공합니다. 리모컨은 fetch 실패 시 사용자에게 인증서 수락 방법을 안내하는 UI(섹션 3.3의 인증서 수락 흐름)를 구현하는 것을 권장합니다.

### 9.2 제출 정보

등록 신청 시 다음 정보를 제출합니다. 필드 이름은 카탈로그 엔트리의 스키마(섹션 2.2)와 1:1로 매핑됩니다.

#### 필수 정보

| 필드 | 설명 |
|---|---|
| `name` | 리모컨 표시 이름. 한국어(`ko`)와 영어(`en`) 두 로케일 필수 |
| `description` | 한 줄 설명. 한국어와 영어 두 로케일 필수. 2~3줄 이내로 핵심 기능을 요약 |
| `url` | HTTPS로 호스팅된 리모컨 웹페이지의 canonical URL. 리다이렉트 없이 직접 접속되어야 함 |
| `category` | 리모컨의 주 용도. `gallery` / `karaoke` / `music` / `video` / `utility` / `custom` 중 하나 |

> **`id` 필드에 대하여**: `id`는 카탈로그 내 고유 식별자이며, 신청자가 직접 제안하지 않습니다. RemoCue 팀이 검토 후 카탈로그에 등록할 때 `name`과 `category`를 기반으로 적절한 값을 **자동 부여**합니다 (예: `my-karaoke-youtube`, `gallery-classic`).

#### 권장 정보

| 필드 | 설명 |
|---|---|
| `icon` | 리모컨을 나타내는 단일 이모지. 카탈로그 리스트와 카드에서 시각 식별에 사용 (예: `🎤`, `📷`, `🎵`) |
| `features` | 지원 기능을 나타내는 태그 배열. 필터링과 검색에 사용. 예: `["queue", "pitch-control", "youtube-search"]` |
| `protocolVersion` | 리모컨이 구현한 최소 프로토콜 버전. 현재 최신은 `"3.4"` |
| `author` | 제작자 또는 팀 이름. 카탈로그 상세에 표시됨 |
| `homepage` | 소스 저장소, 홈페이지, 또는 소개 글의 URL |

#### 테스트 확인 정보

검토를 위한 재현 정보로 다음을 함께 제출해 주세요:

- **Player 버전**: 테스트 시 사용한 RemoCue Player 앱의 버전 (예: `1.4.0`)
- **브라우저**: 자동 연결 흐름이 동작함을 확인한 브라우저 목록 (예: `iOS Safari 17, Chrome 131`)
- **비고**: 알려진 제약이나 특수 사항 (선택)

### 9.3 제출 양식

아래 YAML 스타일 양식을 복사해 채운 뒤 섹션 9.4의 채널로 전송해 주세요.

```yaml
━━━ RemoCue 리모컨 등록 신청 ━━━

name:
  ko: 내 리모컨 이름
  en: My Remote Name
description:
  ko: 이 리모컨의 한 줄 설명
  en: One-line description in English
url: https://your-host.example/remote.html
category: custom
icon: 🎛️
features: [queue, playback-controls]
protocolVersion: "3.4"
author: Your Name
homepage: https://github.com/you/repo

테스트 완료 환경:
  - Player 버전:
  - 브라우저:
  - 비고:
━━━━━━━━━━━━━━━━━━━━━━━━
```

### 9.4 제출 채널

**공식 텔레그램 채널**: <https://t.me/+HOWCzZmQGb9iZGVl>

양식을 그대로 붙여넣어 주세요. 이 채널은 등록 신청 외에도 프로토콜 관련 질문, 버그 리포트, 기능 제안, 리모컨 개발자 간 정보 공유 목적으로도 활용됩니다.

### 9.5 검토 절차

1. **접수 확인** — 제출 직후 자동 응답은 없습니다. RemoCue 팀이 접수 사실을 채널에 확인 코멘트로 남깁니다.
2. **기능 검토** — 제공된 `url`에 직접 접속해 다음을 확인합니다:
   - 쿼리 파라미터 없이 순수 URL로 접속했을 때 정상적인 fallback UI가 표시되는지
   - `?ip=&port=&token=&scheme=` 파라미터가 있을 때 자동 연결이 동작하는지
   - `/api/ping`과 `/api/connect` 호출이 올바른지
   - 명시된 `features` 태그가 실제 동작과 일치하는지
3. **카탈로그 반영** — 검토를 통과하면 RemoCue 팀이 `id`를 부여하고 `remotes.json`에 엔트리를 추가합니다. 업데이트는 GitHub Pages 배포 주기에 따라 **1~24시간 내** 전 세계에 반영됩니다.
4. **수정 요청** — 문제가 발견되면 채널을 통해 구체적인 수정 사항을 전달합니다. 수정 후 재제출하시면 됩니다.

### 9.6 등록 이후

- **URL 변경**: 호스팅 이전 등으로 `url`이 바뀔 경우 채널을 통해 알려주시면 카탈로그 엔트리를 갱신합니다. 사용자의 브라우저 localStorage에 이전 URL이 캐시되어 있을 수 있으므로 30일간은 이전 URL도 유지하는 것을 권장합니다.
- **제거 요청**: 언제든 등록 해제를 요청할 수 있습니다. 제거 후에도 이미 사용 중이던 사용자는 직접 URL 입력으로 계속 사용할 수 있습니다.
- **버전 업데이트**: 리모컨이 새 프로토콜 버전을 지원하도록 업데이트되면 `protocolVersion` 필드 갱신을 요청해 주세요. 기능 추가/제거 시 `features` 태그도 함께 갱신됩니다.

---

## 10. Chroma API (Protocol v3.7)

Chroma API는 **현재 재생 중인 음원의 피치 클래스 에너지 분포(chromagram)** 를 연결된 리모컨에 실시간으로 제공한다. 플레이어는 이 데이터를 "공급"만 하고, 무엇에 사용할지는 각 리모컨이 자유롭게 정한다.

### 10.1 목적과 활용

Chromagram은 각 시점의 오디오를 12개 반음 클래스(C, C#, D, …, B)에 얼마나 분포하는지를 0–1 범위의 벡터로 요약한 정보다. 하나의 지배적 멜로디가 없어도(=전형적인 polyphonic 반주) 의미 있는 값을 뽑을 수 있기 때문에, 어떤 종류의 음원이든 적용 가능하다.

리모컨이 이 데이터를 활용할 수 있는 예시:
- 실시간 피치 시각화 (스펙트럼 오브 · 화성 휠 · 멜로디 미터)
- 화성 분석 UI (현재 코드 추정 · 조성 감지 · 키 비주얼라이저)
- 사용자 마이크 입력과 결합한 음정 매칭 스코어링
- 스케일·화성 학습 도우미
- 라이브 오디오에 반응하는 조명·애니메이션 연출

플레이어는 이 중 어떤 것도 강제하지 않는다. 구현은 리모컨 책임.

### 10.2 세션 흐름

```
Remote                                          Player
  │ POST /api/chroma/start   { }                  │
  │ ─────────────────────────────────────────────► │   audio tap 시작, ring buffer 준비
  │                                         ◄───── │   200 { active: true }
  │                                                │
  │ GET /api/state?chromaSince=<last_t>            │
  │ ─────────────────────────────────────────────► │   state + 직전 chromaSince 이후 프레임만 반환
  │                                         ◄───── │   200 { ..., chroma: { active, frames: [...] } }
  │     (리모컨은 마지막 t 를 갱신 후 다음 폴링에 전달)      │
  │                                                │
  │ POST /api/chroma/stop                          │
  │ ─────────────────────────────────────────────► │   audio tap 정지, 버퍼 해제
  │                                         ◄───── │   200 { active: false }
```

Chroma 프레임은 기존 `/api/state` 폴링에 **피기백(piggyback)** 된다. 별도 폴링 루프가 필요 없다.

### 10.3 엔드포인트

#### `POST /api/chroma/start`

```
Request body: {}   (향후 샘플레이트·프레임 길이 파라미터 예약)
Response:     { "active": true }
```

호출 즉시 플레이어는 현재 재생 중인 오디오 파이프라인에 tap을 걸고 chroma 프레임을 내부 ring buffer에 누적한다. 이미 활성 상태라면 idempotent.

#### `POST /api/chroma/stop`

```
Request body: {}
Response:     { "active": false }
```

Audio tap을 해제하고 ring buffer를 비운다. 이미 비활성 상태라도 200을 반환한다.

#### `GET /api/state` — chroma 필드 확장

Chroma 세션이 활성일 때 `/api/state` 응답에 `chroma` 객체가 추가된다.

```json
{
  "currentTime": 45.3,
  "currentSong": { ... },
  "queue": [ ... ],
  ...

  "chroma": {
    "active": true,
    "frames": [
      { "t": 42.00, "c": [0.12, 0.35, 0.98, 0.11, 0.44, 0.07, 0.22, 0.18, 0.51, 0.09, 0.14, 0.31] },
      { "t": 42.05, "c": [...] },
      { "t": 42.10, "c": [...] }
    ]
  }
}
```

Chroma 세션이 비활성이면 `chroma` 필드는 응답에 포함되지 않는다. 이 필드를 모르는 구형 클라이언트는 그대로 무시하면 되므로 프로토콜 v3.6 이하와 역방향 호환된다.

**쿼리 파라미터 — `chromaSince`** (선택):

```
GET /api/state?chromaSince=44.700
```

리모컨이 마지막으로 받은 프레임의 `t` 값을 전달하면, 플레이어는 `t > chromaSince` 인 프레임만 반환한다. 생략하면 현재 ring buffer 전체를 반환한다. 리모컨은 이 값을 관리해 중복 수신·누락을 방지한다.

### 10.4 Chroma 프레임 포맷

| 필드 | 타입 | 의미 |
|---|---|---|
| `t` | float | 플레이어 시계(`state.currentTime`) 기준 초 |
| `c` | array[12] of float | 12개 피치 클래스 에너지, **각 프레임 내에서 최대값이 1.0으로 max-normalised** (`c[0]=C, c[1]=C#, …, c[11]=B`) |

각 프레임 내 max-normalisation은 트랙의 전체 음량과 무관하게 의미 있는 상대 강도를 보장한다.

### 10.5 생성 파라미터

플레이어가 내부에서 사용하는 값. 리모컨이 직접 설정하지 않는다 (향후 `/api/chroma/start` body 옵션으로 열릴 수 있음).

| 항목 | 값 |
|---|---|
| 샘플레이트 | 22050 Hz (모노 다운믹스) |
| FFT window | 2048 samples (≈ 93 ms) |
| Hop | 1024 samples (50% overlap) |
| 대상 주파수 대역 | 80 – 1100 Hz (보컬 음역대) |
| 프레임 레이트 | ≈ 21.5 fps |

### 10.6 구현 상태

**현재 (v1.8.3)** — 안드로이드(API 29+)에서 종단간 동작. `/api/chroma/start` 호출 시 시스템 MediaProjection 동의 다이얼로그가 한 번 떠서 사용자 승인이 필요하다. 승인 후 foreground service 가 `AudioPlaybackCaptureConfiguration` 으로 자체 프로세스의 미디어 오디오를 캡처하고, 22050 Hz mono Int16 으로 변환해 `ChromagramExtractor`(Dart, fftea 기반)에 전달한다. 캡처 중에는 저우선순위 알림(`Streaming audio chroma to remote`)이 표시된다.

응답 스키마 확장: `/api/chroma/start` 는 `{ active: true, status: <string> }` 를 반환하며 `status` 는 다음 중 하나다.
- `started` — 캡처가 정상적으로 시작됨, 곧 frames 가 흘러옴
- `denied` — 사용자가 마이크 권한 또는 MediaProjection 동의를 거부
- `unsupported` — Android 9 이하 등에서 `AudioPlaybackCaptureConfiguration` 사용 불가
- `error` — 그 외 플랫폼 채널 오류

리모컨은 `started` 가 아닌 경우 `frames` 가 비어있는 상태로 진행될 것을 알고 자체 fallback UX 로 가야 한다.

**알려진 제약**:
- 매 `/api/chroma/start` 마다 시스템 동의 다이얼로그. Android 정책상 영구 캐시 불가.
- iOS · macOS · 데스크톱 미지원. 해당 플랫폼에서는 `status: unsupported`.
- 캡처 활성 중에는 foreground service 알림이 보임 (Android 8+ 정책).
- Bluetooth 출력으로 라우팅된 오디오의 캡처 가능 여부는 미검증.

**알고리즘 검증**: 본 API 위에 구현되는 외부 컨슈머의 채점/시각화 등 알고리즘은 Python 프로토타입과 golden test 로 byte-단위 일치를 확인할 수 있다 (해당 컨슈머의 docs 참고).

---

## 11. License

이 프로토콜 규격은 자유롭게 사용할 수 있습니다.
RemoCue Player를 제어하는 리모컨 소프트웨어를 누구나 개발하고 배포할 수 있습니다.
