build 115 - кнопка в эфире будет загораться
This commit is contained in:
153
agent/core.go
153
agent/core.go
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user