Live video on the AT Protocol

media: add SignMP4 tracing, fix channel leak (#126)

* tracing: add a lot more instrumentation around SignMP4 tracing

* tracing: track signing duration for external signing too

* segchanman: time out segment send

* concat: okay yeah i see why this was a leak now

* webrtc_playback: actually unsubscribe here too

authored by Eli Mallon and committed by GitHub c8fb7f3c a74a4842

+60 -24
+1 -6
go.mod
··· 49 gitlab.com/gitlab-org/release-cli v0.18.0 50 go.opentelemetry.io/otel v1.35.0 51 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 52 - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 53 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 54 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 55 - go.opentelemetry.io/otel/log v0.11.0 56 go.opentelemetry.io/otel/sdk v1.35.0 57 - go.opentelemetry.io/otel/sdk/log v0.11.0 58 - go.opentelemetry.io/otel/sdk/metric v1.35.0 59 go.uber.org/goleak v1.3.0 60 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 61 golang.org/x/image v0.22.0 ··· 244 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 245 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 246 go.opentelemetry.io/otel/metric v1.35.0 // indirect 247 go.opentelemetry.io/otel/trace v1.35.0 // indirect 248 go.opentelemetry.io/proto/otlp v1.5.0 // indirect 249 go.uber.org/atomic v1.11.0 // indirect
··· 49 gitlab.com/gitlab-org/release-cli v0.18.0 50 go.opentelemetry.io/otel v1.35.0 51 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 52 go.opentelemetry.io/otel/sdk v1.35.0 53 go.uber.org/goleak v1.3.0 54 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 55 golang.org/x/image v0.22.0 ··· 238 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 239 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 240 go.opentelemetry.io/otel/metric v1.35.0 // indirect 241 + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 242 go.opentelemetry.io/otel/trace v1.35.0 // indirect 243 go.opentelemetry.io/proto/otlp v1.5.0 // indirect 244 go.uber.org/atomic v1.11.0 // indirect
-10
go.sum
··· 657 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= 658 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= 659 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= 660 - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= 661 - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= 662 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= 663 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= 664 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= 665 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= 666 - go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= 667 - go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= 668 go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 669 go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 670 go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 671 go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 672 - go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= 673 - go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= 674 go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 675 go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 676 go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
··· 657 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= 658 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= 659 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= 660 go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 661 go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 662 go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 663 go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 664 go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 665 go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 666 go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+2 -2
pkg/media/concat.go
··· 126 // them in a pipe so that we don't miss any in between iterations of the output 127 allFiles := make(chan []byte, 1024) 128 go func() { 129 for { 130 - ch := streamer.SubscribeSegment(ctx, user, rendition) 131 select { 132 case <-ctx.Done(): 133 log.Debug(ctx, "exiting segment reader") 134 - streamer.UnsubscribeSegment(ctx, user, rendition, ch) 135 return 136 case file := <-ch: 137 log.Debug(ctx, "got segment", "file", file.Filepath)
··· 126 // them in a pipe so that we don't miss any in between iterations of the output 127 allFiles := make(chan []byte, 1024) 128 go func() { 129 + ch := streamer.SubscribeSegment(ctx, user, rendition) 130 + defer streamer.UnsubscribeSegment(ctx, user, rendition, ch) 131 for { 132 select { 133 case <-ctx.Done(): 134 log.Debug(ctx, "exiting segment reader") 135 return 136 case file := <-ch: 137 log.Debug(ctx, "got segment", "file", file.Filepath)
+22
pkg/media/media_signer.go
··· 9 "fmt" 10 "io" 11 "path/filepath" 12 13 "git.stream.place/streamplace/c2pa-go/pkg/c2pa" 14 "go.opentelemetry.io/otel" ··· 18 "stream.place/streamplace/pkg/crypto/aqpub" 19 "stream.place/streamplace/pkg/crypto/signers" 20 "stream.place/streamplace/pkg/log" 21 ) 22 23 type MediaSigner interface { 24 SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) 25 Pub() aqpub.Pub 26 } 27 28 type MediaSignerLocal struct { ··· 80 }, nil 81 } 82 83 func (ms *MediaSignerLocal) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { 84 ctx, span := otel.Tracer("signer").Start(ctx, "SignMP4") 85 defer span.End() 86 title := "livestream" ··· 109 }, 110 }, 111 } 112 manifestBs, err := json.Marshal(mani) 113 if err != nil { 114 return nil, fmt.Errorf("failed to marshal manifest: %w", err) ··· 118 if err != nil { 119 return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) 120 } 121 alg, err := c2pa.GetSigningAlgorithm(string(c2pa.ES256K)) 122 if err != nil { 123 return nil, fmt.Errorf("failed to get signing algorithm: %w", err) 124 } 125 b, err := c2pa.NewBuilder(&manifest, &c2pa.BuilderParams{ 126 Cert: ms.Cert, 127 Signer: ms.Signer, ··· 131 if err != nil { 132 return nil, fmt.Errorf("failed to create C2PA builder: %w", err) 133 } 134 135 output := &aqio.ReadWriteSeeker{} 136 err = b.Sign(input, output, "video/mp4") 137 if err != nil { 138 return nil, fmt.Errorf("failed to sign MP4: %w", err) 139 } 140 bs, err := output.Bytes() 141 if err != nil { 142 return nil, fmt.Errorf("failed to get output bytes: %w", err) 143 } 144 return bs, nil 145 } 146
··· 9 "fmt" 10 "io" 11 "path/filepath" 12 + "time" 13 14 "git.stream.place/streamplace/c2pa-go/pkg/c2pa" 15 "go.opentelemetry.io/otel" ··· 19 "stream.place/streamplace/pkg/crypto/aqpub" 20 "stream.place/streamplace/pkg/crypto/signers" 21 "stream.place/streamplace/pkg/log" 22 + "stream.place/streamplace/pkg/spmetrics" 23 ) 24 25 type MediaSigner interface { 26 SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) 27 Pub() aqpub.Pub 28 + Streamer() string 29 } 30 31 type MediaSignerLocal struct { ··· 83 }, nil 84 } 85 86 + func (ms *MediaSignerLocal) Streamer() string { 87 + return ms.StreamerName 88 + } 89 + 90 func (ms *MediaSignerLocal) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { 91 + startTime := time.Now() 92 ctx, span := otel.Tracer("signer").Start(ctx, "SignMP4") 93 defer span.End() 94 title := "livestream" ··· 117 }, 118 }, 119 } 120 + ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_MarshalManifest") 121 manifestBs, err := json.Marshal(mani) 122 if err != nil { 123 return nil, fmt.Errorf("failed to marshal manifest: %w", err) ··· 127 if err != nil { 128 return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) 129 } 130 + span.End() 131 + 132 + ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_GetSigningAlgorithm") 133 alg, err := c2pa.GetSigningAlgorithm(string(c2pa.ES256K)) 134 if err != nil { 135 return nil, fmt.Errorf("failed to get signing algorithm: %w", err) 136 } 137 + span.End() 138 + 139 + ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_NewBuilder") 140 b, err := c2pa.NewBuilder(&manifest, &c2pa.BuilderParams{ 141 Cert: ms.Cert, 142 Signer: ms.Signer, ··· 146 if err != nil { 147 return nil, fmt.Errorf("failed to create C2PA builder: %w", err) 148 } 149 + span.End() 150 151 + ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_Sign") 152 output := &aqio.ReadWriteSeeker{} 153 err = b.Sign(input, output, "video/mp4") 154 if err != nil { 155 return nil, fmt.Errorf("failed to sign MP4: %w", err) 156 } 157 + span.End() 158 + 159 + ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_OutputBytes") 160 bs, err := output.Bytes() 161 if err != nil { 162 return nil, fmt.Errorf("failed to get output bytes: %w", err) 163 } 164 + span.End() 165 + spmetrics.SigningDuration.WithLabelValues(ms.StreamerName).Observe(float64(time.Since(startTime).Milliseconds())) 166 return bs, nil 167 } 168
+11 -1
pkg/media/media_signer_ext.go
··· 9 "io" 10 "os" 11 "os/exec" 12 13 "github.com/decred/dcrd/dcrec/secp256k1" 14 "github.com/mr-tron/base58" 15 "stream.place/streamplace/pkg/config" 16 "stream.place/streamplace/pkg/crypto/aqpub" 17 ) 18 19 type MediaSignerExt struct { ··· 52 } 53 54 func (ms *MediaSignerExt) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { 55 // Get the path to the current executable 56 execPath, err := os.Executable() 57 if err != nil { ··· 98 if err := cmd.Wait(); err != nil { 99 return nil, fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String()) 100 } 101 - 102 return stdout.Bytes(), nil 103 } 104 105 func (ms *MediaSignerExt) Pub() aqpub.Pub { 106 return ms.pub 107 }
··· 9 "io" 10 "os" 11 "os/exec" 12 + "time" 13 14 "github.com/decred/dcrd/dcrec/secp256k1" 15 "github.com/mr-tron/base58" 16 + "go.opentelemetry.io/otel" 17 "stream.place/streamplace/pkg/config" 18 "stream.place/streamplace/pkg/crypto/aqpub" 19 + "stream.place/streamplace/pkg/spmetrics" 20 ) 21 22 type MediaSignerExt struct { ··· 55 } 56 57 func (ms *MediaSignerExt) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { 58 + startTime := time.Now() 59 + ctx, span := otel.Tracer("signer").Start(ctx, "SignMP4_Ext") 60 + defer span.End() 61 // Get the path to the current executable 62 execPath, err := os.Executable() 63 if err != nil { ··· 104 if err := cmd.Wait(); err != nil { 105 return nil, fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String()) 106 } 107 + spmetrics.SigningDuration.WithLabelValues(ms.streamer).Observe(float64(time.Since(startTime).Milliseconds())) 108 return stdout.Bytes(), nil 109 } 110 111 func (ms *MediaSignerExt) Pub() aqpub.Pub { 112 return ms.pub 113 } 114 + 115 + func (ms *MediaSignerExt) Streamer() string { 116 + return ms.streamer 117 + }
+11 -2
pkg/media/segchanman/segchanman.go
··· 4 "context" 5 "fmt" 6 "sync" 7 8 "go.opentelemetry.io/otel" 9 ) 10 11 // it's a segment channel manager, you see ··· 39 chs = []chan *Seg{} 40 s.segChans[key] = chs 41 } 42 - ch := make(chan *Seg, 1024) 43 chs = append(chs, ch) 44 s.segChans[key] = chs 45 return ch ··· 74 } 75 for _, ch := range chs { 76 go func(ch chan *Seg) { 77 - ch <- seg 78 }(ch) 79 } 80 }
··· 4 "context" 5 "fmt" 6 "sync" 7 + "time" 8 9 "go.opentelemetry.io/otel" 10 + "stream.place/streamplace/pkg/log" 11 ) 12 13 // it's a segment channel manager, you see ··· 41 chs = []chan *Seg{} 42 s.segChans[key] = chs 43 } 44 + ch := make(chan *Seg) 45 chs = append(chs, ch) 46 s.segChans[key] = chs 47 return ch ··· 76 } 77 for _, ch := range chs { 78 go func(ch chan *Seg) { 79 + select { 80 + case ch <- seg: 81 + case <-ctx.Done(): 82 + return 83 + case <-time.After(1 * time.Minute): 84 + log.Warn(ctx, "failed to send segment to channel, timing out", "user", user, "rendition", rendition) 85 + } 86 + 87 }(ch) 88 } 89 }
+5 -1
pkg/media/segmenter.go
··· 9 "github.com/go-gst/go-gst/gst" 10 "github.com/go-gst/go-gst/gst/app" 11 "go.opentelemetry.io/otel" 12 "stream.place/streamplace/pkg/log" 13 ) 14 ··· 62 appsink.SetCallbacks(&app.SinkCallbacks{ 63 NewSampleFunc: WriterNewSample(ctx, buf), 64 EOSFunc: func(sink *app.Sink) { 65 - ctx, span := otel.Tracer("signer").Start(ctx, "SegmentAndSignElem") 66 defer span.End() 67 resetTimer <- struct{}{} 68 now := time.Now().UnixMilli()
··· 9 "github.com/go-gst/go-gst/gst" 10 "github.com/go-gst/go-gst/gst/app" 11 "go.opentelemetry.io/otel" 12 + "go.opentelemetry.io/otel/attribute" 13 + "go.opentelemetry.io/otel/trace" 14 "stream.place/streamplace/pkg/log" 15 ) 16 ··· 64 appsink.SetCallbacks(&app.SinkCallbacks{ 65 NewSampleFunc: WriterNewSample(ctx, buf), 66 EOSFunc: func(sink *app.Sink) { 67 + ctx, span := otel.Tracer("signer").Start(ctx, "SegmentAndSignElem", trace.WithAttributes( 68 + attribute.String("streamer", ms.Streamer()), 69 + )) 70 defer span.End() 71 resetTimer <- struct{}{} 72 now := time.Now().UnixMilli()
+2 -2
pkg/media/webrtc_playback.go
··· 49 50 segBuffer := make(chan *segchanman.Seg, 1024) 51 go func() { 52 for { 53 - ch := mm.SubscribeSegment(ctx, user, rendition) 54 select { 55 case <-ctx.Done(): 56 log.Debug(ctx, "exiting segment reader") 57 - mm.UnsubscribeSegment(ctx, user, rendition, ch) 58 return 59 case file := <-ch: 60 log.Debug(ctx, "got segment", "file", file.Filepath)
··· 49 50 segBuffer := make(chan *segchanman.Seg, 1024) 51 go func() { 52 + ch := mm.SubscribeSegment(ctx, user, rendition) 53 + defer mm.UnsubscribeSegment(ctx, user, rendition, ch) 54 for { 55 select { 56 case <-ctx.Done(): 57 log.Debug(ctx, "exiting segment reader") 58 return 59 case file := <-ch: 60 log.Debug(ctx, "got segment", "file", file.Filepath)
+6
pkg/spmetrics/spmetrics.go
··· 48 Buckets: []float64{0, 250, 500, 750, 1000, 1250, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000}, 49 }, []string{"streamer"}) 50 51 var QueuedTranscodeDuration = promauto.NewGaugeVec(prometheus.GaugeOpts{ 52 Name: "streamplace_queued_transcode_duration_ms", 53 Help: "duration of transcode in ms, including time spent waiting",
··· 48 Buckets: []float64{0, 250, 500, 750, 1000, 1250, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000}, 49 }, []string{"streamer"}) 50 51 + var SigningDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 52 + Name: "streamplace_signing_duration_ms", 53 + Help: "duration of transcode in ms", 54 + Buckets: []float64{0, 250, 500, 750, 1000, 1250, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000, 20000, 30000, 60000}, 55 + }, []string{"streamer"}) 56 + 57 var QueuedTranscodeDuration = promauto.NewGaugeVec(prometheus.GaugeOpts{ 58 Name: "streamplace_queued_transcode_duration_ms", 59 Help: "duration of transcode in ms, including time spent waiting",