diff --git a/.gitignore b/.gitignore index 987eb6b..0710f85 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bin/ # Web web/node_modules/ web/dist/ +web/dist-lib/ # TLS *.pem diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0490d85..f69ce12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,7 @@ All changes must pass `make check`, which runs: - `gofmt -s` — Go code formatting with simplification - `go vet` — Go static analysis +- `staticcheck` — Extended static analysis ([install](https://staticcheck.dev/)) - `go test -race` — All tests with the race detector enabled - `npx tsc --noEmit` — TypeScript type checking (strict mode) @@ -47,13 +48,13 @@ All changes must pass `make check`, which runs: make test # Run tests for a specific package -go test -v -race ./internal/demux/ +go test -v -race ./demux/ # Run fuzz tests (default 10s, adjust as needed) -go test -fuzz=FuzzParseAnnexB -fuzztime=30s ./internal/demux/ +go test -fuzz=FuzzParseAnnexB -fuzztime=30s ./demux/ # Run benchmarks -go test -bench=. -benchmem ./internal/distribution/ +go test -bench=. -benchmem ./distribution/ ``` ## Project Structure diff --git a/Makefile b/Makefile index ecd2671..a514fc7 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,6 @@ check: fmt vet go mod tidy staticcheck ./... go test -race -cover ./... - govulncheck ./... cd web && npx tsc --noEmit web-install: diff --git a/README.md b/README.md index 452a00e..bcf9cbc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,61 @@ ffmpeg -re -i input.ts -c copy -f mpegts srt://localhost:6000?streamid=mystream Then open `https://localhost:4444/?stream=mystream`. +## Examples + +Prism's packages are designed to be used as a library. The `examples/` directory contains standalone programs showing how to embed Prism in your own application, and `web/examples/` shows how to use the player in a browser. + +### Minimal server (Go) + +A stripped-down version of `cmd/prism` — SRT ingest, demux, and WebTransport delivery in ~60 lines: + +```bash +go run ./examples/minimal-server +ffmpeg -re -i input.ts -c copy -f mpegts srt://localhost:6000?streamid=demo +open https://localhost:4443 +``` + +### Custom ingest (Go) + +Feed any MPEG-TS `io.Reader` directly into the pipeline — no SRT required: + +```bash +go run ./examples/custom-ingest input.ts +open https://localhost:4443/?stream=file +``` + +### Standalone web player (TypeScript) + +Embed `PrismPlayer` in your own page using the built library bundle: + +```bash +cd web && npm run demo:lib # builds dist-lib/prism.js + starts Vite dev server +# (start the Prism server in another terminal: make run) +open http://localhost:5173/examples/standalone.html?stream=demo +``` + +The HTML is ~80 lines and shows the full API: create a player, connect to a stream key, handle lifecycle callbacks. See [`web/examples/standalone.html`](web/examples/standalone.html). + +### Building the web player library + +To use `PrismPlayer` in your own project: + +```bash +cd web && npm run build:lib # outputs web/dist-lib/prism.js +``` + +```js +import { PrismPlayer } from "./dist-lib/prism.js"; + +const player = new PrismPlayer(document.getElementById("container"), { + onStreamConnected(key) { console.log("connected:", key); }, + onStreamDisconnected(key) { console.log("disconnected:", key); }, +}); +player.connect("demo"); +``` + +The library also exports `MoQTransport`, `MoQMultiviewTransport`, `MetricsStore`, and related types for advanced use cases. + ## Architecture ``` @@ -64,18 +119,18 @@ Single Go binary, vanilla TypeScript frontend: | Package | Purpose | |---|---| | `cmd/prism/` | Entry point, wires everything together | -| `internal/ingest/` | Stream ingest registry | -| `internal/ingest/srt/` | SRT server (push) and caller (pull) | -| `internal/demux/` | MPEG-TS demuxer, H.264/H.265/AAC parsers | -| `internal/media/` | Frame types (`VideoFrame`, `AudioFrame`) | -| `internal/distribution/` | WebTransport server, MoQ sessions, relay fan-out | -| `internal/moq/` | MoQ Transport wire protocol codec | -| `internal/pipeline/` | Demux-to-distribution orchestration | -| `internal/stream/` | Stream lifecycle management | -| `internal/mpegts/` | Low-level MPEG-TS packet/PES/PSI parsing | -| `internal/scte35/` | SCTE-35 splice info encoding/decoding | -| `internal/certs/` | Self-signed ECDSA certificate generation | -| `internal/webtransport/` | WebTransport server on quic-go/HTTP3 | +| `ingest/` | Stream ingest registry | +| `ingest/srt/` | SRT server (push) and caller (pull) | +| `demux/` | MPEG-TS demuxer, H.264/H.265/AAC parsers | +| `media/` | Frame types (`VideoFrame`, `AudioFrame`) | +| `distribution/` | WebTransport server, MoQ sessions, relay fan-out | +| `moq/` | MoQ Transport wire protocol codec | +| `pipeline/` | Demux-to-distribution orchestration | +| `stream/` | Stream lifecycle management | +| `mpegts/` | Low-level MPEG-TS packet/PES/PSI parsing | +| `scte35/` | SCTE-35 splice info encoding/decoding | +| `certs/` | Self-signed ECDSA certificate generation | +| `webtransport/` | WebTransport server on quic-go/HTTP3 | | `web/` | Vanilla TypeScript viewer (Vite, WebTransport, WebCodecs) | ## Configuration @@ -131,11 +186,10 @@ make demo make demo-full ``` -`make check` requires [staticcheck](https://staticcheck.dev/) and [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck): +`make check` requires [staticcheck](https://staticcheck.dev/): ```bash go install honnef.co/go/tools/cmd/staticcheck@latest -go install golang.org/x/vuln/cmd/govulncheck@latest ``` ## Security Considerations diff --git a/internal/certs/selfsigned.go b/certs/selfsigned.go similarity index 100% rename from internal/certs/selfsigned.go rename to certs/selfsigned.go diff --git a/internal/certs/selfsigned_test.go b/certs/selfsigned_test.go similarity index 100% rename from internal/certs/selfsigned_test.go rename to certs/selfsigned_test.go diff --git a/cmd/prism/main.go b/cmd/prism/main.go index 55df531..e570090 100644 --- a/cmd/prism/main.go +++ b/cmd/prism/main.go @@ -16,12 +16,12 @@ import ( "golang.org/x/sync/errgroup" - "github.com/zsiec/prism/internal/certs" - "github.com/zsiec/prism/internal/distribution" - "github.com/zsiec/prism/internal/ingest" - srtingest "github.com/zsiec/prism/internal/ingest/srt" - "github.com/zsiec/prism/internal/pipeline" - "github.com/zsiec/prism/internal/stream" + "github.com/zsiec/prism/certs" + "github.com/zsiec/prism/distribution" + "github.com/zsiec/prism/ingest" + srtingest "github.com/zsiec/prism/ingest/srt" + "github.com/zsiec/prism/pipeline" + "github.com/zsiec/prism/stream" ) var version = "dev" diff --git a/internal/demux/aac.go b/demux/aac.go similarity index 100% rename from internal/demux/aac.go rename to demux/aac.go diff --git a/internal/demux/aac_bench_test.go b/demux/aac_bench_test.go similarity index 100% rename from internal/demux/aac_bench_test.go rename to demux/aac_bench_test.go diff --git a/internal/demux/aac_fuzz_test.go b/demux/aac_fuzz_test.go similarity index 100% rename from internal/demux/aac_fuzz_test.go rename to demux/aac_fuzz_test.go diff --git a/internal/demux/aac_test.go b/demux/aac_test.go similarity index 100% rename from internal/demux/aac_test.go rename to demux/aac_test.go diff --git a/internal/demux/caption_harness_test.go b/demux/caption_harness_test.go similarity index 98% rename from internal/demux/caption_harness_test.go rename to demux/caption_harness_test.go index 3a293f1..7d1d690 100644 --- a/internal/demux/caption_harness_test.go +++ b/demux/caption_harness_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/mpegts" + "github.com/zsiec/prism/mpegts" ) func TestCaptionHarness(t *testing.T) { diff --git a/internal/demux/diag_test.go b/demux/diag_test.go similarity index 98% rename from internal/demux/diag_test.go rename to demux/diag_test.go index e93c04f..81c34c6 100644 --- a/internal/demux/diag_test.go +++ b/demux/diag_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/mpegts" + "github.com/zsiec/prism/mpegts" ) func TestDiag_DecodedCaptions(t *testing.T) { diff --git a/internal/demux/doc.go b/demux/doc.go similarity index 100% rename from internal/demux/doc.go rename to demux/doc.go diff --git a/internal/demux/h264.go b/demux/h264.go similarity index 100% rename from internal/demux/h264.go rename to demux/h264.go diff --git a/internal/demux/h264_bench_test.go b/demux/h264_bench_test.go similarity index 100% rename from internal/demux/h264_bench_test.go rename to demux/h264_bench_test.go diff --git a/internal/demux/h264_fuzz_test.go b/demux/h264_fuzz_test.go similarity index 100% rename from internal/demux/h264_fuzz_test.go rename to demux/h264_fuzz_test.go diff --git a/internal/demux/h264_test.go b/demux/h264_test.go similarity index 100% rename from internal/demux/h264_test.go rename to demux/h264_test.go diff --git a/internal/demux/h265.go b/demux/h265.go similarity index 100% rename from internal/demux/h265.go rename to demux/h265.go diff --git a/internal/demux/h265_test.go b/demux/h265_test.go similarity index 100% rename from internal/demux/h265_test.go rename to demux/h265_test.go diff --git a/internal/demux/mpegts.go b/demux/mpegts.go similarity index 99% rename from internal/demux/mpegts.go rename to demux/mpegts.go index a6d4b16..d919dc5 100644 --- a/internal/demux/mpegts.go +++ b/demux/mpegts.go @@ -8,9 +8,9 @@ import ( "time" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/mpegts" - "github.com/zsiec/prism/internal/scte35" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/mpegts" + "github.com/zsiec/prism/scte35" ) const ( diff --git a/internal/distribution/moq_catalog.go b/distribution/moq_catalog.go similarity index 98% rename from internal/distribution/moq_catalog.go rename to distribution/moq_catalog.go index eb071d0..8358e0c 100644 --- a/internal/distribution/moq_catalog.go +++ b/distribution/moq_catalog.go @@ -7,7 +7,7 @@ import ( "fmt" "github.com/quic-go/quic-go/quicvarint" - "github.com/zsiec/prism/internal/webtransport" + "github.com/zsiec/prism/webtransport" ) // moqCatalog is the top-level catalog structure per draft-ietf-moq-catalogformat-01. diff --git a/internal/distribution/moq_catalog_test.go b/distribution/moq_catalog_test.go similarity index 100% rename from internal/distribution/moq_catalog_test.go rename to distribution/moq_catalog_test.go diff --git a/internal/distribution/moq_session.go b/distribution/moq_session.go similarity index 99% rename from internal/distribution/moq_session.go rename to distribution/moq_session.go index c8cfdb5..4dd4213 100644 --- a/internal/distribution/moq_session.go +++ b/distribution/moq_session.go @@ -13,9 +13,9 @@ import ( "time" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/moq" - "github.com/zsiec/prism/internal/webtransport" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/moq" + "github.com/zsiec/prism/webtransport" ) // moqTrackSub holds state for a single track subscription within a MoQ session. diff --git a/internal/distribution/moq_session_test.go b/distribution/moq_session_test.go similarity index 99% rename from internal/distribution/moq_session_test.go rename to distribution/moq_session_test.go index eed4ade..94278e2 100644 --- a/internal/distribution/moq_session_test.go +++ b/distribution/moq_session_test.go @@ -11,9 +11,9 @@ import ( "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/quicvarint" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/moq" - "github.com/zsiec/prism/internal/webtransport" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/moq" + "github.com/zsiec/prism/webtransport" ) // buildClientSetupPayload builds a CLIENT_SETUP payload for testing. diff --git a/internal/distribution/moq_writer.go b/distribution/moq_writer.go similarity index 98% rename from internal/distribution/moq_writer.go rename to distribution/moq_writer.go index be96ac1..5120cbf 100644 --- a/internal/distribution/moq_writer.go +++ b/distribution/moq_writer.go @@ -4,8 +4,8 @@ import ( "io" "github.com/quic-go/quic-go/quicvarint" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/moq" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/moq" ) // Compile-time interface check. diff --git a/internal/distribution/moq_writer_test.go b/distribution/moq_writer_test.go similarity index 99% rename from internal/distribution/moq_writer_test.go rename to distribution/moq_writer_test.go index c707e8e..4bbff73 100644 --- a/internal/distribution/moq_writer_test.go +++ b/distribution/moq_writer_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/quic-go/quic-go/quicvarint" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/media" ) func TestMoQWriterSubgroupHeader(t *testing.T) { diff --git a/internal/distribution/protocol.go b/distribution/protocol.go similarity index 96% rename from internal/distribution/protocol.go rename to distribution/protocol.go index fe06a5a..106db06 100644 --- a/internal/distribution/protocol.go +++ b/distribution/protocol.go @@ -1,13 +1,13 @@ // Package distribution implements the WebTransport-based viewer delivery // layer, including the fan-out relay, MoQ session management, and the // HTTP/QUIC server that ties them together. The low-level MoQ wire protocol -// codec lives in [github.com/zsiec/prism/internal/moq]. +// codec lives in [github.com/zsiec/prism/moq]. package distribution import ( "io" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/media" ) // Track ID constants used to identify media types in the MoQ catalog diff --git a/internal/distribution/relay.go b/distribution/relay.go similarity index 99% rename from internal/distribution/relay.go rename to distribution/relay.go index d3ab8e3..57d9284 100644 --- a/internal/distribution/relay.go +++ b/distribution/relay.go @@ -6,8 +6,8 @@ import ( "sync" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/moq" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/moq" ) // Viewer is the interface that a viewer session (single or mux) must implement diff --git a/internal/distribution/relay_test.go b/distribution/relay_test.go similarity index 99% rename from internal/distribution/relay_test.go rename to distribution/relay_test.go index bffd54d..c4d89d0 100644 --- a/internal/distribution/relay_test.go +++ b/distribution/relay_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/media" ) // mockViewer implements the Viewer interface for testing. diff --git a/internal/distribution/server.go b/distribution/server.go similarity index 99% rename from internal/distribution/server.go rename to distribution/server.go index 23b47e8..b5d6b38 100644 --- a/internal/distribution/server.go +++ b/distribution/server.go @@ -13,9 +13,9 @@ import ( "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" - "github.com/zsiec/prism/internal/certs" - "github.com/zsiec/prism/internal/moq" - "github.com/zsiec/prism/internal/webtransport" + "github.com/zsiec/prism/certs" + "github.com/zsiec/prism/moq" + "github.com/zsiec/prism/webtransport" ) // StatsProvider is implemented by Pipeline to supply stream statistics diff --git a/internal/distribution/server_test.go b/distribution/server_test.go similarity index 99% rename from internal/distribution/server_test.go rename to distribution/server_test.go index 947f6ad..ab7b4f8 100644 --- a/internal/distribution/server_test.go +++ b/distribution/server_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/zsiec/prism/internal/certs" + "github.com/zsiec/prism/certs" ) func newTestServer(t *testing.T) *Server { diff --git a/internal/distribution/session_helpers.go b/distribution/session_helpers.go similarity index 95% rename from internal/distribution/session_helpers.go rename to distribution/session_helpers.go index 275ff2f..0c4def1 100644 --- a/internal/distribution/session_helpers.go +++ b/distribution/session_helpers.go @@ -3,7 +3,7 @@ package distribution import ( "sync/atomic" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/media" ) // trySendVideo implements the damaged-group-aware video send logic shared diff --git a/internal/distribution/session_helpers_test.go b/distribution/session_helpers_test.go similarity index 98% rename from internal/distribution/session_helpers_test.go rename to distribution/session_helpers_test.go index 1db31c9..125bfee 100644 --- a/internal/distribution/session_helpers_test.go +++ b/distribution/session_helpers_test.go @@ -4,7 +4,7 @@ import ( "sync/atomic" "testing" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/media" ) func TestTrySendVideoKeyframeResetsGroup(t *testing.T) { diff --git a/internal/distribution/streamstats.go b/distribution/streamstats.go similarity index 99% rename from internal/distribution/streamstats.go rename to distribution/streamstats.go index 5557369..779c17c 100644 --- a/internal/distribution/streamstats.go +++ b/distribution/streamstats.go @@ -5,7 +5,7 @@ import ( "sync/atomic" "time" - "github.com/zsiec/prism/internal/demux" + "github.com/zsiec/prism/demux" ) // Compile-time interface check. diff --git a/internal/distribution/streamstats_test.go b/distribution/streamstats_test.go similarity index 100% rename from internal/distribution/streamstats_test.go rename to distribution/streamstats_test.go diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..680b10b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,49 @@ +# Examples + +Standalone programs demonstrating how to use Prism's packages as a library. + +## Go + +### Minimal Server + +[`minimal-server/main.go`](minimal-server/main.go) — A complete SRT-to-WebTransport server in ~60 lines. Same architecture as `cmd/prism` but stripped to the essentials. + +```bash +go run ./examples/minimal-server +ffmpeg -re -i input.ts -c copy -f mpegts srt://localhost:6000?streamid=demo +open https://localhost:4443 +``` + +### Custom Ingest + +[`custom-ingest/main.go`](custom-ingest/main.go) — Feed any MPEG-TS `io.Reader` directly into the pipeline, bypassing SRT entirely. + +```bash +go run ./examples/custom-ingest input.ts +open https://localhost:4443/?stream=file +``` + +## Web + +### Standalone Player + +[`../web/examples/standalone.html`](../web/examples/standalone.html) — Embed `PrismPlayer` in a plain HTML page using the built library bundle. + +```bash +cd web && npm run demo:lib # builds dist-lib/prism.js + starts dev server +# (start the Prism server in another terminal: make run) +open http://localhost:5173/examples/standalone.html?stream=demo +``` + +## Key Packages + +These are the main packages you'll use when embedding Prism: + +| Package | Description | +|---|---| +| `certs` | Generate self-signed ECDSA certificates for WebTransport | +| `distribution` | WebTransport server, MoQ sessions, relay fan-out | +| `ingest` | Stream ingest registry (pairs stream keys with pipelines) | +| `ingest/srt` | SRT push server and pull caller | +| `pipeline` | Connects a demuxer to a distribution relay | +| `stream` | Stream lifecycle management | diff --git a/examples/custom-ingest/main.go b/examples/custom-ingest/main.go new file mode 100644 index 0000000..de5c94b --- /dev/null +++ b/examples/custom-ingest/main.go @@ -0,0 +1,71 @@ +// Custom ingest: feed an MPEG-TS file directly without SRT. +// This demonstrates that the ingest layer is optional — any io.Reader +// producing MPEG-TS data can drive the pipeline. +// +// Usage: +// +// go run ./examples/custom-ingest input.ts +// open https://localhost:4443/?stream=file +package main + +import ( + "context" + "log" + "log/slog" + "os" + "os/signal" + "time" + + "github.com/zsiec/prism/certs" + "github.com/zsiec/prism/distribution" + "github.com/zsiec/prism/pipeline" +) + +func main() { + if len(os.Args) < 2 { + log.Fatal("usage: custom-ingest ") + } + + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))) + + f, err := os.Open(os.Args[1]) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + cert, err := certs.Generate(14 * 24 * time.Hour) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + distSrv, err := distribution.NewServer(distribution.ServerConfig{ + Addr: ":4443", + WebDir: "web/dist", + Cert: cert, + }) + if err != nil { + log.Fatal(err) + } + + relay := distSrv.RegisterStream("file") + p := pipeline.New("file", f, relay) + p.SetProtocol("File") + distSrv.SetPipeline("file", p) + + go func() { + if err := p.Run(ctx); err != nil { + slog.Error("pipeline finished", "error", err) + } + slog.Info("file playback complete") + }() + + slog.Info("serving file stream", "webtransport", ":4443", "cert_hash", cert.FingerprintBase64()) + + if err := distSrv.Start(ctx); err != nil { + slog.Error("distribution server error", "error", err) + } +} diff --git a/examples/minimal-server/main.go b/examples/minimal-server/main.go new file mode 100644 index 0000000..c355465 --- /dev/null +++ b/examples/minimal-server/main.go @@ -0,0 +1,86 @@ +// Minimal Prism server: SRT ingest → demux → pipeline → relay → WebTransport. +// This demonstrates wiring the core packages together — the same pattern +// used by cmd/prism but stripped to the essentials. +// +// Usage: +// +// go run ./examples/minimal-server +// ffmpeg -re -i input.ts -c copy -f mpegts srt://localhost:6000?streamid=demo +// open https://localhost:4443 +package main + +import ( + "context" + "io" + "log" + "log/slog" + "os" + "os/signal" + "time" + + "github.com/zsiec/prism/certs" + "github.com/zsiec/prism/distribution" + "github.com/zsiec/prism/ingest" + srtingest "github.com/zsiec/prism/ingest/srt" + "github.com/zsiec/prism/pipeline" + "github.com/zsiec/prism/stream" +) + +func main() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))) + + cert, err := certs.Generate(14 * 24 * time.Hour) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + mgr := stream.NewManager(nil) + + var distSrv *distribution.Server + + registry := ingest.NewRegistry(func(key string, input io.Reader, _ ingest.InputFormat) { + if _, created := mgr.Create(key); !created { + return + } + defer func() { + distSrv.UnregisterStream(key) + mgr.Remove(key) + }() + + relay := distSrv.RegisterStream(key) + p := pipeline.New(key, input, relay) + p.SetProtocol("SRT") + distSrv.SetPipeline(key, p) + + if err := p.Run(ctx); err != nil { + slog.Error("pipeline error", "stream", key, "error", err) + } + }) + + distSrv, err = distribution.NewServer(distribution.ServerConfig{ + Addr: ":4443", + WebDir: "web/dist", + Cert: cert, + }) + if err != nil { + log.Fatal(err) + } + + srtSrv := srtingest.NewServer(":6000", registry, nil) + + go func() { + if err := srtSrv.Start(ctx); err != nil { + slog.Error("SRT server error", "error", err) + cancel() + } + }() + + slog.Info("prism minimal server", "srt", ":6000", "webtransport", ":4443", "cert_hash", cert.FingerprintBase64()) + + if err := distSrv.Start(ctx); err != nil { + slog.Error("distribution server error", "error", err) + } +} diff --git a/internal/ingest/ingest.go b/ingest/ingest.go similarity index 100% rename from internal/ingest/ingest.go rename to ingest/ingest.go diff --git a/internal/ingest/ingest_test.go b/ingest/ingest_test.go similarity index 100% rename from internal/ingest/ingest_test.go rename to ingest/ingest_test.go diff --git a/internal/ingest/srt/caller.go b/ingest/srt/caller.go similarity index 99% rename from internal/ingest/srt/caller.go rename to ingest/srt/caller.go index 47debc8..770c0ad 100644 --- a/internal/ingest/srt/caller.go +++ b/ingest/srt/caller.go @@ -11,7 +11,7 @@ import ( srtgo "github.com/zsiec/srtgo" - "github.com/zsiec/prism/internal/ingest" + "github.com/zsiec/prism/ingest" ) // PullRequest describes a remote SRT source to pull from. diff --git a/internal/ingest/srt/doc.go b/ingest/srt/doc.go similarity index 100% rename from internal/ingest/srt/doc.go rename to ingest/srt/doc.go diff --git a/internal/ingest/srt/server.go b/ingest/srt/server.go similarity index 98% rename from internal/ingest/srt/server.go rename to ingest/srt/server.go index b29ead8..9ba9790 100644 --- a/internal/ingest/srt/server.go +++ b/ingest/srt/server.go @@ -10,7 +10,7 @@ import ( srtgo "github.com/zsiec/srtgo" - "github.com/zsiec/prism/internal/ingest" + "github.com/zsiec/prism/ingest" ) // srtReadBufferSize is the read buffer for SRT socket reads. diff --git a/internal/ingest/srt/server_test.go b/ingest/srt/server_test.go similarity index 100% rename from internal/ingest/srt/server_test.go rename to ingest/srt/server_test.go diff --git a/internal/media/frame.go b/media/frame.go similarity index 100% rename from internal/media/frame.go rename to media/frame.go diff --git a/internal/moq/control.go b/moq/control.go similarity index 100% rename from internal/moq/control.go rename to moq/control.go diff --git a/internal/moq/control_test.go b/moq/control_test.go similarity index 100% rename from internal/moq/control_test.go rename to moq/control_test.go diff --git a/internal/moq/doc.go b/moq/doc.go similarity index 84% rename from internal/moq/doc.go rename to moq/doc.go index 42dcf39..593d184 100644 --- a/internal/moq/doc.go +++ b/moq/doc.go @@ -4,5 +4,5 @@ // decoder configuration records), and typed error definitions. // // This package contains no session or relay logic; those higher-level -// concerns live in [github.com/zsiec/prism/internal/distribution]. +// concerns live in [github.com/zsiec/prism/distribution]. package moq diff --git a/internal/moq/errors.go b/moq/errors.go similarity index 100% rename from internal/moq/errors.go rename to moq/errors.go diff --git a/internal/moq/format.go b/moq/format.go similarity index 99% rename from internal/moq/format.go rename to moq/format.go index c360855..2a55d1c 100644 --- a/internal/moq/format.go +++ b/moq/format.go @@ -3,7 +3,7 @@ package moq import ( "encoding/binary" - "github.com/zsiec/prism/internal/demux" + "github.com/zsiec/prism/demux" ) // AnnexBToAVC1 converts Annex B NALUs (4-byte start code prefixed) to AVC1 diff --git a/internal/moq/format_bench_test.go b/moq/format_bench_test.go similarity index 100% rename from internal/moq/format_bench_test.go rename to moq/format_bench_test.go diff --git a/internal/moq/format_test.go b/moq/format_test.go similarity index 100% rename from internal/moq/format_test.go rename to moq/format_test.go diff --git a/internal/mpegts/accumulator.go b/mpegts/accumulator.go similarity index 100% rename from internal/mpegts/accumulator.go rename to mpegts/accumulator.go diff --git a/internal/mpegts/accumulator_test.go b/mpegts/accumulator_test.go similarity index 100% rename from internal/mpegts/accumulator_test.go rename to mpegts/accumulator_test.go diff --git a/internal/mpegts/crc32.go b/mpegts/crc32.go similarity index 100% rename from internal/mpegts/crc32.go rename to mpegts/crc32.go diff --git a/internal/mpegts/debug_test.go b/mpegts/debug_test.go similarity index 100% rename from internal/mpegts/debug_test.go rename to mpegts/debug_test.go diff --git a/internal/mpegts/demuxer.go b/mpegts/demuxer.go similarity index 100% rename from internal/mpegts/demuxer.go rename to mpegts/demuxer.go diff --git a/internal/mpegts/demuxer_test.go b/mpegts/demuxer_test.go similarity index 100% rename from internal/mpegts/demuxer_test.go rename to mpegts/demuxer_test.go diff --git a/internal/mpegts/packet.go b/mpegts/packet.go similarity index 100% rename from internal/mpegts/packet.go rename to mpegts/packet.go diff --git a/internal/mpegts/packet_fuzz_test.go b/mpegts/packet_fuzz_test.go similarity index 100% rename from internal/mpegts/packet_fuzz_test.go rename to mpegts/packet_fuzz_test.go diff --git a/internal/mpegts/packet_test.go b/mpegts/packet_test.go similarity index 100% rename from internal/mpegts/packet_test.go rename to mpegts/packet_test.go diff --git a/internal/mpegts/pes.go b/mpegts/pes.go similarity index 100% rename from internal/mpegts/pes.go rename to mpegts/pes.go diff --git a/internal/mpegts/pes_test.go b/mpegts/pes_test.go similarity index 100% rename from internal/mpegts/pes_test.go rename to mpegts/pes_test.go diff --git a/internal/mpegts/psi.go b/mpegts/psi.go similarity index 100% rename from internal/mpegts/psi.go rename to mpegts/psi.go diff --git a/internal/mpegts/psi_test.go b/mpegts/psi_test.go similarity index 100% rename from internal/mpegts/psi_test.go rename to mpegts/psi_test.go diff --git a/internal/mpegts/types.go b/mpegts/types.go similarity index 100% rename from internal/mpegts/types.go rename to mpegts/types.go diff --git a/internal/pipeline/pipeline.go b/pipeline/pipeline.go similarity index 97% rename from internal/pipeline/pipeline.go rename to pipeline/pipeline.go index 4013352..3dd88ba 100644 --- a/internal/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -11,10 +11,10 @@ import ( "time" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/demux" - "github.com/zsiec/prism/internal/distribution" - "github.com/zsiec/prism/internal/media" - "github.com/zsiec/prism/internal/moq" + "github.com/zsiec/prism/demux" + "github.com/zsiec/prism/distribution" + "github.com/zsiec/prism/media" + "github.com/zsiec/prism/moq" ) // Broadcaster is the subset of distribution.Relay that the pipeline uses diff --git a/internal/pipeline/pipeline_integration_test.go b/pipeline/pipeline_integration_test.go similarity index 98% rename from internal/pipeline/pipeline_integration_test.go rename to pipeline/pipeline_integration_test.go index 4992b58..f3db75d 100644 --- a/internal/pipeline/pipeline_integration_test.go +++ b/pipeline/pipeline_integration_test.go @@ -9,8 +9,8 @@ import ( "time" "github.com/zsiec/ccx" - "github.com/zsiec/prism/internal/distribution" - "github.com/zsiec/prism/internal/media" + "github.com/zsiec/prism/distribution" + "github.com/zsiec/prism/media" ) // testViewer implements distribution.Viewer to collect frames from the relay. diff --git a/internal/pipeline/pipeline_test.go b/pipeline/pipeline_test.go similarity index 96% rename from internal/pipeline/pipeline_test.go rename to pipeline/pipeline_test.go index aed39ef..719e33f 100644 --- a/internal/pipeline/pipeline_test.go +++ b/pipeline/pipeline_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/zsiec/prism/internal/distribution" + "github.com/zsiec/prism/distribution" ) func TestNew(t *testing.T) { diff --git a/internal/scte35/bits.go b/scte35/bits.go similarity index 100% rename from internal/scte35/bits.go rename to scte35/bits.go diff --git a/internal/scte35/bits_test.go b/scte35/bits_test.go similarity index 100% rename from internal/scte35/bits_test.go rename to scte35/bits_test.go diff --git a/internal/scte35/crc.go b/scte35/crc.go similarity index 100% rename from internal/scte35/crc.go rename to scte35/crc.go diff --git a/internal/scte35/crc_test.go b/scte35/crc_test.go similarity index 100% rename from internal/scte35/crc_test.go rename to scte35/crc_test.go diff --git a/internal/scte35/descriptor.go b/scte35/descriptor.go similarity index 100% rename from internal/scte35/descriptor.go rename to scte35/descriptor.go diff --git a/internal/scte35/scte35.go b/scte35/scte35.go similarity index 100% rename from internal/scte35/scte35.go rename to scte35/scte35.go diff --git a/internal/scte35/scte35_bench_test.go b/scte35/scte35_bench_test.go similarity index 100% rename from internal/scte35/scte35_bench_test.go rename to scte35/scte35_bench_test.go diff --git a/internal/scte35/scte35_fuzz_test.go b/scte35/scte35_fuzz_test.go similarity index 100% rename from internal/scte35/scte35_fuzz_test.go rename to scte35/scte35_fuzz_test.go diff --git a/internal/scte35/scte35_test.go b/scte35/scte35_test.go similarity index 100% rename from internal/scte35/scte35_test.go rename to scte35/scte35_test.go diff --git a/internal/scte35/splice_insert.go b/scte35/splice_insert.go similarity index 100% rename from internal/scte35/splice_insert.go rename to scte35/splice_insert.go diff --git a/internal/scte35/splice_null.go b/scte35/splice_null.go similarity index 100% rename from internal/scte35/splice_null.go rename to scte35/splice_null.go diff --git a/internal/scte35/time_signal.go b/scte35/time_signal.go similarity index 100% rename from internal/scte35/time_signal.go rename to scte35/time_signal.go diff --git a/internal/stream/manager.go b/stream/manager.go similarity index 100% rename from internal/stream/manager.go rename to stream/manager.go diff --git a/internal/stream/manager_test.go b/stream/manager_test.go similarity index 100% rename from internal/stream/manager_test.go rename to stream/manager_test.go diff --git a/test/tools/inject-scte35/main.go b/test/tools/inject-scte35/main.go index b979044..07f6bb5 100644 --- a/test/tools/inject-scte35/main.go +++ b/test/tools/inject-scte35/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/zsiec/prism/internal/scte35" + "github.com/zsiec/prism/scte35" ) const tsPacketSize = 188 diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..5e73ad5 --- /dev/null +++ b/web/README.md @@ -0,0 +1,79 @@ +# Prism Web + +Vanilla TypeScript frontend for Prism. Provides a single-stream player, a 9-up multiview grid, and an embeddable library for use in external applications. + +## Quick Start + +```bash +npm install +npm run dev # Vite dev server on :5173, proxies /api to Prism on :4444 +``` + +Make sure the Prism server is running (`make run` from the repo root). + +- Single stream: `http://localhost:5173/?stream=demo` +- Multiview: `http://localhost:5173/` + +## Scripts + +| Script | Description | +|---|---| +| `npm run dev` | Vite dev server with hot reload and API proxy | +| `npm run build` | Production build to `dist/` | +| `npm run build:lib` | Library build to `dist-lib/prism.js` | +| `npm run demo:lib` | Build library + start dev server (for testing `examples/standalone.html`) | +| `npm run preview` | Preview the production build | + +## Library + +The library build (`npm run build:lib`) produces `dist-lib/prism.js` — an ES module exporting the player and transport classes for embedding in external applications. + +```js +import { PrismPlayer } from "./dist-lib/prism.js"; + +const player = new PrismPlayer(document.getElementById("container"), { + onStreamConnected(key) { console.log("connected:", key); }, + onStreamDisconnected(key) { console.log("disconnected:", key); }, +}); +player.connect("demo"); +``` + +### Exports + +| Export | Description | +|---|---| +| `PrismPlayer` | Single-stream player — creates canvas, audio, captions, and transport internally | +| `MoQTransport` | Low-level MoQ Transport client for a single stream | +| `MoQMultiviewTransport` | Manages N `MoQTransport` instances for multiview | +| `MetricsStore` | Frame-level metrics collection (video, audio, sync, transport, captions) | +| `StreamBuffer` | Buffered stream reader | +| `parseCaptionData` | CEA-608/708 caption parser | + +See [`examples/standalone.html`](examples/standalone.html) for a complete working example. + +## Source Structure + +| File | Purpose | +|---|---| +| `main.ts` | App entry point — routes to single-stream or multiview | +| `lib.ts` | Library entry point — barrel export for `build:lib` | +| `player.ts` | `PrismPlayer` — orchestrates decoding, rendering, and transport for one stream | +| `multiview.ts` | Multiview manager — 9-tile grid with per-tile audio solo | +| `moq-transport.ts` | MoQ Transport client — WebTransport + MoQ control/data parsing | +| `moq-multiview-transport.ts` | Multi-stream MoQ coordinator | +| `video-decoder.ts` | WebCodecs video decoder with worker offload | +| `video-decoder-worker.ts` | Web Worker for `VideoDecoder` | +| `audio-decoder.ts` | WebCodecs audio decoder with AudioWorklet output | +| `renderer.ts` | Canvas 2D / WebGPU video renderer | +| `captions.ts` | CEA-608/708 caption overlay renderer | +| `metrics-store.ts` | Per-frame metrics collection and health scoring | +| `hud.ts` | Heads-up display badges (codec, resolution, bitrate, etc.) | +| `inspector.ts` | Stream inspector panel with real-time charts | +| `protocol.ts` | Wire protocol types and caption parsing | + +## Build Configuration + +Two Vite configs serve different purposes: + +- **`vite.config.ts`** — Main app build. Sets `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` headers required by `SharedArrayBuffer` (used by WebCodecs workers). Proxies `/api` to the Prism server during development. +- **`vite.lib.config.ts`** — Library build. Produces a single ES module (`dist-lib/prism.js`) with worker chunks in `dist-lib/assets/`. diff --git a/web/examples/standalone.html b/web/examples/standalone.html new file mode 100644 index 0000000..6546d79 --- /dev/null +++ b/web/examples/standalone.html @@ -0,0 +1,102 @@ + + + + + + +Prism Player — Standalone Demo + + + + +

Prism Player

+ +
+ + +
+
Not connected
+
+ + + + + diff --git a/web/package.json b/web/package.json index a59244e..526e526 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,9 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "build:lib": "tsc && vite build --config vite.lib.config.ts", + "preview": "vite preview", + "demo:lib": "npm run build:lib && vite dev" }, "devDependencies": { "@webgpu/types": "^0.1.69", diff --git a/web/src/lib.ts b/web/src/lib.ts new file mode 100644 index 0000000..0be8668 --- /dev/null +++ b/web/src/lib.ts @@ -0,0 +1,57 @@ +// Headless library entry point for embedding Prism's player and transport +// in external applications. Build with: npx vite build --config vite.lib.config.ts + +// Player +export { PrismPlayer } from "./player"; +export type { TilePerfStats } from "./player"; + +// MoQ Transport +export { MoQTransport } from "./moq-transport"; +export type { MoQTransportCallbacks } from "./moq-transport"; + +// MoQ Multiview Transport +export { MoQMultiviewTransport } from "./moq-multiview-transport"; +export type { MoQMultiviewCallbacks } from "./moq-multiview-transport"; + +// Metrics +export { MetricsStore } from "./metrics-store"; +export type { + FrameEvent, + VideoMetrics, + AudioMetrics, + SyncMetrics, + TransportMetrics, + CaptionMetrics, + HealthStatus, + StreamInfo, + ErrorCounters, +} from "./metrics-store"; + +// Transport types +export type { + TrackInfo, + ServerAudioTrackStats, + ServerViewerStats, + ServerSCTE35Event, + ServerStats, +} from "./transport"; + +// Protocol types +export { parseCaptionData } from "./protocol"; +export type { + CaptionSpan, + CaptionRow, + CaptionRegion, + CaptionData, + ProtocolDiagnostics, +} from "./protocol"; + +// Stream buffer +export { StreamBuffer } from "./stream-buffer"; + +// Multiview types +export type { + MuxStreamEntry, + MuxStreamCallbacks, + MuxViewerStats, +} from "./multiview-types"; diff --git a/web/vite.lib.config.ts b/web/vite.lib.config.ts new file mode 100644 index 0000000..cb644c9 --- /dev/null +++ b/web/vite.lib.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; + +export default defineConfig({ + build: { + target: "ES2022", + outDir: "dist-lib", + lib: { + entry: resolve(__dirname, "src/lib.ts"), + formats: ["es"], + fileName: "prism", + }, + rollupOptions: { + output: { + entryFileNames: "prism.js", + }, + }, + }, +}); diff --git a/internal/webtransport/doc.go b/webtransport/doc.go similarity index 100% rename from internal/webtransport/doc.go rename to webtransport/doc.go diff --git a/internal/webtransport/errors.go b/webtransport/errors.go similarity index 100% rename from internal/webtransport/errors.go rename to webtransport/errors.go diff --git a/internal/webtransport/protocol.go b/webtransport/protocol.go similarity index 100% rename from internal/webtransport/protocol.go rename to webtransport/protocol.go diff --git a/internal/webtransport/server.go b/webtransport/server.go similarity index 100% rename from internal/webtransport/server.go rename to webtransport/server.go diff --git a/internal/webtransport/session.go b/webtransport/session.go similarity index 100% rename from internal/webtransport/session.go rename to webtransport/session.go diff --git a/internal/webtransport/session_manager.go b/webtransport/session_manager.go similarity index 100% rename from internal/webtransport/session_manager.go rename to webtransport/session_manager.go diff --git a/internal/webtransport/stream.go b/webtransport/stream.go similarity index 100% rename from internal/webtransport/stream.go rename to webtransport/stream.go diff --git a/internal/webtransport/streams_map.go b/webtransport/streams_map.go similarity index 100% rename from internal/webtransport/streams_map.go rename to webtransport/streams_map.go