-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/http maintainance #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,134 @@ package main | |
|
|
||
| import ( | ||
| "fmt" | ||
| "html/template" | ||
| "net/http" | ||
|
|
||
| "github.com/go-gst/go-gst/gst" | ||
| ) | ||
|
|
||
| type httpServer struct { | ||
| daemonController | ||
| combPort string | ||
| presPort string | ||
| camPort string | ||
|
Comment on lines
+13
to
+15
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can retrieve the values from the |
||
| lb *logBuffer | ||
| } | ||
|
|
||
| type indexData struct { | ||
| Warnings uint64 | ||
| QosEvents map[string]uint64 | ||
| CompCallers int | ||
| PresentCallers int | ||
| CamCallers int | ||
| LoadOne float64 | ||
| LoadFive float64 | ||
| LoadFifteen float64 | ||
| MemUsedMB int64 | ||
| MemFreeMB int64 | ||
| CompPort string | ||
| PresentPort string | ||
| CamPort string | ||
| } | ||
|
|
||
| var indexTmpl = template.Must(template.New("index").Parse(`<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>streamd</title> | ||
| <style> | ||
| * { box-sizing: border-box; } | ||
| body { font-family: monospace; max-width: 860px; margin: 2em auto; padding: 0 1em; color: #222; } | ||
| h1 { margin-bottom: 0.2em; } | ||
| h2 { font-size: 1em; margin: 1.4em 0 0.4em; text-transform: uppercase; letter-spacing: 0.05em; color: #555; } | ||
| table { border-collapse: collapse; width: 100%; margin-bottom: 0.5em; } | ||
| th, td { padding: 5px 10px; text-align: left; border: 1px solid #ddd; } | ||
| th { background: #f5f5f5; font-weight: normal; width: 40%; } | ||
| .ok { color: #2a2; } | ||
| .warn { color: #c80; } | ||
| .nav { margin-top: 2em; } | ||
| .nav a { margin-right: 1em; } | ||
| button { padding: 6px 16px; cursor: pointer; font-family: monospace; } | ||
| .danger { background: #fee; border: 1px solid #c00; color: #c00; } | ||
| .danger:hover { background: #c00; color: #fff; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>streamd</h1> | ||
|
|
||
| <h2>Pipeline</h2> | ||
| <table> | ||
| <tr><th>State</th><td><span class="ok">● running</span></td></tr> | ||
| <tr><th>Warnings</th><td{{if .Warnings}} class="warn"{{end}}>{{.Warnings}}</td></tr> | ||
| {{if .QosEvents -}} | ||
| <tr><th>QoS events</th><td class="warn">{{range $k, $v := .QosEvents}}{{$k}}: {{$v}}<br>{{end}}</td></tr> | ||
| {{- end}} | ||
| </table> | ||
|
|
||
| <h2>SRT Sinks</h2> | ||
| <table> | ||
| <tr><th>Combined (port {{.CompPort}})</th><td>{{.CompCallers}} caller(s)</td></tr> | ||
| <tr><th>Presentation (port {{.PresentPort}})</th><td>{{.PresentCallers}} caller(s)</td></tr> | ||
| <tr><th>Camera (port {{.CamPort}})</th><td>{{.CamCallers}} caller(s)</td></tr> | ||
| </table> | ||
|
|
||
| <h2>System</h2> | ||
| <table> | ||
| <tr><th>Load average (1/5/15 min)</th><td>{{printf "%.2f" .LoadOne}} / {{printf "%.2f" .LoadFive}} / {{printf "%.2f" .LoadFifteen}}</td></tr> | ||
| <tr><th>Memory used</th><td>{{.MemUsedMB}} MB</td></tr> | ||
| <tr><th>Memory available</th><td>{{.MemFreeMB}} MB</td></tr> | ||
| </table> | ||
|
|
||
| <h2>Actions</h2> | ||
| <form method="POST" action="/restart" onsubmit="return confirm('Restart the pipeline?')"> | ||
| <button type="submit" class="danger">Restart pipeline</button> | ||
| </form> | ||
|
|
||
| <div class="nav"> | ||
| <a href="/logs">/logs</a> | ||
| <a href="/metrics">/metrics</a> | ||
| <a href="/graph?details=states">/graph</a> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| `)) | ||
|
|
||
| func (h *httpServer) handleIndex(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path != "/" { | ||
| http.NotFound(w, r) | ||
| return | ||
| } | ||
| m := h.metricsSnapshot() | ||
| data := indexData{ | ||
| Warnings: m.pipelineStats.warnings, | ||
| QosEvents: m.pipelineStats.qosEvents, | ||
| CompCallers: len(m.compSinkStats.callers), | ||
| PresentCallers: len(m.presentSinkStats.callers), | ||
| CamCallers: len(m.camSinkStats.callers), | ||
| LoadOne: m.loadAvg.One, | ||
| LoadFive: m.loadAvg.Five, | ||
| LoadFifteen: m.loadAvg.Fifteen, | ||
| MemUsedMB: int64(m.mem.MemTotal-m.mem.MemFree-m.mem.Buffers-m.mem.Cached) / 1024, | ||
| MemFreeMB: int64(m.mem.MemFree+m.mem.Buffers+m.mem.Cached) / 1024, | ||
| CompPort: h.combPort, | ||
| PresentPort: h.presPort, | ||
| CamPort: h.camPort, | ||
| } | ||
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| indexTmpl.Execute(w, data) | ||
| } | ||
|
|
||
| func (h *httpServer) handleRestart(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost { | ||
| http.Error(w, "method not allowed", http.StatusMethodNotAllowed) | ||
| return | ||
| } | ||
| if err := h.daemonController.restart(); err != nil { | ||
| http.Error(w, fmt.Sprintf("restart failed: %v", err), http.StatusInternalServerError) | ||
| return | ||
| } | ||
| http.Redirect(w, r, "/", http.StatusSeeOther) | ||
| } | ||
|
|
||
| func writeSRTStatsMeta(w http.ResponseWriter) { | ||
|
|
@@ -225,6 +346,9 @@ func (h *httpServer) graph(w http.ResponseWriter, r *http.Request) { | |
| } | ||
|
|
||
| func (h *httpServer) setupHTTPHandlers() { | ||
| http.HandleFunc("/", h.handleIndex) | ||
| http.HandleFunc("/logs", h.handleLogs) | ||
| http.HandleFunc("/metrics", h.metrics) | ||
| http.HandleFunc("/graph", h.graph) | ||
| http.HandleFunc("/restart", h.handleRestart) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "strings" | ||
| "sync" | ||
| ) | ||
|
|
||
| const logBufferSize = 10000 | ||
|
|
||
| type logBuffer struct { | ||
| mu sync.Mutex | ||
| lines []string | ||
| pos int | ||
| full bool | ||
| } | ||
|
|
||
| func newLogBuffer() *logBuffer { | ||
| return &logBuffer{lines: make([]string, logBufferSize)} | ||
| } | ||
|
|
||
| // Write implements io.Writer so logBuffer can be passed to klog.SetOutput. | ||
| // klog writes one complete formatted log line per Write call. | ||
| func (lb *logBuffer) Write(p []byte) (n int, err error) { | ||
| s := string(p) | ||
| if len(s) > 0 && s[len(s)-1] == '\n' { | ||
| s = s[:len(s)-1] | ||
| } | ||
| if s == "" { | ||
| return len(p), nil | ||
| } | ||
| lb.mu.Lock() | ||
| lb.lines[lb.pos] = s | ||
| lb.pos = (lb.pos + 1) % logBufferSize | ||
| if lb.pos == 0 { | ||
| lb.full = true | ||
| } | ||
| lb.mu.Unlock() | ||
| return len(p), nil | ||
| } | ||
|
|
||
| // snapshot returns buffered lines in chronological order. | ||
| func (lb *logBuffer) snapshot() []string { | ||
| lb.mu.Lock() | ||
| defer lb.mu.Unlock() | ||
| if !lb.full { | ||
| out := make([]string, lb.pos) | ||
| copy(out, lb.lines[:lb.pos]) | ||
| return out | ||
| } | ||
| out := make([]string, logBufferSize) | ||
| copy(out, lb.lines[lb.pos:]) | ||
| copy(out[logBufferSize-lb.pos:], lb.lines[:lb.pos]) | ||
| return out | ||
| } | ||
|
|
||
| func (h *httpServer) handleLogs(w http.ResponseWriter, r *http.Request) { | ||
| lines := h.lb.snapshot() | ||
| w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||
| w.Write([]byte(strings.Join(lines, "\n"))) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |||
| "context" | ||||
| "flag" | ||||
| "fmt" | ||||
| "io" | ||||
| "net" | ||||
| "net/http" | ||||
| "os" | ||||
|
|
@@ -75,6 +76,7 @@ type daemonController interface { | |||
| metricsSnapshot() metrics | ||||
| graph(details gst.DebugGraphDetails) string | ||||
| srtStatistics() ([]*srtStats, error) | ||||
| restart() error | ||||
| } | ||||
|
|
||||
| func (d *daemon) srtStatistics() ([]*srtStats, error) { | ||||
|
|
@@ -136,6 +138,28 @@ func (d *daemon) runPipeline() error { | |||
| return nil | ||||
| } | ||||
|
|
||||
| func (d *daemon) restart() error { | ||||
| d.mu.Lock() | ||||
| oldGstPipeline := d.pipeline.pipeline | ||||
| d.mu.Unlock() | ||||
|
|
||||
| oldGstPipeline.BlockSetState(gst.StateNull) | ||||
|
|
||||
| newP, err := newPipeline(&d.daemonConfig) | ||||
| if err != nil { | ||||
| return err | ||||
| } | ||||
|
|
||||
| d.mu.Lock() | ||||
| d.pipeline = newP | ||||
| d.metrics.pipelineStats = newPipelineStats() | ||||
| d.mu.Unlock() | ||||
|
|
||||
| d.registerBusWatch() | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before creating a new pipeline, I think you'll need to unregister the bus with LectureMix/streamd/gstreamer_bus.go Line 25 in d210cde
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tested, whether restarting of the pipeline works, and that all objects are gc'd eventually?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My sanity check wasn't very thorough, I did it with a local test source which worked well. |
||||
| newP.pipeline.SetState(gst.StatePlaying) | ||||
| return nil | ||||
|
Comment on lines
+148
to
+160
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not call |
||||
| } | ||||
|
|
||||
| func main() { | ||||
| d := &daemon{} | ||||
|
|
||||
|
|
@@ -154,6 +178,7 @@ func main() { | |||
| flag.IntVar(&d.audioEncBitrateKbps, "audio-enc-bitrate", 96, "Video encoding bitrate in Kbps") | ||||
| flag.Float64Var(&d.audioAmplification, "audio-amplification", 1.0, "Audio amplifcation after conversion") | ||||
| flag.BoolVar(&d.hwAccel, "hw-accel", false, "Enable hardware acceleration and offload processing tasks onto the GPU or a DSP") | ||||
| klog.InitFlags(nil) // register klog flags with flag.CommandLine before parsing | ||||
| flag.Parse() | ||||
|
|
||||
| if d.listenCidr != "" { | ||||
|
|
@@ -173,11 +198,24 @@ func main() { | |||
| d.listenAddr = "[::]" | ||||
| } | ||||
|
|
||||
| lb := newLogBuffer() | ||||
| // klog defaults to logtostderr=true, which writes directly to os.Stderr | ||||
| // and bypasses the file sinks that SetOutput replaces. Disable it so all | ||||
| // log lines go through our MultiWriter (which still writes to os.Stderr). | ||||
| flag.Set("logtostderr", "false") | ||||
| klog.SetOutput(io.MultiWriter(os.Stderr, lb)) | ||||
|
|
||||
| d.mainloop = glib.NewMainLoop(glib.MainContextDefault(), false) | ||||
| ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) | ||||
|
|
||||
| // Create and start HTTP server | ||||
| h := &httpServer{d} | ||||
| h := &httpServer{ | ||||
| daemonController: d, | ||||
| combPort: d.combPort, | ||||
| presPort: d.presPort, | ||||
| camPort: d.camPort, | ||||
| lb: lb, | ||||
| } | ||||
| h.setupHTTPHandlers() | ||||
|
|
||||
| klog.Infof("listening for HTTP at %s:%s", d.listenAddr, d.listenHTTP) | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.