package cast import ( "context" "net" "time" "github.com/barnybug/go-cast" "github.com/barnybug/go-cast/discovery" "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 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 = 2 * time.Second ) func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) { device, err := findDeviceByUUID(ctx, uuid) if err != nil { return nil, errors.WithStack(err) } client := cast.NewClient(device.Host, device.Port) return client, nil } func findDeviceByUUID(ctx context.Context, uuid string) (*Device, error) { service := discovery.NewService(ctx) defer service.Stop() go func() { if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil { logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err))) } }() LOOP: for { select { case c := <-service.Found(): if c.Uuid() == uuid { return &Device{ Host: c.IP().To4(), Port: c.Port(), Name: c.Name(), UUID: c.Uuid(), }, nil } case <-ctx.Done(): break LOOP } } if err := ctx.Err(); err != nil { return nil, errors.WithStack(err) } return nil, errors.WithStack(ErrDeviceNotFound) } func findDevices(ctx context.Context) ([]*Device, error) { service := discovery.NewService(ctx) defer service.Stop() go func() { if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) { logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err))) } }() devices := make([]*Device, 0) found := make(map[string]struct{}) LOOP: for { select { case c := <-service.Found(): if _, exists := found[c.Uuid()]; exists { continue } devices = append(devices, &Device{ Host: c.IP().To4(), Port: c.Port(), Name: c.Name(), UUID: c.Uuid(), }) found[c.Uuid()] = struct{}{} case <-ctx.Done(): break LOOP } } if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) { return nil, errors.WithStack(err) } return devices, nil } func loadURL(ctx context.Context, deviceUUID string, url string) error { client, err := getDeviceClientByUUID(ctx, deviceUUID) if err != nil { return errors.WithStack(err) } if err := client.Connect(ctx); err != nil { return errors.WithStack(err) } defer client.Close() 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) { 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" } func stopCast(ctx context.Context, deviceUUID string) error { client, err := getDeviceClientByUUID(ctx, deviceUUID) if err != nil { return errors.WithStack(err) } if err := client.Connect(ctx); err != nil { return errors.WithStack(err) } defer client.Close() if _, err := client.Receiver().QuitApp(ctx); err != nil { return errors.WithStack(err) } return nil } func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) { client, err := getDeviceClientByUUID(ctx, deviceUUID) if err != nil { return nil, errors.WithStack(err) } if err := client.Connect(ctx); err != nil { return nil, errors.WithStack(err) } defer client.Close() ctrlStatus, err := 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 }