🎉 live server seems to be working now
This commit is contained in:
150
third_party/sendspin-go/pkg/sendspin/role_metadata.go
vendored
Normal file
150
third_party/sendspin-go/pkg/sendspin/role_metadata.go
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
// ABOUTME: MetadataGroupRole pushes track metadata to joining clients
|
||||
// ABOUTME: Sends server/state with metadata snapshot on join, transition, or on demand
|
||||
package sendspin
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/protocol"
|
||||
)
|
||||
|
||||
// MetadataConfig configures the MetadataGroupRole.
|
||||
type MetadataConfig struct {
|
||||
// GetMetadata returns the current track metadata (title, artist, album).
|
||||
GetMetadata func() (title, artist, album string)
|
||||
|
||||
// ClockMicros returns the server's current clock in microseconds.
|
||||
ClockMicros func() int64
|
||||
}
|
||||
|
||||
// MetadataGroupRole handles the "metadata" role family. It pushes a
|
||||
// server/state message with the current track metadata to clients
|
||||
// that advertise the metadata role: on join, on stopped→playing
|
||||
// transitions, and on demand via BroadcastMetadata.
|
||||
type MetadataGroupRole struct {
|
||||
config MetadataConfig
|
||||
|
||||
mu sync.RWMutex
|
||||
group *Group
|
||||
}
|
||||
|
||||
// NewMetadataRole creates a MetadataGroupRole with the given config.
|
||||
// The role must be registered on a Group via Group.RegisterRole; the
|
||||
// group reference is captured so OnPlaybackStateChanged and
|
||||
// BroadcastMetadata can iterate the group's attached clients.
|
||||
func NewMetadataRole(config MetadataConfig) *MetadataGroupRole {
|
||||
return &MetadataGroupRole{config: config}
|
||||
}
|
||||
|
||||
// attachToGroup is called by Group.RegisterRole to give the role a
|
||||
// back-reference for client iteration. Unexported because role authors
|
||||
// must register via Group.RegisterRole rather than calling this directly.
|
||||
func (r *MetadataGroupRole) attachToGroup(g *Group) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.group = g
|
||||
}
|
||||
|
||||
// attachedGroup returns the group this role was registered on, or nil
|
||||
// if it has not yet been registered.
|
||||
func (r *MetadataGroupRole) attachedGroup() *Group {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.group
|
||||
}
|
||||
|
||||
func (r *MetadataGroupRole) Role() string { return "metadata" }
|
||||
|
||||
// snapshotMessage builds the current server/state metadata snapshot.
|
||||
// Returns nil if no GetMetadata callback is configured.
|
||||
func (r *MetadataGroupRole) snapshotMessage() *protocol.ServerStateMessage {
|
||||
if r.config.GetMetadata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
title, artist, album := r.config.GetMetadata()
|
||||
|
||||
var clockMicros int64
|
||||
if r.config.ClockMicros != nil {
|
||||
clockMicros = r.config.ClockMicros()
|
||||
}
|
||||
|
||||
return &protocol.ServerStateMessage{
|
||||
Metadata: &protocol.MetadataState{
|
||||
Timestamp: clockMicros,
|
||||
Title: strPtr(title),
|
||||
Artist: strPtr(artist),
|
||||
Album: strPtr(album),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OnClientJoin sends the current metadata snapshot to clients that
|
||||
// advertise the metadata role.
|
||||
func (r *MetadataGroupRole) OnClientJoin(c *ServerClient) {
|
||||
if !c.HasRole("metadata") {
|
||||
return
|
||||
}
|
||||
|
||||
state := r.snapshotMessage()
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.Send("server/state", *state); err != nil {
|
||||
log.Printf("MetadataRole: failed to send state to %s: %v", c.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MetadataGroupRole) OnClientLeave(id string, name string) {}
|
||||
|
||||
// OnPlaybackStateChanged broadcasts the current metadata snapshot to
|
||||
// every attached metadata-role client when the group transitions to
|
||||
// "playing" from a non-playing state. This closes the gap where a
|
||||
// client connects while the group is stopped (and thus receives only
|
||||
// the empty-state on join), then never sees metadata when playback
|
||||
// subsequently begins.
|
||||
//
|
||||
// Other transitions (playing→paused, playing→stopped) are intentionally
|
||||
// not re-broadcast — the existing client state is already correct for
|
||||
// "the same track is paused/stopped".
|
||||
func (r *MetadataGroupRole) OnPlaybackStateChanged(oldState, newState string) {
|
||||
if newState != "playing" || oldState == "playing" {
|
||||
return
|
||||
}
|
||||
r.broadcast()
|
||||
}
|
||||
|
||||
// BroadcastMetadata pushes the current metadata snapshot to every
|
||||
// attached metadata-role client. Use this when the server's metadata
|
||||
// source has mutated and connected clients need to see the new value
|
||||
// (e.g., HLS playlist advance, controller-driven track change).
|
||||
//
|
||||
// Safe to call concurrently with the group's own event dispatch — uses
|
||||
// Group.Clients which returns a snapshot copy.
|
||||
func (r *MetadataGroupRole) BroadcastMetadata() {
|
||||
r.broadcast()
|
||||
}
|
||||
|
||||
// broadcast sends the snapshot to every metadata-role client in the
|
||||
// attached group. Logs and continues on per-client send errors so one
|
||||
// dropped client does not stall the rest.
|
||||
func (r *MetadataGroupRole) broadcast() {
|
||||
g := r.attachedGroup()
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
state := r.snapshotMessage()
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
for _, c := range g.Clients() {
|
||||
if !c.HasRole("metadata") {
|
||||
continue
|
||||
}
|
||||
if err := c.Send("server/state", *state); err != nil {
|
||||
log.Printf("MetadataRole: broadcast to %s failed: %v", c.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user