diff --git a/field/arena.go b/field/arena.go index 051dc4ed..32dd0149 100644 --- a/field/arena.go +++ b/field/arena.go @@ -9,6 +9,7 @@ import ( "fmt" "log" "math" + "net" "reflect" "time" @@ -459,6 +460,7 @@ func (arena *Arena) AbortMatch() error { arena.AudienceDisplayModeNotifier.Notify() arena.AllianceStationDisplayMode = "logo" arena.AllianceStationDisplayModeNotifier.Notify() + arena.stopRecording() return nil } @@ -548,6 +550,7 @@ func (arena *Arena) Update() { arena.AudienceDisplayModeNotifier.Notify() arena.AllianceStationDisplayMode = "match" arena.AllianceStationDisplayModeNotifier.Notify() + arena.startRecording() if game.MatchTiming.WarmupDurationSec > 0 { arena.MatchState = WarmupPeriod enabled = false @@ -599,6 +602,7 @@ func (arena *Arena) Update() { auto = false enabled = false sendDsPacket = true + arena.stopRecording() go func() { // Leave the scores on the screen briefly at the end of the match. time.Sleep(time.Second * matchEndScoreDwellSec) @@ -1087,3 +1091,59 @@ func (arena *Arena) runPeriodicTasks() { arena.updateEarlyLateMessage() arena.purgeDisconnectedDisplays() } + +// Connect to Blackmagic device. Returns connection object. +func connectToRecorder(connString string) (net.Conn, error) { + const MAX_CONN_ATTEMPTS = 3 + const CONN_TIMEOUT = time.Millisecond * 100 + + var err error + var conn net.Conn + + for i := 0; i < MAX_CONN_ATTEMPTS; i++ { + conn, err = net.DialTimeout("tcp", connString, CONN_TIMEOUT) + if err != nil { + log.Println("CONNECTION ERROR*******", "connString:***", connString, "***error: ", err) + } else { + return conn, nil + } + } + + return nil, err +} + +// Starts recording using configured, on-network Blackmagic Device +func (arena *Arena) startRecording() { + var connections []net.Conn + + for i := 0; i < len(arena.EventSettings.RecorderAddresses); i++ { + recCon, err := connectToRecorder(arena.EventSettings.RecorderAddresses[i]) + if recCon == nil { + log.Println("recorder connection error: ", err) + } else { + connections = append(connections, recCon) + } + } + + for i := 0; i < len(connections); i++ { + fmt.Fprint(connections[i], "record\n") + } +} + +// Stops recording using configured, on-network Blackmagic Device +func (arena *Arena) stopRecording() { + var connections []net.Conn + + for i := 0; i < len(arena.EventSettings.RecorderAddresses); i++ { + recCon, err := connectToRecorder(arena.EventSettings.RecorderAddresses[i]) + if recCon == nil { + log.Println("recorder connection error: ", err) + } else { + connections = append(connections, recCon) + } + } + + for i := 0; i < len(connections); i++ { + fmt.Fprint(connections[i], "stop\n") + } +} diff --git a/model/event_settings.go b/model/event_settings.go index fd87c1ca..b0c9a7cd 100644 --- a/model/event_settings.go +++ b/model/event_settings.go @@ -44,6 +44,8 @@ type EventSettings struct { TeamSignBlue2Address string TeamSignBlue3Address string TeamSignBlueTimerAddress string + RecorderAddressesRaw string + RecorderAddresses []string WarmupDurationSec int AutoDurationSec int PauseDurationSec int diff --git a/templates/setup_settings.html b/templates/setup_settings.html index 68314713..b5e51d79 100644 --- a/templates/setup_settings.html +++ b/templates/setup_settings.html @@ -291,6 +291,19 @@ +
+ Instant Replay Match Recording +

+ If you are using a Blackmagic HyperDeck device to record matches, enter device information here. + Format entries as ip_address:port (e.g. 192.168.0.123:9993). One entry per line. +

+
+ +
+ +
+
+
Game-Specific
diff --git a/web/setup_settings.go b/web/setup_settings.go index 7673e2cb..d003fbad 100644 --- a/web/setup_settings.go +++ b/web/setup_settings.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "net/http" "os" + "regexp" "strconv" "strings" "time" @@ -94,6 +95,17 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) { eventSettings.TeamSignBlue2Address = r.PostFormValue("teamSignBlue2Address") eventSettings.TeamSignBlue3Address = r.PostFormValue("teamSignBlue3Address") eventSettings.TeamSignBlueTimerAddress = r.PostFormValue("teamSignBlueTimerAddress") + + eventSettings.RecorderAddressesRaw = r.PostFormValue("recorderAddresses") + const recAddrEntryPattern = "^(?:\\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}|[0-9])\\b(?:\r?\n|$))+$" + match, _ := regexp.MatchString(recAddrEntryPattern, eventSettings.RecorderAddressesRaw) + if match { + eventSettings.RecorderAddresses = strings.Split(strings.ReplaceAll(eventSettings.RecorderAddressesRaw, "\r", ""), "\n") + } else { + web.renderSettings(w, r, "Recorder address entry error. Please ensure all addresses entered correctly.") + return + } + eventSettings.WarmupDurationSec, _ = strconv.Atoi(r.PostFormValue("warmupDurationSec")) eventSettings.AutoDurationSec, _ = strconv.Atoi(r.PostFormValue("autoDurationSec")) eventSettings.PauseDurationSec, _ = strconv.Atoi(r.PostFormValue("pauseDurationSec"))