Skip to content

Commit 172f1dc

Browse files
committed
feat: don't use ring buffer anymore
1 parent eabc76f commit 172f1dc

6 files changed

Lines changed: 53 additions & 101 deletions

File tree

config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ transcode:
4646
audio_bitrate: "320k"
4747
audio_sample_rate: 48000
4848
output_format: "mpegts"
49-
buffer_capacity: 104857600
5049
initial_data_threshold: 65536
5150

5251
# Define your streaming sources here. Each source maps an embed service to URL templates.

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ require (
1010
github.com/knadh/koanf/parsers/yaml v1.1.0
1111
github.com/knadh/koanf/providers/file v1.2.1
1212
github.com/knadh/koanf/v2 v2.3.2
13-
github.com/smallnest/ringbuffer v0.1.1
1413
github.com/urfave/cli/v3 v3.6.2
1514
github.com/vishen/go-chromecast v0.3.4
1615
)

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
6969
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7070
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
7171
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
72-
github.com/smallnest/ringbuffer v0.1.1 h1:KL2iILLdDCr9nWxYrNcsQ7Px7EVnoNBDJ0r/M/hEksA=
73-
github.com/smallnest/ringbuffer v0.1.1/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0=
7472
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7573
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
7674
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

internal/app/config.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ type TranscodeConfig struct {
7777
AudioSampleRate int `koanf:"audio_sample_rate" validate:"required"`
7878
AudioBitrate string `koanf:"audio_bitrate" validate:"required"`
7979
OutputFormat string `koanf:"output_format" validate:"required"`
80-
BufferCapacity int `koanf:"buffer_capacity" validate:"required"`
8180
InitialDataThreshold int `koanf:"initial_data_threshold" validate:"required"`
8281
}
8382

internal/cast/service.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cast
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68
"log/slog"
79
"net"
810
"slices"
@@ -76,12 +78,12 @@ func CastStream(ctx context.Context, cfg *app.Config, stream *media.Stream) erro
7678
if err != nil {
7779
return fmt.Errorf("starting transcode: %w", err)
7880
}
79-
defer reader.Close()
8081
defer func() {
8182
if err := wait(); err != nil {
8283
slog.WarnContext(ctx, "ffmpeg exited with error", "error", err)
8384
}
8485
}()
86+
defer reader.Close()
8587

8688
localIP, err := localIPFromInterface(iface)
8789
if err != nil {
@@ -93,21 +95,22 @@ func CastStream(ctx context.Context, cfg *app.Config, stream *media.Stream) erro
9395
streamHeaders = dlna.StreamHeaders(fmtInfo.ContentType)
9496
}
9597

96-
srv, err = transcode.NewStreamServer(ctx, transcode.StreamServerConfig{
97-
LocalIP: localIP,
98-
ContentType: fmtInfo.ContentType,
99-
Extension: fmtInfo.Extension,
100-
Headers: streamHeaders,
101-
BufferCapacity: cfg.Transcode.BufferCapacity,
102-
}, reader)
103-
if err != nil {
104-
return fmt.Errorf("starting stream server: %w", err)
98+
initial := make([]byte, cfg.Transcode.InitialDataThreshold)
99+
n, err := io.ReadFull(reader, initial)
100+
if err != nil && err != io.ErrUnexpectedEOF {
101+
return fmt.Errorf("waiting for initial transcode data: %w", err)
105102
}
106-
defer srv.Stop()
107103

108-
if err := srv.WaitForData(ctx, cfg.Transcode.InitialDataThreshold); err != nil {
109-
return fmt.Errorf("waiting for initial stream data: %w", err)
104+
srv, err = transcode.NewStreamServer(transcode.StreamServerConfig{
105+
LocalIP: localIP,
106+
ContentType: fmtInfo.ContentType,
107+
Extension: fmtInfo.Extension,
108+
Headers: streamHeaders,
109+
}, io.MultiReader(bytes.NewReader(initial[:n]), reader))
110+
if err != nil {
111+
return fmt.Errorf("starting stream server: %w", err)
110112
}
113+
defer srv.Close()
111114

112115
streamURL = srv.URL()
113116
contentType = fmtInfo.ContentType

internal/transcode/server.go

Lines changed: 37 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,85 +7,52 @@ import (
77
"net/http"
88
"net/url"
99
"sync/atomic"
10-
"time"
11-
12-
"github.com/smallnest/ringbuffer"
1310
)
1411

15-
const defaultReadBufSize = 32 * 1024 / 2
16-
17-
// StreamServer serves transcode output over HTTP using a blocking ring
18-
// buffer. Write blocks when full, applying backpressure to ffmpeg so
19-
// no data is ever silently overwritten.
12+
// StreamServer serves an io.Reader over HTTP. The consumer's read
13+
// pace drives the producer through OS pipe backpressure.
2014
type StreamServer struct {
21-
ring *ringbuffer.RingBuffer
22-
cancel context.CancelFunc
15+
reader io.Reader
2316
active atomic.Bool
2417
contentType string
2518
extension string
2619
headers map[string]string
2720
listener net.Listener
2821
server *http.Server
29-
errCh chan error
22+
done chan struct{}
3023
}
3124

32-
// StreamServerConfig holds configuration for creating a StreamServer.
25+
// StreamServerConfig holds the parameters for NewStreamServer.
3326
type StreamServerConfig struct {
34-
LocalIP string
35-
ContentType string
36-
Extension string
37-
Headers map[string]string
38-
BufferCapacity int
27+
LocalIP string
28+
ContentType string
29+
Extension string
30+
Headers map[string]string
3931
}
4032

41-
// NewStreamServer creates, starts, and returns a StreamServer. It begins
42-
// ingesting from stream and serving HTTP immediately.
43-
func NewStreamServer(ctx context.Context, cfg StreamServerConfig, stream io.Reader) (*StreamServer, error) {
33+
func NewStreamServer(cfg StreamServerConfig, reader io.Reader) (*StreamServer, error) {
4434
ln, err := net.Listen("tcp", cfg.LocalIP+":0")
4535
if err != nil {
4636
return nil, err
4737
}
48-
49-
ctx, cancel := context.WithCancel(ctx)
50-
5138
s := &StreamServer{
52-
ring: ringbuffer.New(cfg.BufferCapacity).SetBlocking(true),
53-
cancel: cancel,
39+
reader: reader,
5440
contentType: cfg.ContentType,
5541
extension: cfg.Extension,
5642
headers: cfg.Headers,
5743
listener: ln,
58-
errCh: make(chan error, 1),
44+
done: make(chan struct{}),
5945
}
60-
61-
s.ring.WithCancel(ctx)
62-
6346
mux := http.NewServeMux()
6447
mux.HandleFunc("/stream"+cfg.Extension, s.handleStream)
65-
6648
s.server = &http.Server{Handler: mux}
67-
68-
go func() {
69-
s.ring.ReadFrom(stream)
70-
s.ring.CloseWriter()
71-
}()
72-
7349
go func() {
74-
if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
75-
s.errCh <- err
76-
}
77-
close(s.errCh)
50+
s.server.Serve(ln)
51+
close(s.done)
7852
}()
79-
8053
return s, nil
8154
}
8255

83-
// Stop cancels ingestion and closes the server.
84-
func (s *StreamServer) Stop() {
85-
s.cancel()
86-
s.server.Close()
87-
}
88-
8956
// URL returns the full URL the server is listening on.
9057
func (s *StreamServer) URL() *url.URL {
9158
return &url.URL{
@@ -95,72 +62,59 @@ func (s *StreamServer) URL() *url.URL {
9562
}
9663
}
9764

65+
// Close shuts down the server.
66+
func (s *StreamServer) Close() error { return s.server.Close() }
67+
9868
// Wait blocks until the server exits or the context is cancelled.
9969
func (s *StreamServer) Wait(ctx context.Context) error {
10070
select {
101-
case err := <-s.errCh:
102-
return err
103-
case <-ctx.Done():
71+
case <-s.done:
10472
return nil
105-
}
106-
}
107-
108-
// WaitForData blocks until the buffer has at least minBytes of data or
109-
// the context is cancelled.
110-
func (s *StreamServer) WaitForData(ctx context.Context, minBytes int) error {
111-
const tick = 50 * time.Millisecond
112-
ticker := time.NewTicker(tick)
113-
defer ticker.Stop()
114-
115-
for {
116-
if s.ring.Length() >= minBytes {
117-
return nil
118-
}
119-
select {
120-
case <-ctx.Done():
121-
return ctx.Err()
122-
case <-ticker.C:
123-
}
73+
case <-ctx.Done():
74+
return ctx.Err()
12475
}
12576
}
12677

12778
func (s *StreamServer) handleStream(w http.ResponseWriter, r *http.Request) {
128-
if !s.active.CompareAndSwap(false, true) {
129-
http.Error(w, "stream already has an active reader", http.StatusServiceUnavailable)
130-
return
131-
}
132-
defer s.active.Store(false)
133-
13479
w.Header().Set("Content-Type", s.contentType)
13580
for k, v := range s.headers {
13681
w.Header().Set(k, v)
13782
}
138-
13983
if r.Method == http.MethodHead {
14084
w.WriteHeader(http.StatusOK)
14185
return
14286
}
14387

144-
w.WriteHeader(http.StatusOK)
88+
if !s.active.CompareAndSwap(false, true) {
89+
http.Error(w, "stream already active", http.StatusServiceUnavailable)
90+
return
91+
}
92+
defer s.active.Store(false)
14593

94+
w.WriteHeader(http.StatusOK)
14695
if f, ok := w.(http.Flusher); ok {
14796
f.Flush()
14897
}
14998

150-
buf := make([]byte, defaultReadBufSize)
151-
99+
buf := make([]byte, 32*1024)
100+
readerDone := false
152101
for {
153-
n, err := s.ring.Read(buf)
102+
n, err := s.reader.Read(buf)
154103
if n > 0 {
155-
if _, writeErr := w.Write(buf[:n]); writeErr != nil {
156-
return
104+
if _, we := w.Write(buf[:n]); we != nil {
105+
break // TV disconnected — keep server alive
157106
}
158107
if f, ok := w.(http.Flusher); ok {
159108
f.Flush()
160109
}
161110
}
162111
if err != nil {
163-
return
112+
readerDone = true
113+
break
164114
}
165115
}
116+
if readerDone {
117+
// Shut down so Wait() unblocks. Goroutine avoids handler deadlock.
118+
go s.server.Close()
119+
}
166120
}

0 commit comments

Comments
 (0)