v2: tune playback for bursty upstream + clean overflow handling

- Player: match webplayer.online's proven mpegts.js live-sync config
  (deep 30-90s buffer, 45s target latency, 1.1x catch-up). The upstream
  delivers in bursts with multi-second gaps (measured up to ~6s); the
  deep buffer rides over them so 4K HEVC plays smoothly.
- Hub: on subscriber buffer overflow, cleanly disconnect the client so
  it reconnects and resyncs from a TS packet boundary instead of
  dropping bytes mid-stream (which corrupted alignment / sync_byte).
- Enlarge per-client buffer (256 -> 2048 chunks).
- Refresh real1.m3u from upstream.
This commit is contained in:
2026-06-12 19:48:33 -07:00
parent 723ae1f1ac
commit 03296e7572
4 changed files with 410 additions and 328 deletions

View File

@@ -16,13 +16,15 @@ import (
const chunkSize = 64 * 1024
// subscriberBuffer is how many chunks may queue per client before it is
// considered too slow and dropped (prevents one slow client stalling others).
const subscriberBuffer = 256
// considered too slow. At ~64KB/chunk this is a large cushion so transient
// MSE backpressure never overflows for a normal viewer.
const subscriberBuffer = 2048
// Subscriber receives copies of upstream TS chunks.
type subscriber struct {
ch chan []byte
closed chan struct{}
ch chan []byte
closed chan struct{}
overflow chan struct{} // closed by the hub when the client falls too far behind
}
// Hub manages a single shared upstream connection and fans its bytes out to
@@ -118,8 +120,9 @@ func (h *Hub) GotData() (bool, time.Duration) {
// function the caller must invoke when done.
func (h *Hub) Subscribe() (*subscriber, func()) {
sub := &subscriber{
ch: make(chan []byte, subscriberBuffer),
closed: make(chan struct{}),
ch: make(chan []byte, subscriberBuffer),
closed: make(chan struct{}),
overflow: make(chan struct{}),
}
h.mu.Lock()
@@ -149,7 +152,13 @@ func (s *subscriber) Chan() <-chan []byte { return s.ch }
// Closed signals the subscriber has been removed by the hub.
func (s *subscriber) Done() <-chan struct{} { return s.closed }
// broadcast sends a chunk to all subscribers, dropping any that are too slow.
// Overflow signals the client fell too far behind and must reconnect. We never
// drop bytes mid-stream (that corrupts TS packet alignment); instead we close
// the connection so the client reconnects and resyncs from a packet boundary.
func (s *subscriber) Overflow() <-chan struct{} { return s.overflow }
// broadcast sends a chunk to all subscribers. A subscriber whose buffer is full
// is flagged for disconnect rather than having bytes silently dropped.
func (h *Hub) broadcast(chunk []byte) {
h.mu.Lock()
if !h.gotData {
@@ -165,7 +174,14 @@ func (h *Hub) broadcast(chunk []byte) {
select {
case s.ch <- chunk:
default:
// Slow client: drop this chunk for them rather than blocking everyone.
// Buffer full: signal overflow once so the handler closes the
// connection cleanly. Never drop bytes — that breaks TS alignment.
select {
case <-s.overflow:
// already flagged
default:
close(s.overflow)
}
}
}
}