Tally follows MMO Stream (darkice connected) automatically
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f5c5c593f9
commit
19e5b3d898
4 files changed
+103
-23
No files matched your search
@@ -30,6 +30,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -55,6 +56,7 @@ var (
|
||||
sourceName = flag.String("source", "motu_mixminus.monitor", "PipeWire source sent to the guest")
|
||||
hostLabel = flag.String("host-label", "Host", "name the guest sees on host chat messages")
|
||||
testPlayoutFile = flag.String("test-playout-file", "", "TEST ONLY: write guest audio to this raw file instead of the sink")
|
||||
tallyAuto = flag.Bool("tally-auto", true, "set the guest's tally from the MMO Stream app: ON AIR while any darkice is connected to its server")
|
||||
stateDir = flag.String("state", filepath.Join(os.Getenv("HOME"), ".local/state/guestlink"), "state directory (invite token)")
|
||||
)
|
||||
|
||||
@@ -82,16 +84,17 @@ func (w *wsConn) send(v any) {
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
mu sync.Mutex
|
||||
token string
|
||||
guest *wsConn
|
||||
guestName string
|
||||
call *Call
|
||||
hosts map[*wsConn]bool
|
||||
chat []chatMsg
|
||||
onAir bool // tally shown on the guest page; set by the host, never automatic
|
||||
udp *net.UDPConn
|
||||
mux ice.UDPMux
|
||||
mu sync.Mutex
|
||||
token string
|
||||
guest *wsConn
|
||||
guestName string
|
||||
call *Call
|
||||
hosts map[*wsConn]bool
|
||||
chat []chatMsg
|
||||
onAir bool // tally shown on the guest page (sign only, no audio effect)
|
||||
streamLive bool // MMO Stream (darkice) currently connected, per watchStream
|
||||
udp *net.UDPConn
|
||||
mux ice.UDPMux
|
||||
}
|
||||
|
||||
var hub = &Hub{hosts: map[*wsConn]bool{}}
|
||||
@@ -131,6 +134,8 @@ func (h *Hub) stateLocked() map[string]any {
|
||||
"guestOnline": h.guest != nil,
|
||||
"guestName": h.guestName,
|
||||
"onAir": h.onAir,
|
||||
"tallyAuto": *tallyAuto,
|
||||
"streamLive": h.streamLive,
|
||||
"call": nil,
|
||||
}
|
||||
if h.call != nil {
|
||||
@@ -466,7 +471,7 @@ func hostWS(w http.ResponseWriter, r *http.Request) {
|
||||
hub.token = randToken()
|
||||
hub.chat = nil
|
||||
hub.guestName = ""
|
||||
hub.onAir = false
|
||||
hub.onAir = *tallyAuto && hub.streamLive // a new session starts matching the stream
|
||||
hub.saveToken()
|
||||
hub.mu.Unlock()
|
||||
log.Printf("new invite link created")
|
||||
@@ -476,7 +481,7 @@ func hostWS(w http.ResponseWriter, r *http.Request) {
|
||||
hub.kickGuest("The host closed this session.")
|
||||
hub.mu.Lock()
|
||||
hub.token = ""
|
||||
hub.onAir = false
|
||||
hub.onAir = *tallyAuto && hub.streamLive // a new session starts matching the stream
|
||||
hub.saveToken()
|
||||
hub.mu.Unlock()
|
||||
log.Printf("invite link revoked")
|
||||
@@ -492,15 +497,7 @@ func hostWS(w http.ResponseWriter, r *http.Request) {
|
||||
g.send(map[string]any{"t": "callended"})
|
||||
}
|
||||
case "onair":
|
||||
hub.mu.Lock()
|
||||
hub.onAir = m.On
|
||||
g := hub.guest
|
||||
hub.mu.Unlock()
|
||||
log.Printf("tally: on air %v", m.On)
|
||||
if g != nil {
|
||||
g.send(map[string]any{"t": "onair", "on": m.On})
|
||||
}
|
||||
hub.pushState()
|
||||
hub.setTally(m.On, "set by host")
|
||||
case "clearchat":
|
||||
hub.mu.Lock()
|
||||
hub.chat = nil
|
||||
@@ -515,12 +512,86 @@ func hostWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- automatic tally
|
||||
|
||||
// streamLive reports whether any darkice process (launched by mmo-stream) holds an
|
||||
// established TCP connection, i.e. a stream server accepted it. That is the same moment
|
||||
// mmo-stream shows LIVE. Read-only: it never touches mmo-stream or darkice.
|
||||
func streamLive() (bool, error) {
|
||||
out, err := exec.Command("ss", "-Htnp", "state", "established").Output()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(string(out), `(("darkice",`), nil
|
||||
}
|
||||
|
||||
// setTally sets the tally and tells the guest and the consoles.
|
||||
func (h *Hub) setTally(on bool, why string) {
|
||||
h.mu.Lock()
|
||||
changed := h.onAir != on
|
||||
h.onAir = on
|
||||
g := h.guest
|
||||
h.mu.Unlock()
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
log.Printf("tally: on air %v (%s)", on, why)
|
||||
if g != nil {
|
||||
g.send(map[string]any{"t": "onair", "on": on})
|
||||
}
|
||||
h.pushState()
|
||||
}
|
||||
|
||||
// watchStream follows the stream state. It acts only on a CHANGE of stream state, so a
|
||||
// manual click on the console sticks until the stream next connects or stops. ON after two
|
||||
// polls (a refused connection is open for a moment), OFF after 15 s down, which rides out
|
||||
// mmo-stream's 5 s reconnect.
|
||||
func watchStream() {
|
||||
const poll = 2 * time.Second
|
||||
live, upPolls, downSince := false, 0, time.Time{}
|
||||
for range time.Tick(poll) {
|
||||
up, err := streamLive()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if up {
|
||||
upPolls++
|
||||
downSince = time.Time{}
|
||||
if !live && upPolls >= 2 {
|
||||
live = true
|
||||
hub.setStreamLive(true)
|
||||
hub.setTally(true, "MMO Stream connected")
|
||||
}
|
||||
} else {
|
||||
upPolls = 0
|
||||
if downSince.IsZero() {
|
||||
downSince = time.Now()
|
||||
}
|
||||
if live && time.Since(downSince) >= 15*time.Second {
|
||||
live = false
|
||||
hub.setStreamLive(false)
|
||||
hub.setTally(false, "MMO Stream stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) setStreamLive(v bool) {
|
||||
h.mu.Lock()
|
||||
h.streamLive = v
|
||||
h.mu.Unlock()
|
||||
h.pushState()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- main
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(log.LstdFlags)
|
||||
hub.loadToken()
|
||||
if *tallyAuto {
|
||||
go watchStream()
|
||||
}
|
||||
|
||||
udp, err := net.ListenUDP("udp4", &net.UDPAddr{Port: *udpPort})
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user