package main import ( "embed" "fmt" "html/template" "net" "net/http" "strings" "sync" "sync/atomic" "time" ) //go:embed template.html var embedFS embed.FS var htmlTemplate *template.Template type clientState int const ( INITIAL = iota ACTIVE ) type client struct { channel chan string state clientState } var ( clients = make([]*client, 0) clientMutex sync.Mutex ) func deleteClient(client *client) { for i := 0; i < len(clients); i++ { if clients[i] == client { clients[i] = clients[len(clients)-1] clients = clients[:len(clients)-1] return } } } func httpServer() error { htmlTemplate = template.Must(template.ParseFS(embedFS, "template.html")) http.HandleFunc("/stream", stream) http.HandleFunc("/", serveRoot) return http.ListenAndServe(":9090", nil) } func getInterfaceBaseIP() string { iFace, err := net.InterfaceByName(interfaceName) if err != nil { return "" } addresses, err := iFace.Addrs() if err != nil { return "" } gua := "" ula := "" for _, v := range addresses { addr := v.String() if !strings.Contains(addr, ":") { continue } _, anet, err := net.ParseCIDR(addr) if err != nil { continue } if anet.IP.IsLinkLocalUnicast() { continue } if anet.IP.IsGlobalUnicast() { gua = strings.Split(anet.String(), "/")[0] } if anet.IP.IsPrivate() { ula = strings.Split(anet.String(), "/")[0] } } if gua != "" { return gua } else { return ula } } func serveRoot(w http.ResponseWriter, r *http.Request) { if r.RequestURI != "/" { http.Error(w, "Not found", http.StatusNotFound) return } type pageData struct { BaseIP string CanvasWidth int CanvasHeight int } baseIP := getInterfaceBaseIP() if len(baseIP) == 21 { baseIP = strings.TrimSuffix(baseIP, ":") } err := htmlTemplate.Execute(w, pageData{ BaseIP: baseIP, CanvasHeight: 512, CanvasWidth: 512, }) if err != nil { fmt.Println("Error executing HTML template:", err) } } var streamServerRunning atomic.Bool func streamServer() { if !streamServerRunning.CompareAndSwap(false, true) { return } go func() { for { clientMutex.Lock() if len(clients) == 0 { streamServerRunning.Store(false) clientMutex.Unlock() return } requiresInitial := false requiresUpdate := false for _, v := range clients { if v.state == INITIAL { requiresInitial = true } else { requiresUpdate = true } if requiresInitial && requiresUpdate { break } } dataInitial, dataUpdate := getPicture(requiresInitial, requiresUpdate) tmp := clients[:0] for _, v := range clients { if v.state == INITIAL { v.state = ACTIVE select { case v.channel <- dataInitial: default: } } else { if dataUpdate != "0" { select { case v.channel <- dataUpdate: default: // Client cannot keep up close(v.channel) continue } } } tmp = append(tmp, v) } clients = tmp clientMutex.Unlock() time.Sleep(500 * time.Millisecond) } }() } func stream(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) return } streamServer() w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") messageChan := make(chan string, 40) newClient := &client{ channel: messageChan, state: INITIAL, } clientMutex.Lock() clients = append(clients, newClient) clientMutex.Unlock() // For when clients are removed prior to connection closed, to avoid a call to delete(clients, id) var channelClosedFirst = false go func() { // Listen for connection close <-r.Context().Done() clientMutex.Lock() if !channelClosedFirst { deleteClient(newClient) } close(messageChan) clientMutex.Unlock() }() for { data, ok := <-messageChan if !ok { channelClosedFirst = true return } _, _ = w.Write([]byte(data)) flusher.Flush() } }