Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/buger/jsonparser v1.2.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
Expand All @@ -53,13 +54,15 @@ require (
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.23 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
Expand All @@ -75,7 +78,10 @@ require (
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect
golang.org/x/image v0.44.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/tools v0.48.0 // indirect
gopkg.in/ini.v1 v1.67.3 // indirect
)
65 changes: 60 additions & 5 deletions internal/device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/huin/goupnp"
castdns "github.com/vishen/go-chromecast/dns"
)

type Type string
Expand Down Expand Up @@ -68,17 +69,43 @@ func FindInfo(ctx context.Context, timeout time.Duration, dtype Type, name strin
return Info{}, fmt.Errorf("device %q (type %s) not found", name, dtype)
}

// Discover scans the local network for DLNA renderers. (Chromecast discovery
// requires mDNS which the current dependency set doesn't ship; callers
// connect to known Chromecast addresses directly.)
// Discover scans the local network for DLNA and Chromecast renderers.
func Discover(ctx context.Context, timeout time.Duration) ([]Info, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

type result struct {
kind Type
devices []Info
err error
}
results := make(chan result, 2)

go func() {
devices, err := discoverDLNA(ctx)
results <- result{kind: TypeDLNA, devices: devices, err: err}
}()
go func() {
devices, err := discoverChromecasts(ctx)
results <- result{kind: TypeChromecast, devices: devices, err: err}
}()

var devices []Info
for range 2 {
result := <-results
if result.err != nil {
slog.WarnContext(ctx, "device discovery error", "type", result.kind, "error", result.err)
continue
}
devices = append(devices, result.devices...)
}
return devices, nil
}

func discoverDLNA(ctx context.Context) ([]Info, error) {
results, err := goupnp.DiscoverDevicesCtx(ctx, "urn:schemas-upnp-org:device:MediaRenderer:1")
if err != nil {
slog.WarnContext(ctx, "DLNA discovery error", "error", err)
return nil, nil
return nil, err
}

devices := make([]Info, 0, len(results))
Expand All @@ -94,3 +121,31 @@ func Discover(ctx context.Context, timeout time.Duration) ([]Info, error) {
}
return devices, nil
}

func discoverChromecasts(ctx context.Context) ([]Info, error) {
entries, err := castdns.DiscoverCastDNSEntries(ctx, nil)
if err != nil {
return nil, err
}

var devices []Info
for entry := range entries {
info, ok := chromecastInfo(entry)
if !ok {
continue
}
devices = append(devices, info)
}
return devices, nil
}

func chromecastInfo(entry castdns.CastEntry) (Info, bool) {
if entry.DeviceName == "" || entry.AddrV4 == nil {
return Info{}, false
}
return Info{
Name: entry.DeviceName,
Type: TypeChromecast,
Address: entry.AddrV4.String(),
}, true
}
30 changes: 30 additions & 0 deletions internal/device/device_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package device

import (
"net"
"testing"

castdns "github.com/vishen/go-chromecast/dns"
)

func TestChromecastInfo(t *testing.T) {
entry := castdns.CastEntry{
DeviceName: "Office Display",
AddrV4: net.ParseIP("192.0.2.10"),
}

got, ok := chromecastInfo(entry)
if !ok {
t.Fatal("chromecastInfo() rejected a complete entry")
}
want := Info{Name: "Office Display", Type: TypeChromecast, Address: "192.0.2.10"}
if got != want {
t.Errorf("chromecastInfo() = %+v, want %+v", got, want)
}
}

func TestChromecastInfoRejectsIncompleteEntry(t *testing.T) {
if _, ok := chromecastInfo(castdns.CastEntry{DeviceName: "Office Display"}); ok {
t.Error("chromecastInfo() accepted an entry without an IPv4 address")
}
}