149 lines
3.6 KiB

package upnp_ssdp
* Original Code Copyright (C) 2022 Blender Foundation.
* This file is part of Flamenco.
* Flamenco is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
* Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with
* Flamenco. If not, see <https://www.gnu.org/licenses/>.
* ***** END GPL LICENSE BLOCK ***** */
import (
type Client struct {
ssdp *gossdp.ClientSsdp
log *zerolog.Logger
wrappedLog *ssdpLogger
mutex *sync.Mutex
urls []string // Preserves order
seenURLs map[string]bool // Removes duplicates
func NewClient(logger zerolog.Logger) (*Client, error) {
wrap := wrappedLogger(&logger)
client := Client{
log: &logger,
wrappedLog: wrap,
mutex: new(sync.Mutex),
urls: make([]string, 0),
seenURLs: make(map[string]bool),
ssdp, err := gossdp.NewSsdpClientWithLogger(&client, wrap)
if err != nil {
return nil, fmt.Errorf("create UPnP/SSDP client: %w", err)
client.ssdp = ssdp
return &client, nil
func (c *Client) Run(ctx context.Context) ([]string, error) {
defer c.stopCleanly()
log.Debug().Msg("waiting for UPnP/SSDP answer")
go c.ssdp.Start()
var waitTime time.Duration
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(waitTime):
if err := c.ssdp.ListenFor(FlamencoServiceType); err != nil {
return nil, fmt.Errorf("unable to find Manager: %w", err)
waitTime = 1 * time.Second
urls := c.receivedURLs()
if len(urls) > 0 {
return urls, nil
// Response is called by the gossdp library on M-SEARCH responses.
func (c *Client) Response(message gossdp.ResponseMessage) {
logger := c.log.With().
Int("maxAge", message.MaxAge).
Str("searchType", message.SearchType).
Str("deviceID", message.DeviceId).
Str("usn", message.Usn).
Str("location", message.Location).
Str("server", message.Server).
Str("urn", message.Urn).
if message.DeviceId != FlamencoUUID {
logger.Debug().Msg("ignoring message from unknown device")
logger.Debug().Msg("UPnP/SSDP message received")
func (c *Client) appendURL(url string) {
defer c.mutex.Unlock()
// Only append URLs that we haven't seen yet.
if c.seenURLs[url] {
c.urls = append(c.urls, url)
c.seenURLs[url] = true
Int("total", len(c.urls)).
Msg("new URLs received")
// receivedURLs takes a thread-safe copy of the URLs received so far.
func (c *Client) receivedURLs() []string {
defer c.mutex.Unlock()
urls := make([]string, len(c.urls))
copy(urls, c.urls)
return urls
// stopCleanly tries to stop the SSDP client cleanly, without spurious logging.
func (c *Client) stopCleanly() {
c.log.Trace().Msg("UPnP/SSDP client stopping")
// Sneakily disable warnings when shutting down, otherwise the read operation
// from the UDP socket will cause a warning.
tempLog := c.log.Level(zerolog.ErrorLevel)
c.wrappedLog.zlog = &tempLog
c.wrappedLog.zlog = c.log
c.log.Debug().Msg("UPnP/SSDP client stopped")