build 115 - кнопка в эфире будет загораться

This commit is contained in:
2026-08-26 12:59:31 +03:00
parent 077e9c496f
commit 2c9b0d4fcc
7 changed files with 357 additions and 154 deletions

View File

@@ -27,10 +27,12 @@ import (
)
const (
agentVersion = "1.4.0"
protocolVersion = 1
websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
heartbeatInterval = 8 * time.Second
agentVersion = "1.5.0"
protocolVersion = 1
websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
heartbeatInterval = 8 * time.Second
overlayPollInterval = 400 * time.Millisecond
overlayResendInterval = 2 * time.Second
)
type Config struct {
@@ -75,8 +77,22 @@ type VmixInventory struct {
}
type vmixAPIXML struct {
Version string `xml:"version"`
Inputs []vmixInputXML `xml:"inputs>input"`
Version string `xml:"version"`
Inputs []vmixInputXML `xml:"inputs>input"`
Overlays []vmixOverlayXML `xml:"overlays>overlay"`
}
type vmixOverlayXML struct {
Number string `xml:"number,attr"`
Key string `xml:"key,attr"`
Value string `xml:",chardata"`
}
type VmixOverlayTally struct {
Input string `json:"input"`
InputNumber string `json:"input_number,omitempty"`
InputKey string `json:"input_key,omitempty"`
InputTitle string `json:"input_title,omitempty"`
}
type vmixInputXML struct {
@@ -151,6 +167,7 @@ func (a *Agent) Start() {
a.logf("Agent %s started", agentVersion)
a.logf("Device: %s (%s)", a.cfg.DeviceName, short(a.cfg.DeviceID))
go a.vmixMonitor()
go a.vmixOverlayMonitor()
go a.connectionLoop()
}
@@ -295,6 +312,130 @@ func (a *Agent) vmixMonitor() {
}
}
func (a *Agent) vmixOverlayMonitor() {
ticker := time.NewTicker(overlayPollInterval)
defer ticker.Stop()
lastHash := ""
var lastWS *wsConn
lastSentAt := time.Time{}
for {
a.mu.RLock()
vmixURL := a.cfg.VmixURL
ws := a.currentWS
a.mu.RUnlock()
overlays, connected := scanVmixOverlayTally(vmixURL)
encoded, _ := json.Marshal(overlays)
stateHash := fmt.Sprintf("%t:%s", connected, string(encoded))
force := ws != nil && ws != lastWS
periodic := ws != nil && (lastSentAt.IsZero() || time.Since(lastSentAt) >= overlayResendInterval)
changed := stateHash != lastHash
if ws != nil && (force || periodic || changed) {
payload := map[string]any{
"type": "vmix.overlay_state",
"device_id": a.cfg.DeviceID,
"vmix_connected": connected,
"overlays": overlays,
"observed_at": time.Now().UTC().Format(time.RFC3339Nano),
}
if err := ws.WriteJSON(payload); err == nil {
lastHash = stateHash
lastWS = ws
lastSentAt = time.Now()
}
}
if ws == nil {
lastWS = nil
}
select {
case <-a.stop:
return
case <-ticker.C:
}
}
}
func scanVmixOverlayTally(base string) (map[string]VmixOverlayTally, bool) {
result := map[string]VmixOverlayTally{}
target := strings.TrimSpace(base)
if target == "" {
return result, false
}
resp, err := vmixHTTPClient.Get(target)
if err != nil {
return result, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return result, false
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 8*1024*1024))
if err != nil {
return result, false
}
var api vmixAPIXML
if err := xml.Unmarshal(body, &api); err != nil {
return result, true
}
byNumber := map[string]vmixInputXML{}
byKey := map[string]vmixInputXML{}
for _, input := range api.Inputs {
number := strings.TrimSpace(input.Number)
key := strings.TrimSpace(input.Key)
if number != "" {
byNumber[number] = input
}
if key != "" {
byKey[strings.ToLower(key)] = input
}
}
for _, overlay := range api.Overlays {
layer := strings.TrimSpace(overlay.Number)
if layer != "1" && layer != "2" && layer != "3" && layer != "4" {
continue
}
raw := strings.TrimSpace(overlay.Value)
rawKey := strings.TrimSpace(overlay.Key)
if raw == "" && rawKey == "" {
continue
}
var matched vmixInputXML
found := false
if raw != "" {
if item, ok := byNumber[raw]; ok {
matched, found = item, true
} else if item, ok := byKey[strings.ToLower(raw)]; ok {
matched, found = item, true
}
}
if !found && rawKey != "" {
if item, ok := byKey[strings.ToLower(rawKey)]; ok {
matched, found = item, true
}
}
entry := VmixOverlayTally{Input: raw}
if entry.Input == "" {
entry.Input = rawKey
}
if found {
entry.InputNumber = strings.TrimSpace(matched.Number)
entry.InputKey = strings.TrimSpace(matched.Key)
entry.InputTitle = strings.TrimSpace(matched.Title)
if entry.Input == "" {
entry.Input = entry.InputNumber
}
} else {
entry.InputKey = rawKey
if _, ok := byNumber[raw]; ok || raw != "" {
entry.InputNumber = raw
}
}
result[layer] = entry
}
return result, true
}
func suffixVersion(v string) string {
if v == "" {
return ""