Skip to content

Commit

Permalink
Add streaming network visualisation
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Jun 16, 2024
1 parent 96504e2 commit 734393d
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 1 deletion.
21 changes: 21 additions & 0 deletions internal/streams/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
164 changes: 164 additions & 0 deletions internal/streams/dot.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions internal/streams/streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func Init() {
}

api.HandleFunc("api/streams", apiStreams)
api.HandleFunc("api/streams.dot", apiStreamsDOT)

if cfg.Publish == nil {
return
Expand Down
2 changes: 1 addition & 1 deletion www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
const isChecked = checkboxStates[name] ? 'checked' : '';
tr.innerHTML =
`<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&microphone">probe</a></td>` +
`<td><a href="api/streams?src=${src}">${online} / info</a> / <a href="api/streams?src=${src}&video=all&audio=all&microphone">probe</a> / <a href="network.html?src=${src}">net</a></td>` +
`<td>${links}</td>`;
}

Expand Down
1 change: 1 addition & 0 deletions www/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ body.dark-mode hr {
<li><a href="add.html">Add</a></li>
<li><a href="editor.html">Config</a></li>
<li><a href="log.html">Log</a></li>
<li><a href="network.html">Net</a></li>
<li><a href="#" id="darkModeToggle">
&#127769;
</a>
Expand Down
44 changes: 44 additions & 0 deletions www/network.html
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 734393d

Please sign in to comment.