Add streaming network visualisation
This commit is contained in:
@@ -101,3 +101,24 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
|
||||||
|
dot := make([]byte, 0, 1024)
|
||||||
|
dot = append(dot, "digraph {\n"...)
|
||||||
|
if query.Has("src") {
|
||||||
|
for _, name := range query["src"] {
|
||||||
|
if stream := streams[name]; stream != nil {
|
||||||
|
dot = AppendDOT(dot, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, stream := range streams {
|
||||||
|
dot = AppendDOT(dot, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dot = append(dot, '}')
|
||||||
|
|
||||||
|
api.Response(w, dot, "text/vnd.graphviz")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AppendDOT(dot []byte, stream *Stream) []byte {
|
||||||
|
for _, prod := range stream.producers {
|
||||||
|
if prod.conn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, err := marshalConn(prod.conn)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "producer")
|
||||||
|
}
|
||||||
|
for _, cons := range stream.consumers {
|
||||||
|
c, err := marshalConn(cons)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dot = c.appendDOT(dot, "consumer")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalConn(v any) (*conn, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var c conn
|
||||||
|
if err = json.Unmarshal(b, &c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytesK = "KMGTP"
|
||||||
|
|
||||||
|
func humanBytes(i int) string {
|
||||||
|
if i < 1000 {
|
||||||
|
return fmt.Sprintf("%d B", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := float64(i) / 1000
|
||||||
|
var n uint8
|
||||||
|
for f >= 1000 && n < 5 {
|
||||||
|
f /= 1000
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Codec map[string]any `json:"codec"`
|
||||||
|
Parent uint32 `json:"parent"`
|
||||||
|
Childs []uint32 `json:"childs"`
|
||||||
|
Bytes int `json:"bytes"`
|
||||||
|
//Packets uint32 `json:"packets"`
|
||||||
|
//Drops uint32 `json:"drops"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
|
||||||
|
|
||||||
|
func (n *node) codec() []byte {
|
||||||
|
b := make([]byte, 0, 128)
|
||||||
|
for _, k := range codecKeys {
|
||||||
|
if v := n.Codec[k]; v != nil {
|
||||||
|
b = fmt.Appendf(b, "%s=%v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[:len(b)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) appendDOT(dot []byte, group string) []byte {
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.Codec["codec_name"], n.codec())
|
||||||
|
//for _, sink := range n.Childs {
|
||||||
|
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
|
||||||
|
//}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
type conn struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
FormatName string `json:"format_name"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Receivers []node `json:"receivers"`
|
||||||
|
Senders []node `json:"senders"`
|
||||||
|
BytesRecv int `json:"bytes_recv"`
|
||||||
|
BytesSend int `json:"bytes_send"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||||
|
host := c.host()
|
||||||
|
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||||
|
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
||||||
|
if group == "producer" {
|
||||||
|
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||||
|
} else {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, recv := range c.Receivers {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
|
||||||
|
dot = recv.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
for _, send := range c.Senders {
|
||||||
|
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
|
||||||
|
//dot = send.appendDOT(dot, "node")
|
||||||
|
}
|
||||||
|
return dot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) host() (s string) {
|
||||||
|
if c.Protocol == "pipe" {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if s = c.RemoteAddr; s == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(s, "forwarded"); i > 0 {
|
||||||
|
s = s[i+10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s[0] == '[' {
|
||||||
|
if i := strings.Index(s, "]"); i > 0 {
|
||||||
|
return s[1:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.IndexAny(s, " ,:"); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) label() (s string) {
|
||||||
|
s = "format_name=" + c.FormatName
|
||||||
|
if c.Protocol != "" {
|
||||||
|
s += "\nprotocol=" + c.Protocol
|
||||||
|
}
|
||||||
|
if c.Source != "" {
|
||||||
|
s += "\nsource=" + c.Source
|
||||||
|
}
|
||||||
|
if c.URL != "" {
|
||||||
|
s += "\nurl=" + c.URL
|
||||||
|
}
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
s += "\nuser_agent=" + c.UserAgent
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.HandleFunc("api/streams", apiStreams)
|
api.HandleFunc("api/streams", apiStreams)
|
||||||
|
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||||
|
|
||||||
if cfg.Publish == nil {
|
if cfg.Publish == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
+1
-1
@@ -139,7 +139,7 @@
|
|||||||
const isChecked = checkboxStates[name] ? 'checked' : '';
|
const isChecked = checkboxStates[name] ? 'checked' : '';
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
`<td><label><input type="checkbox" name="${name}" ${isChecked}>${name}</label></td>` +
|
`<td><label><input type="checkbox" name="${name}" ${isChecked}>${name}</label></td>` +
|
||||||
`<td><a href="api/streams?src=${src}">${online} / info</a> / <a href="api/streams?src=${src}&video=all&audio=allµphone">probe</a></td>` +
|
`<td><a href="api/streams?src=${src}">${online} / info</a> / <a href="api/streams?src=${src}&video=all&audio=allµphone">probe</a> / <a href="network.html?src=${src}">net</a></td>` +
|
||||||
`<td>${links}</td>`;
|
`<td>${links}</td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ body.dark-mode hr {
|
|||||||
<li><a href="add.html">Add</a></li>
|
<li><a href="add.html">Add</a></li>
|
||||||
<li><a href="editor.html">Config</a></li>
|
<li><a href="editor.html">Config</a></li>
|
||||||
<li><a href="log.html">Log</a></li>
|
<li><a href="log.html">Log</a></li>
|
||||||
|
<li><a href="network.html">Net</a></li>
|
||||||
<li><a href="#" id="darkModeToggle">
|
<li><a href="#" id="darkModeToggle">
|
||||||
🌙
|
🌙
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>go2rtc - Network</title>
|
||||||
|
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background-color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #network {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
<div id="network"></div>
|
||||||
|
<script>
|
||||||
|
/* global vis */
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
const url = new URL('api/streams.dot' + location.search, location.href);
|
||||||
|
const r = await fetch(url, {cache: 'no-cache'});
|
||||||
|
const data = vis.parseDOTNetwork(await r.text());
|
||||||
|
const options = {
|
||||||
|
edges: {
|
||||||
|
font: {align: 'middle'},
|
||||||
|
smooth: false,
|
||||||
|
},
|
||||||
|
nodes: {shape: 'box'},
|
||||||
|
physics: false,
|
||||||
|
};
|
||||||
|
new vis.Network(document.getElementById('network'), data, options);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user