add blackmagic atem discovery via udp 9910

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-23 07:18:14 -08:00
parent c2318279bf
commit c7db6c0c68
3 changed files with 120 additions and 0 deletions

111
bmd.go Normal file
View File

@@ -0,0 +1,111 @@
package tendrils
import (
"context"
"log"
"net"
"time"
)
func (t *Tendrils) listenBMD(ctx context.Context, iface net.Interface) {
addrs, err := iface.Addrs()
if err != nil {
return
}
var srcIP net.IP
var broadcast net.IP
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
srcIP = ipnet.IP.To4()
mask := ipnet.Mask
broadcast = make(net.IP, 4)
for i := 0; i < 4; i++ {
broadcast[i] = srcIP[i] | ^mask[i]
}
break
}
}
if srcIP == nil {
return
}
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0})
if err != nil {
return
}
defer conn.Close()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
t.sendATEMDiscovery(conn, broadcast, iface.Name)
go t.receiveATEMResponses(ctx, conn, iface.Name)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
t.sendATEMDiscovery(conn, broadcast, iface.Name)
}
}
}
func (t *Tendrils) sendATEMDiscovery(conn *net.UDPConn, broadcast net.IP, ifaceName string) {
// ATEM protocol hello packet
// Flag 0x10 (hello), length 20 bytes
packet := []byte{
0x10, 0x14, // Flags (0x10 = hello) + length (20 = 0x14)
0x00, 0x00, // Session ID
0x00, 0x00, // Ack number
0x00, 0x00, // Local sequence
0x00, 0x00, // Remote sequence
0x00, 0x00, // Unknown
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client hello data
}
conn.WriteToUDP(packet, &net.UDPAddr{IP: broadcast, Port: 9910})
if t.DebugBMD {
log.Printf("[bmd] %s: sent atem discovery to %s", ifaceName, broadcast)
}
}
func (t *Tendrils) receiveATEMResponses(ctx context.Context, conn *net.UDPConn, ifaceName string) {
seen := map[string]bool{}
buf := make([]byte, 2048)
for {
select {
case <-ctx.Done():
return
default:
}
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, src, err := conn.ReadFromUDP(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
continue
}
if n < 12 {
continue
}
ipKey := src.IP.String()
if seen[ipKey] {
continue
}
seen[ipKey] = true
if t.DebugBMD {
log.Printf("[bmd] %s: atem at %s", ifaceName, src.IP)
}
t.nodes.Update(nil, nil, []net.IP{src.IP}, "", "atem", "bmd")
}
}

View File

@@ -15,6 +15,7 @@ func main() {
noMDNS := flag.Bool("no-mdns", false, "disable mDNS discovery") noMDNS := flag.Bool("no-mdns", false, "disable mDNS discovery")
noArtNet := flag.Bool("no-artnet", false, "disable Art-Net discovery") noArtNet := flag.Bool("no-artnet", false, "disable Art-Net discovery")
noDante := flag.Bool("no-dante", false, "disable Dante discovery") noDante := flag.Bool("no-dante", false, "disable Dante discovery")
noBMD := flag.Bool("no-bmd", false, "disable Blackmagic discovery")
logEvents := flag.Bool("log-events", false, "log node events") logEvents := flag.Bool("log-events", false, "log node events")
logNodes := flag.Bool("log-nodes", false, "log full node details on changes") logNodes := flag.Bool("log-nodes", false, "log full node details on changes")
debugARP := flag.Bool("debug-arp", false, "debug ARP discovery") debugARP := flag.Bool("debug-arp", false, "debug ARP discovery")
@@ -24,6 +25,7 @@ func main() {
debugMDNS := flag.Bool("debug-mdns", false, "debug mDNS discovery") debugMDNS := flag.Bool("debug-mdns", false, "debug mDNS discovery")
debugArtNet := flag.Bool("debug-artnet", false, "debug Art-Net discovery") debugArtNet := flag.Bool("debug-artnet", false, "debug Art-Net discovery")
debugDante := flag.Bool("debug-dante", false, "debug Dante discovery") debugDante := flag.Bool("debug-dante", false, "debug Dante discovery")
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
flag.Parse() flag.Parse()
t := tendrils.New() t := tendrils.New()
@@ -35,6 +37,7 @@ func main() {
t.DisableMDNS = *noMDNS t.DisableMDNS = *noMDNS
t.DisableArtNet = *noArtNet t.DisableArtNet = *noArtNet
t.DisableDante = *noDante t.DisableDante = *noDante
t.DisableBMD = *noBMD
t.LogEvents = *logEvents t.LogEvents = *logEvents
t.LogNodes = *logNodes t.LogNodes = *logNodes
t.DebugARP = *debugARP t.DebugARP = *debugARP
@@ -44,5 +47,6 @@ func main() {
t.DebugMDNS = *debugMDNS t.DebugMDNS = *debugMDNS
t.DebugArtNet = *debugArtNet t.DebugArtNet = *debugArtNet
t.DebugDante = *debugDante t.DebugDante = *debugDante
t.DebugBMD = *debugBMD
t.Run() t.Run()
} }

View File

@@ -23,6 +23,7 @@ type Tendrils struct {
DisableMDNS bool DisableMDNS bool
DisableArtNet bool DisableArtNet bool
DisableDante bool DisableDante bool
DisableBMD bool
LogEvents bool LogEvents bool
LogNodes bool LogNodes bool
DebugARP bool DebugARP bool
@@ -32,6 +33,7 @@ type Tendrils struct {
DebugMDNS bool DebugMDNS bool
DebugArtNet bool DebugArtNet bool
DebugDante bool DebugDante bool
DebugBMD bool
} }
func New() *Tendrils { func New() *Tendrils {
@@ -200,4 +202,7 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) {
if !t.DisableDante { if !t.DisableDante {
go t.listenDante(ctx, iface) go t.listenDante(ctx, iface)
} }
if !t.DisableBMD {
go t.listenBMD(ctx, iface)
}
} }