Release notes

StreamHub changelog

Every shipped feature, change, fix and security update — one entry per release, newest first. Versions follow the repo-root VERSION and are kept in sync with the source repo.

v2.8.7 Latest July 9, 2026

Added

  • SRT in the dashboard — the create-ingress form gains "SRT (push)": selecting it locks passthrough mode ON (per the backend rule), disables transcode/backup, and explains the model (UDP, ~120 ms, the key travels as streamid — the key IS the credential, no user/password). The result view reveals the srt://…?streamid=… push URL + key + HLS/player URLs + latency; SRT badges on the ingress list; the URL-pull type hints srt:// sources.
  • HLS playback on /play and /embed — the public player probes the room's HLS playlist and defaults to HLS playback when live (WebRTC | HLS toggle on /play), so passthrough channels are now watchable on their player pages — and this is the surface where an active player-provider plugin (Vidstack) renders, including for anonymous viewers. No playlist → prior WebRTC-only behavior unchanged.
  • "Compartir" menu on live stream rows — copy the player URL, the /embed iframe snippet, the public HLS .m3u8 URL, or a self-contained video.js embed snippet straight from the Streams tab. Passthrough rows open their HLS directly (no LiveKit egress round-trip) and hide actions that don't apply.
v2.8.6 July 9, 2026

Fixed

  • Streams reconcile was killing passthrough rows — the LiveKit reconcile prunes active rows whose room has no live LiveKit publisher, and a passthrough room never exists in LiveKit, so the 2.8.5 mirrored rows were ended on every Streams list. Passthrough rows (participant='passthrough') are now exempt from the prune (same precedent as ws-mjpeg: liveness owned elsewhere), and the passthrough sync re-upserts EVERY live tick (self-healing if anything else ends the row). The dashboard Streams view now reliably shows live passthrough channels.
v2.8.5 July 9, 2026

Fixed

  • Passthrough channels were invisible in the dashboard Streams view — a passthrough ingest bypasses LiveKit, so no webhook ever created its streams row: the dashboard said "no active streams" while HLS was on air. The passthrough service now mirrors live remuxes into the per-app streams table (15s sync on playlist freshness: fresh ⇒ ACTIVE <room>/passthrough row; stale/stopped/deleted ⇒ ended). Best-effort — the media plane never depends on the dashboard write. +4 specs.
v2.8.4 July 9, 2026

Added

  • SRT ingest — two paths: (1) SRT push → passthrough (remux → HLS, near-zero CPU): POST /apps/:app/ingress {inputType:'srt', mode:'passthrough'} arms an ffmpeg SRT listener (srt://0.0.0.0:<port>?mode=listener, -c copy) and returns srt_url (srt://<host>:<port>?streamid=<key>); reuses the passthrough gate + port pool (SRT is UDP — open the same ports as udp). SRT requires passthrough mode (LiveKit's ingress API has no SRT input type); enforced at the DTO + controller. (2) SRT URL-pull for the transcode path: {inputType:'url', url:'srt://source:port'} — LiveKit pulls via GStreamer srtsrc (server-version dependent); the SSRF pull-guard rules for srt:// are regression-locked (public ok; loopback/metadata 400). +34 tests (152 suites / 1874). Docs: features/rtmp-passthrough.md (now RTMP/SRT).
v2.8.3 July 9, 2026

Changed — ingest profile switch (stability hardening + instant rollback)

  • New global switch STREAMHUB_INGEST_PROFILE (livekit | efficient) for how the LiveKit-transcoded RTMP/URL ingress is encoded. Default: livekit — the stable legacy routing where StreamHub attaches NO encoding opinions and LiveKit decides everything (pre-2.7.1 behaviour); new installs always start here. efficient is the opt-in experimental profile: an explicit single-layer preset (capped 3-layer preset when the app enables adaptive) so LiveKit's CPU-heavy 3-layer default can't leak through (measured: 3×1080p ≈ 100% → ~24% of 8 cores). Server-wide, per node in a cluster; restart to apply; flipping back to livekit is the instant rollback. See features/efficient-transcode.md.
  • Presets only — the efficient profile now exclusively sends LiveKit's built-in IngressVideoEncodingPreset values (never custom encoding layers, which proved LiveKit-version-coupled — the 2.8.2 H264_MAIN incident). Sub-720 caps fold up to the 720p preset; adaptive uses the matching capped 3-layer preset.
  • Fallback-on-reject — if LiveKit rejects StreamHub's encoding options, the create logs a warning and retries once with LiveKit defaults: options can only degrade the profile, never break ingress creation.
  • Audio-only footgun closed — LiveKit (≤1.8.4) ignores the audioOnly ingress flag, and an option-less ingress falls back to the 3-layer default (the opposite of what the flag promises). audioOnly is now DEPRECATED on the API (kept for back-compat); under the efficient profile it still pins the single-layer preset (worst case 1 layer, never 3); radios should use the RTMP→HLS passthrough mode.
v2.8.2 July 9, 2026

Fixed

  • Efficient transcode profile rejected by LiveKit ("video codec unsupported"). The 2.8.1 encoding builder set IngressVideoEncodingOptions.videoCodec to H264_MAIN, which the LiveKit ingress (validated against server 1.8.4) rejects — so creating a transcoded ingress for an adaptive app (the default) failed with a 503. Use H264_BASELINE (widest-compat live profile; probed against the live server). Regression-locked. Unit tests mock the ingress client, so this only surfaced under a real functional test against LiveKit.
v2.8.1 July 9, 2026

Fixed / Added

  • Efficient transcode profile — no more CPU blow-ups on the RTMP ingress. The LiveKit ingress used to fall back to LiveKit's default 3-layer 720p simulcast (three software x264 encodes per stream) whenever explicit video options weren't attached — which pinned an 8-core box at ~100% with just 3 streams. Now createIngress always attaches explicit single-layer video options by default (the safeguard: never an accidental 3-layer), and a new per-app webrtc.transcode_max_height (360 | 480 | 720 | 1080, default 720) caps the resolution. Measured on prod (3× 1080p sources, 8-core box): 3-layer 720p ≈ 100% → 1-layer ≈ 24%. (The output cap is decode-bound for high-res sources, so 720/480/360 land near the same ~24% — the layer count, not the cap, is the CPU lever; see features/efficient-transcode.md.) webrtc.adaptive: true gives a ladder, but capped at transcode_max_height (so adaptive can't explode CPU either). Pure per-app config, consumed at ingress-create on whichever node owns the room — scales locally and across the cluster with no central state. New features/efficient-transcode.md; IngressVideoEncodingOptions builder with 19 unit tests.
v2.8.0 July 8, 2026

Added

  • RTMP→HLS passthrough (remux, no transcode) — AntMedia parity, near-zero CPU without a GPU. A new ingest mode: POST /apps/:app/ingress {mode:'passthrough'} ingests RTMP and remuxes it straight to HLS (ffmpeg -c copy), bypassing LiveKit entirely — no video encode at all, so CPU is near-idle even without a GPU. This is the no-transcode path LiveKit can't offer (its RTMP ingress always transcodes). Additive and safe: LiveKit's :1935 and the whole WebRTC/transcode/egress/recording path are untouched; passthrough runs a separate ingest on :1937 and is env-gated OFF by default (STREAMHUB_RTMP_PASSTHROUGH_ENABLE, port STREAMHUB_RTMP_PASSTHROUGH_PORT). Mutually exclusive with transcoding/adaptive (enforced in config): turning on passthrough forces rtmp.transcode/transcoding.enabled/webrtc.adaptive off, and vice-versa. The API returns a capabilities block enumerating what a remux gives up (no WebRTC/sub-second, no ABR ladder, source must be H.264/AAC, no egress recording, no room features) so the UI can warn. The existing HLS player plays it with no change. Implemented with zero new dependencies (ffmpeg -listen, not a bundled RTMP server). See features/rtmp-passthrough.md.
  • Audio-only ingest already shipped in 2.7.2 remains the recommended path for a radio (drops the video track → the trivial-audio-transcode path); use passthrough when you need to carry video with no re-encode.
v2.7.2 July 8, 2026

Added

  • Audio-only ingest (radio) — near-idle CPU without a GPUPOST /apps/:app/ingress {audioOnly:true} sets LiveKit's audioOnly so the ingress DROPS the incoming video track: no video encode at all (the ~1 core/channel cost of software x264), leaving only the trivial AAC→Opus audio transcode. For a radio/podcast this collapses ingress CPU from ~85%/channel to ~1-2%. Since RTMP cannot passthrough on LiveKit (it always transcodes video), this is the no-GPU path to very low CPU for audio channels. New CreateIngressInput.audioOnly
    • audioOnly regression tests. (Video passthrough without transcoding needs WHIP, or a future RTMP→HLS remux path.)
v2.7.1 July 8, 2026

Fixed

  • RTMP ingress pinned CPU with a forced 3-layer simulcast, unaffected by the adaptive/transcoding toggles — a transcoded RTMP/URL ingress used LiveKit's default 3-layer 720p H.264 simulcast (three real-time encodes), which no app setting could reduce, so turning "adaptive"/"transcoding" off in the config never lowered the load. Now webrtc.adaptive: false wires an explicit single-layer ingress preset (H264_720P_30FPS_1_LAYER) — ~3× less ingress CPU — so the toggle actually controls the transcode. Default keeps the simulcast ladder (back-compat). Takes effect on newly-created ingresses (delete
    • recreate to apply to an existing one). New CreateIngressInput.simulcast; regression tests in livekit.service.ingress.spec.ts.
v2.7.0 July 8, 2026

Added

  • Swappable advanced player (Vidstack) as a plugin — a new player plugin slot: when an app enables a player-provider plugin, the HLS + VOD/MP4 player renders it instead of the built-in video.js player (falls back to video.js otherwise); WebRTC sub-second live is untouched. First provider = Vidstack (@vidstack/react + local hls.js) — modern, modular, accessible, HLS/DASH/DRM capable. Anonymous /play + /embed resolve the provider too. Foundation for an OTT that serves both live + VOD. See features/players.md.
  • Per-stream plugin opt-out — a plugin enabled on an app still applies to every live stream by default, but you can now edit a stream and disable specific plugins for that stream only (streams.disabled_plugins; GET/PATCH /apps/:app/streams/:id/plugins; overlay endpoints gained ?stream=&room=). Other streams are unaffected. StreamsTab per-stream plugin editor.
  • CDN / VOD delivery playbook (features/CDN-VOD.md) — serve VOD via an external CDN in front of S3 (s3.public_url), live HLS via a CDN + the 2.5.0 cache headers, or the cluster's own edge nodes as a pull-through CDN (deploy/nginx-hls-edge.conf). Hybrid recipes for an OTT.

Testing

  • Real component/DOM testing (vitest + jsdom + testing-library) added to the web suite — it mounts React components and exercises DOM behaviour (the layer the logic-only node --test suite was blind to). Caught + regression-locks the controlled-Switcher bug; covers the player swap + the Vidstack mount + the per-stream editor. Wired into CI (npm run test:components).

Fixed

  • Every dashboard toggle was stuck off — the controlled Switcher reported the current value to onChange instead of the intended new one, so plugin / recording / config toggles could never turn on. Report the new value.
  • RTMP/URL ingress transcoding bypass rejectionenableTranscoding:false (bypass) is WHIP-only; LiveKit rejects it for RTMP/URL ("bypassing transcoding impossible with selected input type"). Only forward the bypass flag for WHIP.
  • CI ran on node:20 while the project targets Node 24 (engines>=24) — Jest 30's transform uses crypto.hash() (Node 21+), so all core suites failed to transform. Bumped CI images to node:24.
v2.6.0 July 8, 2026

Added

  • Dynamic media-pipeline diagram per appGET /apps/:app/pipeline resolves the app's real topology (ingest RTMP/RTMPS/WHIP/WebRTC/RTSP/WS-MJPEG, transcoding gpu/cpu + adaptive ladder, delivery WebRTC/HLS/CDN, recording→S3, plugins, live streams) from config.yaml + live hints; the dashboard Overview renders it as an animated SVG flow (AppFlowDiagram) that only shows enabled stages, with active-vs-configured pulses, theme-aware + prefers-reduced-motion.
  • Primary + backup RTMP stream keys (Dacast/Vimeo-style failover) — POST /apps/:app/ingress {backup:true} mints a second LiveKit ingress bound to the same room (distinct identity) → identical HLS/WebRTC playback URL whichever key is live; backup-as-standby, primary preferred. Dashboard shows both keys.
  • Endless Radio / 24-7 linear-channel hardening (all behind features.linear.*, default off): persistent-room emptyTimeout so a source outage doesn't recycle the room/SID; auto-HLS watchdog; a broadcaster quota preset (maxConcurrentStreams:-1); DVR bounded live manifest (livePlaylistName + .ts retention janitor). Gap audit in features/ENDLESS-RADIO-STATUS.md.
  • Cluster observability stackdeploy/observability/ cluster overlay: Prometheus (3-node scrape via nginx metrics-export allowlisted to the master) + Loki/Promtail log ingestion + Grafana cluster-overview/cluster-logs dashboards + install scripts. operations/CLUSTER-OBSERVABILITY.md.

Fixed

  • Observability: Grafana Prometheus datasource :9090→:9091; Loki common.instance_addr=127.0.0.1 (single-binary on a docker host advertised its ring on docker0 → self-dial refused / push 500).
v2.5.0 July 8, 2026

Cluster becomes a real distribution fabric (phase 1) — see streamhub-docs/architecture/CLUSTER-ROADMAP.md.

Added

  • Node roles + edge login-lock — a node now has a DB-driven role (standalone | master | edge, in _streamhub_meta). An edge serves the portal but blocks login (AuthService.requireSecret → 403), so a cluster node can't be used as a standalone app; the SPA shows a "manage at the master" screen. GET /auth/config exposes nodeRole/masterUrl/loginEnabled.
  • Standalone ↔ cluster conversionPOST /cluster/join-master (become an edge of a master) and POST /cluster/leave (revert to standalone). Master promotion (promote) + the installer "edges run a core" change are the next phase.
  • GRID-1 — remote GPU worker offload — plugin workers with placement: 'grid' now dispatch to a BYO-GPU node: a X-Cluster-Token-gated grid agent (POST /api/v1/grid/workers + status/stop/logs) runs the worker on the node; the origin injects originUrl so results post back; the heartbeat advertises a grid GPU block via nvidia-smi. yolo ships as the reference (placement: 'grid', falls back to local when no GPU node matches). New streamhub-docs/features/GRID.md.
  • HLS edge/CDN caching — correct Cache-Control on /hls (.ts immutable, .m3u8 no-cache by default; STREAMHUB_HLS_CDN opts into short SWR for a CDN)
    • ready-to-use nginx/Caddy edge-cache configs. New features/HLS-CDN.md.

Security

  • Cluster join no longer vends secrets over plaintext — the LiveKit apiSecret + Redis URL are withheld from the POST /cluster/join response unless the request arrived over TLS (X-Forwarded-Proto=https/req.secure/ loopback); STREAMHUB_CLUSTER_REQUIRE_TLS (default on).
  • Cluster-token brute-force now feeds the auto-ban — a failed X-Cluster-Token records an invalid_token offense, so the network-security middleware bans the IP (previously the cluster endpoints recorded nothing).
v2.4.1 July 7, 2026

Fixed

  • NestJS 11 SPA-fallback regression — the ServeStaticModule exclude patterns used the old path-to-regexp (.*) capture, which throws under path-to-regexp v8 (bundled with NestJS 11): every request that fell through to the SPA fallback (dashboard deep-link reloads, unmatched /api paths, the cluster /nodes view) returned 500 instead of serving index.html / a clean 404. Rewrote the excludes to the v8 named-wildcard spelling (/api/*splat, /rtc/*splat, …). Discovered on the 2.4.0 rollout.
v2.4.0 July 7, 2026

Added

  • Transcription plugin (#10) — VOD → mp3 → faster-whisper (OSS) or OpenAI → SRT uploaded to the app's S3 + transcript stored on the VOD, with per-track speaker split. Dashboard transcript panel + "Download SRT".
  • StreamHub MCP server (mcp-server/) — the /api/v1 surface as 47 MCP tools so an AI client (Claude Desktop/Code) can operate a StreamHub instance; destructive ops hidden unless --allow-destructive + confirm:true.
  • Documentation sitedocs.streamhub.studio (+ /docs) renders the streamhub-docs/ tree (CommonMark, sidebar, search); php artisan docs:sync.
  • Downloadable example bundles — every app example as a self-contained, configurable bundle (edit config.js, host anywhere) + /apps/:app/examples list/zip download.
  • Recording on/off toggle per app (dashboard VODs tab) — master switch over config.recording.enabled.
  • Delete-app danger zone in the app System tab (typed-name confirmation).
  • StreamHub Agent (separate repo) — CCTV edge agent with detection as a mode (remote | edge | off); one-command Linux/ARM installer.
  • n8n automation workflows (build platform/agent/docs, clean GitHub push, IMAP + ticket chatbots) — documented in operations/N8N-WORKFLOWS.md.

Fixed

  • WebRTC adaptor validated — 0 → 92 tests; fixed play_finished never firing and data_channel_opened double-firing (AntMedia drop-in contract).
  • Rendition ladder can be cleared — deleting all adaptive resolutions no longer 400s (@ArrayMinSize removed) and no longer silently reverts to the defaults on read; clearing the ladder now disables adaptive delivery.
v2.3.0 July 7, 2026

Large stabilization + feature wave.

Added

  • LiveKit native features: rooms/moderation API (kick/mute), data messages (real-time scheduled-live push), room/participant metadata, auto-record on room start, track/audio-only egress (no headless Chrome), config-driven adaptive stream + dynacast, SSRF-guarded URL-pull ingress.
  • In-app SQLite backups/system/backups: versioned + gzipped snapshots, 7-day retention, guarded restore (integrity-check + auto safety copy).
  • ISO-readiness — append-only hash-chained audit trail (/system/audit)
    • /system/compliance posture (ISO 27002 §8 evidence); dependency scanning in CI (npm high advisories 10 → 0).
  • RTMPS ingest — opt-in TLS front (:1936), rtmps_url surfaced on the ingress API. Validated end-to-end on a real box.
  • OS-tuning reporter (/system/os-tuning) + 30-day resource-usage history (/system/metrics/history, CPU/RAM/net/disk dot charts).
  • DRM implementation-ready (DrmProvider interface, CENC/CBCS packaging seam, features.drm, off by default).
  • Next-gen EDGE-via-GRID plugin contract (worker.placement: local|grid) + Vision/Grid/Babel/semantic-search manifest scaffolds.
  • Debian 12/13 support in the installer (in addition to Ubuntu).
  • StreamHub Agent (separate repo) — a C edge agent for CCTV cameras that streams to a StreamHub instance with remote AI detection.
  • Docs: AWS cluster guide, OpenAPI snapshot + Postman, ffmpeg-slim analysis, OWASP 2025 + ISO readiness, managed-hosting billing/pricing design.

Changed

  • Node 20 → 24 LTS; NestJS 10 → 11; nodemailer → 9 (SMTP-injection fix).
  • Public user registration off by default (STREAMHUB_PUBLIC_REGISTRATION).
  • Security P0 class closed + regression-locked: SSRF egress guards (callbacks/S3/MQTT), X-Forwarded-For trust fix, signed playback URLs, multi-tenant isolation ordered before the api_token bypass.

Security

  • LICENSE AGPL-3.0 at repo root; per-package license fields.
  • The customer-panel instance-provisioning flow is disabled by default (STREAMHUB_PROVISIONING_ENABLED) until the owner-supervised launch is signed off.
v2.2.0 July 5, 2026
  • Single-app packaging (/opt/streamhub, streamhub CLI, systemd unit), versioned release pipeline, upgrade + migration in install.sh.
  • scheduled-live plugin (9th). Observability connect guide.
  • Cluster join / Redis reachability fixes; anonymous install telemetry.

Source of truth: the core repo CHANGELOG.md, versioned with VERSION.