Initial upload

This commit is contained in:
MassiveBox 2025-05-29 22:52:44 +02:00
commit cefd7abe8a
Signed by: massivebox
GPG key ID: 9B74D3A59181947D
19 changed files with 1027 additions and 0 deletions

78
fcast/connection.go Normal file
View file

@ -0,0 +1,78 @@
package fcast
import (
"encoding/binary"
"encoding/json"
"errors"
"log"
"net"
"strconv"
)
type Connection struct {
net.Conn
}
func Connect(host string) (*Connection, error) {
conn, err := net.Dial("tcp", host+":"+strconv.Itoa(DefaultPort))
return &Connection{conn}, err
}
func (c *Connection) ListenForMessages(em *EventManager) error {
c.addPingHandlerIfNotSet(em)
for {
var (
bodyLenBuf = make([]byte, 4)
opCodeBuf [1]byte
// bodyBuf's size depends on bodyLen's value
)
_, err := c.Read(bodyLenBuf)
bodyBuf := make([]byte, binary.LittleEndian.Uint32(bodyLenBuf)-1)
_, err = c.Read(opCodeBuf[:])
_, err = c.Read(bodyBuf)
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
rawMsg, err := DeserializeRaw(bodyLenBuf, opCodeBuf[:], bodyBuf)
if err != nil {
return err
}
err = em.dispatchMessage(rawMsg)
if err != nil {
return err
}
}
return nil
}
func (c *Connection) addPingHandlerIfNotSet(em *EventManager) {
if em.codes[Ping].handler == nil {
em.SetHandler(PingMessage{}, func(message Message) {
err := c.SendMessage(PongMessage{})
if err != nil {
log.Println("[ERROR} fcast: error in default pong responder:", err)
}
})
}
}
func (c *Connection) SendMessage(message Message) error {
body, _ := json.Marshal(message)
rawMessage := NewMessage(message.getOpCode(), body)
serialized := SerializeRaw(rawMessage)
_, err := c.Write(serialized)
return err
}

57
fcast/discovery.go Normal file
View file

@ -0,0 +1,57 @@
package fcast
import (
"github.com/hashicorp/mdns"
"log"
)
type DiscoveredHost struct {
Name string
IPv4 string
IPv6 string
}
func Discover() ([]DiscoveredHost, error) {
entriesCh := make(chan *mdns.ServiceEntry)
var discoveredHosts []DiscoveredHost
go func() {
for entry := range entriesCh {
if !validateEntry(entry) {
continue
}
discoveredHosts = append(discoveredHosts, DiscoveredHost{
Name: entry.Name,
IPv4: entry.AddrV4.String(),
IPv6: entry.AddrV6.String(),
})
}
}()
done := make(chan struct{})
go func() {
err := mdns.Lookup("_fcast._tcp", entriesCh)
if err != nil {
log.Println("[ERROR] mdns: lookup error:", err)
}
close(entriesCh)
close(done)
}()
select {
case <-done:
return discoveredHosts, nil
}
}
func validateEntry(entry *mdns.ServiceEntry) bool {
if entry.Port != DefaultPort {
return false
}
return true
}

56
fcast/events.go Normal file
View file

@ -0,0 +1,56 @@
package fcast
import (
"encoding/json"
"log"
"reflect"
"sync"
)
type EventHandler func(message Message)
type CodeRegistry struct {
handler EventHandler
constructor func() Message
}
type EventManager struct {
codes map[OpCode]CodeRegistry
mu sync.RWMutex
}
func (em *EventManager) SetHandler(message Message, handler EventHandler) {
em.mu.Lock()
defer em.mu.Unlock()
if em.codes == nil {
em.codes = make(map[OpCode]CodeRegistry)
}
em.codes[message.getOpCode()] = CodeRegistry{
handler: handler,
constructor: func() Message {
return reflect.New(reflect.TypeOf(message)).Interface().(Message)
},
}
}
func (em *EventManager) dispatchMessage(raw *RawMessage) error {
if _, exists := em.codes[raw.Header.OpCode]; !exists {
log.Printf("[INFO] fcast: Got a message without a listener - OpCode=%d, Size=%d, Body=%s\n", raw.Header.OpCode, raw.Header.Size, string(raw.Body))
return nil
}
constructor := em.codes[raw.Header.OpCode].constructor
handler := em.codes[raw.Header.OpCode].handler
msg := constructor()
if len(raw.Body) > 0 {
if err := json.Unmarshal(raw.Body, msg); err != nil {
return err
}
}
handler(msg)
return nil
}

47
fcast/message.go Normal file
View file

@ -0,0 +1,47 @@
package fcast
type Message interface {
getOpCode() OpCode
}
func (m PlayMessage) getOpCode() OpCode {
return Play
}
func (m SeekMessage) getOpCode() OpCode {
return Seek
}
func (m PlaybackUpdateMessage) getOpCode() OpCode {
return PlaybackUpdate
}
func (m SetVolumeMessage) getOpCode() OpCode {
return SetVolume
}
func (m VolumeUpdateMessage) getOpCode() OpCode {
return VolumeUpdate
}
func (m PlaybackErrorMessage) getOpCode() OpCode {
return PlaybackError
}
func (m VersionMessage) getOpCode() OpCode {
return Version
}
func (m SetSpeedMessage) getOpCode() OpCode {
return SetSpeed
}
// body-less
func (m PauseMessage) getOpCode() OpCode {
return Pause
}
func (m ResumeMessage) getOpCode() OpCode {
return Resume
}
func (m StopMessage) getOpCode() OpCode {
return Stop
}
func (m PingMessage) getOpCode() OpCode {
return Ping
}
func (m PongMessage) getOpCode() OpCode {
return Pong
}

76
fcast/protocol.go Normal file
View file

@ -0,0 +1,76 @@
package fcast
const (
DefaultPort = 46899
)
type PlayMessage struct {
Container string `json:"container"`
Url string `json:"url,omitempty"`
Content string `json:"content,omitempty"`
Time int `json:"time"` // start time of playback (s)
Speed float32 `json:"speed,omitempty"` // 1=100%, 1 is default
Headers map[string]string `json:"headers,omitempty"` // headers to be passed to the server when requesting media
}
type SeekMessage struct {
Time int `json:"time"` // time to seek in seconds
}
type PlaybackUpdateMessage struct {
GenerationTime int64 `json:"generationTime"` // generation time in UNIX (ms)
Time float32 `json:"time"` // current time playing (s)
Duration float32 `json:"duration"`
State PlaybackState `json:"state"`
}
type SetVolumeMessage struct {
Volume float32 `json:"volume"` // range: 0-1
}
type VolumeUpdateMessage struct {
GenerationTime int64 `json:"generationTime"`
Volume float32 `json:"volume"` // range: 0-1
}
type SetSpeedMessage struct {
Speed float32 `json:"speed"`
}
type PlaybackErrorMessage struct {
Message string `json:"message"`
}
type VersionMessage struct {
Version float32 `json:"version"`
}
type PauseMessage struct{}
type ResumeMessage struct{}
type StopMessage struct{}
type PingMessage struct{}
type PongMessage struct{}
type PlaybackState uint8
const (
Play OpCode = 1
Pause OpCode = 2
Resume OpCode = 3
Stop OpCode = 4
Seek OpCode = 5
PlaybackUpdate OpCode = 6
VolumeUpdate OpCode = 7
SetVolume OpCode = 8
PlaybackError OpCode = 9
SetSpeed OpCode = 10
Version OpCode = 11
Ping OpCode = 12
Pong OpCode = 13
)
const (
Idle PlaybackState = 0
Playing PlaybackState = 1
Paused PlaybackState = 2
)

54
fcast/raw.go Normal file
View file

@ -0,0 +1,54 @@
package fcast
import (
"encoding/binary"
"errors"
)
type OpCode uint8
type MessageHeader struct {
Size uint32 // little endian (4) 0-3
OpCode OpCode // (1) 4-4
}
type RawMessage struct {
Header MessageHeader // 0-4
Body []byte // 5-
}
func NewMessage(op OpCode, body []byte) *RawMessage {
return &RawMessage{
Header: MessageHeader{
Size: uint32(len(body) + 1),
OpCode: op,
},
Body: body,
}
}
func SerializeRaw(message *RawMessage) []byte {
buf := make([]byte, 4+message.Header.Size)
binary.LittleEndian.PutUint32(buf[0:4], message.Header.Size)
buf[4] = byte(message.Header.OpCode)
copy(buf[5:], message.Body)
return buf
}
var (
ErrHeaderTooShort = errors.New("header is too short")
ErrBodyTooShort = errors.New("body is too short")
)
func DeserializeRaw(bodyLen, opCode, body []byte) (*RawMessage, error) {
var msg RawMessage
msg.Header.Size = binary.LittleEndian.Uint32(bodyLen)
msg.Header.OpCode = OpCode(uint8(opCode[0]))
msg.Body = body
return &msg, nil
}