Initial upload
This commit is contained in:
commit
cefd7abe8a
19 changed files with 1027 additions and 0 deletions
78
fcast/connection.go
Normal file
78
fcast/connection.go
Normal 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
57
fcast/discovery.go
Normal 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
56
fcast/events.go
Normal 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
47
fcast/message.go
Normal 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
76
fcast/protocol.go
Normal 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
54
fcast/raw.go
Normal 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
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue