feat(module): generalizing casting process
All checks were successful
arcad/edge/pipeline/head This commit looks good
All checks were successful
arcad/edge/pipeline/head This commit looks good
This commit is contained in:
@ -2,22 +2,13 @@ package cast
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/barnybug/go-cast"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
UUID string `goja:"uuid" json:"uuid"`
|
||||
Host net.IP `goja:"host" json:"host"`
|
||||
Port int `goja:"port" json:"port"`
|
||||
Name string `goja:"name" json:"name"`
|
||||
}
|
||||
|
||||
type CachedDevice struct {
|
||||
Device
|
||||
UpdatedAt time.Time
|
||||
@ -27,66 +18,49 @@ func (d CachedDevice) Expired() bool {
|
||||
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
|
||||
}
|
||||
|
||||
type DeviceStatus struct {
|
||||
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
|
||||
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
|
||||
}
|
||||
|
||||
type DeviceStatusCurrentApp struct {
|
||||
ID string `goja:"id" json:"id"`
|
||||
DisplayName string `goja:"displayName" json:"displayName"`
|
||||
StatusText string `goja:"statusText" json:"statusText"`
|
||||
}
|
||||
|
||||
type DeviceStatusVolume struct {
|
||||
Level float64 `goja:"level" json:"level"`
|
||||
Muted bool `goja:"muted" json:"muted"`
|
||||
}
|
||||
|
||||
const (
|
||||
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var cache sync.Map
|
||||
|
||||
func getCachedDevice(uuid string) (Device, bool) {
|
||||
value, exists := cache.Load(uuid)
|
||||
if !exists {
|
||||
return Device{}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
cachedDevice, ok := value.(CachedDevice)
|
||||
if !ok {
|
||||
return Device{}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if cachedDevice.Expired() {
|
||||
return Device{}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return cachedDevice.Device, true
|
||||
}
|
||||
|
||||
func cacheDevice(dev Device) {
|
||||
cache.Store(dev.UUID, CachedDevice{
|
||||
cache.Store(dev.DeviceID(), CachedDevice{
|
||||
Device: dev,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
|
||||
device, err := FindDeviceByUUID(ctx, uuid)
|
||||
func getDeviceClientByID(ctx context.Context, deviceID string) (Client, error) {
|
||||
device, err := findCachedDeviceByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
client := cast.NewClient(device.Host, device.Port)
|
||||
client, err := NewClient(ctx, device)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
|
||||
device, exists := getCachedDevice(uuid)
|
||||
func findCachedDeviceByID(ctx context.Context, deviceID string) (Device, error) {
|
||||
device, exists := getCachedDevice(deviceID)
|
||||
if exists {
|
||||
return device, nil
|
||||
}
|
||||
@ -94,18 +68,14 @@ func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
devices, err := SearchDevices(ctx)
|
||||
device, err := Find(ctx, deviceID)
|
||||
if err != nil {
|
||||
return Device{}, nil
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for dev := range devices {
|
||||
if dev.UUID == uuid {
|
||||
return dev, nil
|
||||
}
|
||||
}
|
||||
cacheDevice(device)
|
||||
|
||||
return Device{}, errors.Errorf("could not find device '%s'", uuid)
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
|
||||
@ -125,119 +95,74 @@ func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
ch, err := SearchDevices(ctx)
|
||||
devices, err := SearchDevices(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for dev := range ch {
|
||||
devices = append(devices, dev)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
var searchDevicesMutex sync.Mutex
|
||||
|
||||
func SearchDevices(ctx context.Context) (chan Device, error) {
|
||||
service := NewService(ctx)
|
||||
defer service.Stop()
|
||||
func SearchDevices(ctx context.Context) ([]Device, error) {
|
||||
searchDevicesMutex.Lock()
|
||||
defer searchDevicesMutex.Unlock()
|
||||
|
||||
go func() {
|
||||
searchDevicesMutex.Lock()
|
||||
defer searchDevicesMutex.Unlock()
|
||||
devices, err := Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
||||
logger.Error(ctx, "error while running cast service discovery", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
devices := make(chan Device)
|
||||
|
||||
go func() {
|
||||
defer close(devices)
|
||||
|
||||
found := make(map[string]struct{})
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case c := <-service.Found():
|
||||
dev := Device{
|
||||
Host: c.IP().To4(),
|
||||
Port: c.Port(),
|
||||
Name: c.Name(),
|
||||
UUID: c.Uuid(),
|
||||
}
|
||||
|
||||
if _, exists := found[dev.UUID]; !exists {
|
||||
devices <- dev
|
||||
|
||||
found[dev.UUID] = struct{}{}
|
||||
}
|
||||
|
||||
cacheDevice(dev)
|
||||
|
||||
case <-ctx.Done():
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}()
|
||||
for _, device := range devices {
|
||||
cacheDevice(device)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
var loadURLMutex sync.Mutex
|
||||
|
||||
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
|
||||
func LoadURL(ctx context.Context, deviceID string, url string) error {
|
||||
loadURLMutex.Lock()
|
||||
defer loadURLMutex.Unlock()
|
||||
|
||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||
client, err := getDeviceClientByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
controller, err := client.URL(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
|
||||
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
|
||||
if err := client.Load(ctx, url); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// False positive workaround.
|
||||
func isLoadURLContextExceeded(err error) bool {
|
||||
return err.Error() == "Failed to send load command: context deadline exceeded"
|
||||
}
|
||||
|
||||
var stopCastMutex sync.Mutex
|
||||
|
||||
func StopCast(ctx context.Context, deviceUUID string) error {
|
||||
func StopCast(ctx context.Context, deviceID string) error {
|
||||
stopCastMutex.Lock()
|
||||
defer stopCastMutex.Unlock()
|
||||
|
||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||
client, err := getDeviceClientByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := client.Receiver().QuitApp(ctx); err != nil {
|
||||
if err := client.Unload(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -246,42 +171,25 @@ func StopCast(ctx context.Context, deviceUUID string) error {
|
||||
|
||||
var getStatusMutex sync.Mutex
|
||||
|
||||
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
|
||||
func GetStatus(ctx context.Context, deviceID string) (DeviceStatus, error) {
|
||||
getStatusMutex.Lock()
|
||||
defer getStatusMutex.Unlock()
|
||||
|
||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||
client, err := getDeviceClientByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
ctrlStatus, err := client.Receiver().GetStatus(ctx)
|
||||
status, err := client.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
status := &DeviceStatus{
|
||||
CurrentApp: DeviceStatusCurrentApp{
|
||||
ID: "",
|
||||
DisplayName: "",
|
||||
StatusText: "",
|
||||
},
|
||||
Volume: DeviceStatusVolume{
|
||||
Level: *ctrlStatus.Volume.Level,
|
||||
Muted: *ctrlStatus.Volume.Muted,
|
||||
},
|
||||
}
|
||||
|
||||
if len(ctrlStatus.Applications) > 0 {
|
||||
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
|
||||
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
|
||||
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package cast
|
||||
package cast_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -7,9 +7,13 @@ import (
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
// Register casting device supported types
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/chromecast"
|
||||
)
|
||||
|
||||
func TestCastLoadURL(t *testing.T) {
|
||||
@ -28,7 +32,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
devices, err := ListDevices(ctx, true)
|
||||
devices, err := cast.ListDevices(ctx, true)
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
@ -39,7 +43,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
devices, err = ListDevices(ctx, false)
|
||||
devices, err = cast.ListDevices(ctx, false)
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
@ -55,14 +59,14 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel2()
|
||||
|
||||
if err := LoadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
|
||||
if err := cast.LoadURL(ctx, dev.DeviceID(), "https://go.dev"); err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel3()
|
||||
|
||||
status, err := getStatus(ctx, dev.UUID)
|
||||
status, err := cast.GetStatus(ctx, dev.DeviceID())
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
@ -72,7 +76,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel4()
|
||||
|
||||
if err := StopCast(ctx, dev.UUID); err != nil {
|
||||
if err := cast.StopCast(ctx, dev.DeviceID()); err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
78
pkg/module/cast/chromecast/client.go
Normal file
78
pkg/module/cast/chromecast/client.go
Normal file
@ -0,0 +1,78 @@
|
||||
package chromecast
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
gcast "github.com/barnybug/go-cast"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *gcast.Client
|
||||
}
|
||||
|
||||
// Close implements cast.Client.
|
||||
func (c *Client) Close() error {
|
||||
c.client.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load implements cast.Client.
|
||||
func (c *Client) Load(ctx context.Context, url string) error {
|
||||
controller, err := c.client.URL(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
|
||||
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status implements cast.Client.
|
||||
func (c *Client) Status(ctx context.Context) (cast.DeviceStatus, error) {
|
||||
ctrlStatus, err := c.client.Receiver().GetStatus(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
status := &DeviceStatus{
|
||||
CurrentApp: DeviceStatusCurrentApp{
|
||||
ID: "",
|
||||
DisplayName: "",
|
||||
StatusText: "",
|
||||
},
|
||||
Volume: DeviceStatusVolume{
|
||||
Level: *ctrlStatus.Volume.Level,
|
||||
Muted: *ctrlStatus.Volume.Muted,
|
||||
},
|
||||
}
|
||||
|
||||
if len(ctrlStatus.Applications) > 0 {
|
||||
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
|
||||
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
|
||||
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// Unload implements cast.Client.
|
||||
func (c *Client) Unload(ctx context.Context) error {
|
||||
if _, err := c.client.Receiver().QuitApp(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ cast.Client = &Client{}
|
||||
|
||||
// False positive workaround.
|
||||
func isLoadURLContextExceeded(err error) bool {
|
||||
return err.Error() == "Failed to send load command: context deadline exceeded"
|
||||
}
|
43
pkg/module/cast/chromecast/device.go
Normal file
43
pkg/module/cast/chromecast/device.go
Normal file
@ -0,0 +1,43 @@
|
||||
package chromecast
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
)
|
||||
|
||||
const DeviceTypeChromecast cast.DeviceType = "chromecast"
|
||||
|
||||
type Device struct {
|
||||
UUID string `goja:"uuid" json:"uuid"`
|
||||
Host net.IP `goja:"host" json:"host"`
|
||||
Port int `goja:"port" json:"port"`
|
||||
Name string `goja:"name" json:"name"`
|
||||
}
|
||||
|
||||
// DeviceHost implements cast.Device.
|
||||
func (d *Device) DeviceHost() net.IP {
|
||||
return d.Host
|
||||
}
|
||||
|
||||
// DeviceID implements cast.Device.
|
||||
func (d *Device) DeviceID() string {
|
||||
return d.UUID
|
||||
}
|
||||
|
||||
// DeviceName implements cast.Device.
|
||||
func (d *Device) DeviceName() string {
|
||||
return d.Name
|
||||
}
|
||||
|
||||
// DevicePort implements cast.Device.
|
||||
func (d *Device) DevicePort() int {
|
||||
return d.Port
|
||||
}
|
||||
|
||||
// DeviceType implements cast.Device.
|
||||
func (d *Device) DeviceType() cast.DeviceType {
|
||||
return DeviceTypeChromecast
|
||||
}
|
||||
|
||||
var _ cast.Device = &Device{}
|
@ -1,4 +1,4 @@
|
||||
package cast
|
||||
package chromecast
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -17,24 +17,28 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
const (
|
||||
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
type Discovery struct {
|
||||
found chan *cast.Client
|
||||
entriesCh chan *mdns.ServiceEntry
|
||||
|
||||
stopPeriodic chan struct{}
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context) *Service {
|
||||
s := &Service{
|
||||
func NewDiscovery(ctx context.Context) *Discovery {
|
||||
d := &Discovery{
|
||||
found: make(chan *cast.Client),
|
||||
entriesCh: make(chan *mdns.ServiceEntry, 10),
|
||||
}
|
||||
|
||||
go s.listener(ctx)
|
||||
return s
|
||||
go d.listener(ctx)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Service) Run(ctx context.Context, interval time.Duration) error {
|
||||
func (d *Discovery) Run(ctx context.Context, interval time.Duration) error {
|
||||
ifaces, err := findMulticastInterfaces(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -82,7 +86,7 @@ func (d *Service) Run(ctx context.Context, interval time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
|
||||
func (d *Discovery) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
|
||||
err := mdns.Query(&mdns.QueryParam{
|
||||
Service: "_googlecast._tcp",
|
||||
Domain: "local",
|
||||
@ -99,7 +103,7 @@ func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
|
||||
func (d *Discovery) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
for {
|
||||
select {
|
||||
@ -118,18 +122,18 @@ func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interv
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Service) Stop() {
|
||||
func (d *Discovery) Stop() {
|
||||
if d.stopPeriodic != nil {
|
||||
close(d.stopPeriodic)
|
||||
d.stopPeriodic = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Service) Found() chan *cast.Client {
|
||||
func (d *Discovery) Found() chan *cast.Client {
|
||||
return d.found
|
||||
}
|
||||
|
||||
func (d *Service) listener(ctx context.Context) {
|
||||
func (d *Discovery) listener(ctx context.Context) {
|
||||
for entry := range d.entriesCh {
|
||||
name := strings.Split(entry.Name, "._googlecast")
|
||||
// Skip everything that doesn't have googlecast in the fdqn
|
110
pkg/module/cast/chromecast/service.go
Normal file
110
pkg/module/cast/chromecast/service.go
Normal file
@ -0,0 +1,110 @@
|
||||
package chromecast
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
gcast "github.com/barnybug/go-cast"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cast.Register(DeviceTypeChromecast, &Service{})
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
// Find implements cast.Service.
|
||||
func (s *Service) Find(ctx context.Context, deviceID string) (cast.Device, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
results, err := s.scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for dev := range results {
|
||||
if dev.DeviceID() != deviceID {
|
||||
continue
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(cast.ErrDeviceNotFound)
|
||||
}
|
||||
|
||||
// NewClient implements cast.Service.
|
||||
func (*Service) NewClient(ctx context.Context, device cast.Device) (cast.Client, error) {
|
||||
client := gcast.NewClient(device.DeviceHost(), device.DevicePort())
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &Client{client}, nil
|
||||
}
|
||||
|
||||
// Scan implements cast.Service.
|
||||
func (s *Service) Scan(ctx context.Context) ([]cast.Device, error) {
|
||||
results, err := s.scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
devices := make([]cast.Device, 0)
|
||||
|
||||
for dev := range results {
|
||||
devices = append(devices, dev)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (*Service) scan(ctx context.Context) (chan cast.Device, error) {
|
||||
discovery := NewDiscovery(ctx)
|
||||
defer discovery.Stop()
|
||||
|
||||
go func() {
|
||||
if err := discovery.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
||||
logger.Error(ctx, "could not run chromecast discovery", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
results := make(chan cast.Device)
|
||||
|
||||
go func() {
|
||||
defer close(results)
|
||||
|
||||
found := make(map[string]struct{})
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case c := <-discovery.Found():
|
||||
dev := &Device{
|
||||
Host: c.IP().To4(),
|
||||
Port: c.Port(),
|
||||
Name: c.Name(),
|
||||
UUID: c.Uuid(),
|
||||
}
|
||||
|
||||
if _, exists := found[dev.UUID]; !exists {
|
||||
results <- dev
|
||||
|
||||
found[dev.UUID] = struct{}{}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
var _ cast.Service = &Service{}
|
31
pkg/module/cast/chromecast/status.go
Normal file
31
pkg/module/cast/chromecast/status.go
Normal file
@ -0,0 +1,31 @@
|
||||
package chromecast
|
||||
|
||||
import "forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
|
||||
type DeviceStatus struct {
|
||||
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
|
||||
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
|
||||
}
|
||||
|
||||
// State implements cast.DeviceStatus.
|
||||
func (s *DeviceStatus) State() string {
|
||||
return s.CurrentApp.StatusText
|
||||
}
|
||||
|
||||
// Title implements cast.DeviceStatus.
|
||||
func (s *DeviceStatus) Title() string {
|
||||
return s.CurrentApp.DisplayName
|
||||
}
|
||||
|
||||
var _ cast.DeviceStatus = &DeviceStatus{}
|
||||
|
||||
type DeviceStatusCurrentApp struct {
|
||||
ID string `goja:"id" json:"id"`
|
||||
DisplayName string `goja:"displayName" json:"displayName"`
|
||||
StatusText string `goja:"statusText" json:"statusText"`
|
||||
}
|
||||
|
||||
type DeviceStatusVolume struct {
|
||||
Level float64 `goja:"level" json:"level"`
|
||||
Muted bool `goja:"muted" json:"muted"`
|
||||
}
|
10
pkg/module/cast/client.go
Normal file
10
pkg/module/cast/client.go
Normal file
@ -0,0 +1,10 @@
|
||||
package cast
|
||||
|
||||
import "context"
|
||||
|
||||
type Client interface {
|
||||
Load(ctx context.Context, url string) error
|
||||
Unload(ctx context.Context) error
|
||||
Status(ctx context.Context) (DeviceStatus, error)
|
||||
Close() error
|
||||
}
|
13
pkg/module/cast/device.go
Normal file
13
pkg/module/cast/device.go
Normal file
@ -0,0 +1,13 @@
|
||||
package cast
|
||||
|
||||
import "net"
|
||||
|
||||
type DeviceType string
|
||||
|
||||
type Device interface {
|
||||
DeviceType() DeviceType
|
||||
DeviceHost() net.IP
|
||||
DevicePort() int
|
||||
DeviceName() string
|
||||
DeviceID() string
|
||||
}
|
@ -2,4 +2,7 @@ package cast
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrDeviceNotFound = errors.New("device not found")
|
||||
var (
|
||||
ErrDeviceNotFound = errors.New("device not found")
|
||||
ErrUnknownDeviceType = errors.New("unknown device type")
|
||||
)
|
||||
|
@ -175,7 +175,7 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
status, err := getStatus(ctx, deviceUUID)
|
||||
status, err := GetStatus(ctx, deviceUUID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "error while getting casting device status", logger.CapturedE(err))
|
||||
|
131
pkg/module/cast/registry.go
Normal file
131
pkg/module/cast/registry.go
Normal file
@ -0,0 +1,131 @@
|
||||
package cast
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Scan(ctx context.Context) ([]Device, error)
|
||||
Find(ctx context.Context, deviceID string) (Device, error)
|
||||
NewClient(ctx context.Context, device Device) (Client, error)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
index map[DeviceType]Service
|
||||
}
|
||||
|
||||
func (r *Registry) NewClient(ctx context.Context, device Device) (Client, error) {
|
||||
deviceType := device.DeviceType()
|
||||
|
||||
srv, exists := r.index[deviceType]
|
||||
if !exists {
|
||||
return nil, errors.Wrapf(ErrUnknownDeviceType, "device type '%s' is not registered", deviceType)
|
||||
}
|
||||
|
||||
client, err := srv.NewClient(ctx, device)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Find(ctx context.Context, deviceID string) (Device, error) {
|
||||
for _, srv := range r.index {
|
||||
device, err := srv.Find(ctx, deviceID)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not get device", logger.CapturedE(errors.WithStack(err)))
|
||||
continue
|
||||
}
|
||||
|
||||
if device != nil {
|
||||
return device, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(ErrDeviceNotFound)
|
||||
}
|
||||
|
||||
func (r *Registry) Scan(ctx context.Context) ([]Device, error) {
|
||||
results := make([]Device, 0)
|
||||
errs := make([]error, 0)
|
||||
|
||||
var (
|
||||
lock sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
wg.Add(len(r.index))
|
||||
|
||||
for _, srv := range r.index {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
devices, err := srv.Scan(ctx)
|
||||
if err != nil {
|
||||
lock.Lock()
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
results = append(results, devices...)
|
||||
lock.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
logger.Error(ctx, "error occured while scanning", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Register(deviceType DeviceType, service Service) {
|
||||
r.index[deviceType] = service
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
index: make(map[DeviceType]Service),
|
||||
}
|
||||
}
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
func NewClient(ctx context.Context, device Device) (Client, error) {
|
||||
client, err := defaultRegistry.NewClient(ctx, device)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func Scan(ctx context.Context) ([]Device, error) {
|
||||
devices, err := defaultRegistry.Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func Find(ctx context.Context, deviceID string) (Device, error) {
|
||||
device, err := defaultRegistry.Find(ctx, deviceID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func Register(deviceType DeviceType, service Service) {
|
||||
defaultRegistry.Register(deviceType, service)
|
||||
}
|
6
pkg/module/cast/status.go
Normal file
6
pkg/module/cast/status.go
Normal file
@ -0,0 +1,6 @@
|
||||
package cast
|
||||
|
||||
type DeviceStatus interface {
|
||||
Title() string
|
||||
State() string
|
||||
}
|
@ -21,7 +21,7 @@ func (m *ConsoleModule) Name() string {
|
||||
func (m *ConsoleModule) log(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
var sb strings.Builder
|
||||
|
||||
fields := make([]logger.Field, 0)
|
||||
fields := make([]any, 0)
|
||||
|
||||
stack := rt.CaptureCallStack(0, nil)
|
||||
if len(stack) > 1 {
|
||||
|
Reference in New Issue
Block a user