diff --git a/.dockerignore b/.dockerignore index 50e7d30..3e4c844 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,5 @@ target config.toml certs docs +# The server embeds the built bundle from static/. +sdk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c1f86d..ee97a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,27 @@ jobs: # The load run and the tests that need ffmpeg are ignored, so this is the # unit suite plus every integration suite. - run: cargo test + + sdk: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: sdk + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: sdk/package-lock.json + + - run: npm ci + + - run: npm run check + + # The server serves this file, so it has to match the sources it was + # built from. + - run: git diff --exit-code ../static/stream-viewer.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e12096f..25f6c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # [Unreleased] +## Added + +- A viewer SDK for browsers, `@kimsun721/stream-viewer`: joining, playback, simulcast layers, perf reports, reconnecting, the relay and leaf sides of P2P relay, a debug log, and a player built on it. See [sdk/README.md](sdk/README.md). It does not answer the upload probe yet, so the server does not promote a browser to relay. +- `/stream-viewer.js` serves the same SDK as one file. + +## Changed + +- `/watch` runs on the SDK. + # [0.1.1] ## Added diff --git a/README.ko.md b/README.ko.md index 74e328f..4fc6b18 100644 --- a/README.ko.md +++ b/README.ko.md @@ -102,12 +102,15 @@ HTTPS `:8443`, 클라이언트가 직접 호출합니다. | 메서드 | 경로 | 인증 | 설명 | | --- | --- | --- | --- | | `GET` | `/watch?room={id}` | 없음 | 최소한의 시청 페이지 | +| `GET` | `/stream-viewer.js` | 없음 | 한 파일로 묶은 뷰어 SDK. `/watch`가 이걸로 동작 | | `POST` | `/offer` | 없음 | 시청자 입장. SDP offer를 보내고 SDP answer를 받음 | | `POST` | `/whip` | 스트림 키 | 송출. `application/sdp`로 주고받음 | | `DELETE` | `/whip/sessions/{id}` | 스트림 키 | 송출 세션 종료 | `/whip`은 OBS로 검증했습니다. 브라우저를 포함한 다른 WHIP 클라이언트도 송출할 수 있지만, 키프레임을 주기적으로 보내는 것은 송출자의 책임입니다. 시청자의 레이어 전환은 새 레이어의 키프레임에서 일어나고, 서버는 그 키프레임을 한 번만 요청합니다. +직접 만드는 페이지에서 시청자 프로토콜을 손으로 구현할 필요는 없습니다. npm의 `@kimsun721/stream-viewer`인 [뷰어 SDK](sdk/README.md)가 입장, 재생, 레이어 전환, 재접속, P2P 중계를 맡고, 플레이어도 함께 제공합니다. + ## 방 상태 ``` @@ -170,7 +173,7 @@ LOAD_METRICS_PORT=9464 cargo test --release --test load -- --ignored --nocapture - [Decisions](docs/decisions/): 설계가 지금 모양인 이유를 남긴 ADR - [Architecture](docs/architecture/overview.md): 컴포넌트, 스레드, 데이터 흐름 -- [Features](docs/feature/): simulcast, P2P relay, 뷰어 SDK 설계 문서 +- [Features](docs/feature/): simulcast, P2P relay 설계 문서 - [Tests](docs/test/README.md): 테스트 구성, 부하 테스트, 기록된 베이스라인 ## 로드맵 diff --git a/README.md b/README.md index f5141dd..16e3f4c 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,15 @@ HTTPS `:8443`, called by clients directly. | Method | Path | Auth | Description | | --- | --- | --- | --- | | `GET` | `/watch?room={id}` | none | A minimal viewer page | +| `GET` | `/stream-viewer.js` | none | The viewer SDK as one file, which `/watch` runs on | | `POST` | `/offer` | none | Viewer joins. SDP offer in, SDP answer out | | `POST` | `/whip` | stream key | Publish. `application/sdp` in and out | | `DELETE` | `/whip/sessions/{id}` | stream key | End the publishing session | `/whip` is tested with OBS. Another WHIP client, a browser included, can publish as well, but sending keyframes on an interval is its job: a viewer changes layer on a keyframe of the new layer, and the server asks for that keyframe once. +A page of your own does not speak the viewer protocol by hand: the [viewer SDK](sdk/README.md), `@kimsun721/stream-viewer` on npm, joins, plays, follows the layers, reconnects and relays, and comes with a player. + ## Room State ``` @@ -170,7 +173,7 @@ LOAD_METRICS_PORT=9464 cargo test --release --test load -- --ignored --nocapture - [Decisions](docs/decisions/): ADRs recording why the design is what it is - [Architecture](docs/architecture/overview.md): components, threads and data flow -- [Features](docs/feature/): design docs for simulcast, the P2P relay and the viewer SDK +- [Features](docs/feature/): design docs for simulcast and the P2P relay - [Tests](docs/test/README.md): suites, the load run, and recorded baselines ## Roadmap diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index af864d5..a845365 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -11,7 +11,8 @@ A WebRTC SFU for low-latency live streaming. One process; persistence and horizo | Media loops | Each owns a share of the rooms, drives str0m for their clients and forwards RTP | | HTTP API | Room CRUD and JSON metrics for an external backend | | Metrics port | Prometheus exposition, without a key. Off unless configured | -| HTTPS API | SDP offers from viewers, WHIP from publishers, and a minimal viewer page at `/watch` | +| HTTPS API | SDP offers from viewers, WHIP from publishers, a minimal viewer page at `/watch`, and the viewer SDK it runs on | +| Viewer SDK | `sdk/`, published to npm. The browser side of the viewer protocol, and a player built on it | | DataChannel | Per-client side band: renegotiation, simulcast layer requests, P2P relay signaling | The external backend is treated as a black box that speaks HTTP. This server makes no assumptions about its stack. diff --git a/docs/feature/p2p-relay.md b/docs/feature/p2p-relay.md index 96c885e..211be80 100644 --- a/docs/feature/p2p-relay.md +++ b/docs/feature/p2p-relay.md @@ -62,7 +62,7 @@ Before choosing the DataChannel, check that str0m's SCTP takes a few Mbps. If it **Rejected: the server as a stand-in leaf.** The server would open a peer link to the candidate as a leaf and attach the real one only after it passed. That makes the server a WebRTC client inside a media loop, decrypts the whole stream for the trial, and still measures only the path to the server. -**When.** Once relays run in browsers. Today only the str0m test clients answer the relay protocol, and `watch.html` does not speak it. +**When.** Browsers can relay through the viewer SDK, which leaves the probe unanswered until this lands. Today only the str0m test clients answer it. ## Notes @@ -70,4 +70,5 @@ Before choosing the DataChannel, check that str0m's SCTP takes a few Mbps. If it - A relay forwards only what it receives, so its leaf watches the relay's layer. - One relay carries one leaf. - No TURN. A pair that cannot connect directly times out and stays on the SFU. -- A browser re-encodes a track it forwards to another peer link, which costs the relay CPU and quality and changes the bitrate it sends. Passing encoded frames through needs the Encoded Transform API. The test clients forward RTP as is, so this has not come up. +- A browser re-encodes a track it forwards to another peer link, which costs the relay CPU and quality and changes the bitrate it sends. The viewer SDK holds the encoder near the bitrate the relay receives, with a start and a floor, because a browser starts low and the rate is judged after `min_p2p_connection_age_secs`. Passing encoded frames through needs the Encoded Transform API and a keyframe from the publisher at link up, which the server has no message for. The test clients forward RTP as is. +- A relay that changes layer while paired changes what its encoder is held to, and its rate lags behind. Seen with a headless Chrome publisher, whose layers come and go: the server demoted pairs while the relay moved between `h` and `m`. diff --git a/docs/feature/viewer-sdk.md b/docs/feature/viewer-sdk.md deleted file mode 100644 index 4dbc0c1..0000000 --- a/docs/feature/viewer-sdk.md +++ /dev/null @@ -1,33 +0,0 @@ -# Viewer SDK - -A browser package that plays a room, so an integrator does not reimplement the viewer protocol. Not started. - -## Why - -The viewer protocol is this server's own: an SDP offer to join, renegotiation over the DataChannel, the layer messages, and periodic perf reports. `static/watch.html` speaks all of it, but only inside that page. - -## Scope - -- Join a room and hand back a stream to attach to a `