tvOS 14.6 fails trying to bind UDP/1900 (succeeds on tvOS Simulator)

I'm working on a tvOS app/library which uses SSDP. The code binds a UDP socket to port 1900 and joins a multicast group using BSD Socket library. I believe this used to work on a previous tvOS version, but specifically as of tvOS 14.6 there is a divergence between the tvOS Simulator and actual devices. Actual devices fail at the call to bind, with an Address Already in Use error. The tvOS simulator binds just fine after throwing up a macOS firewall 'allow incoming connections' dialog.

Any ideas what the official situation is here? The com.apple.developer.networking.multicast entitlement seems to be iOS only at this time, is that correct? Also there seems to be no 'local networking' permission dialog in tvOS - is that also correct?

The com.apple.developer.networking.multicast entitlement seems to be iOS only at this time, is that correct?

That’s correct.

I’m not sure what’s going on with your code. My experience is that the multicast APIs are very easy to use incorrectly, in a way that works in some environments and then fails in others. However, there’s no guarantee that’s what’s going on here.

First things first, lets rule out a port number issue: If you temporarily change the code to use some other port, does that work?

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo thanks for the reply - yes I confirmed that after changing the port number to 19000 instead of 1900 (on both my Client iOS App and tvOS server app) it does indeed work correctly. And it works correctly on the tvOS simulator using the correct SSDP port (1900), and I can see the port bound in lsof when it's running:

MyTVApp      76639 ikura    5u  IPv4 0x5f8914619e22755b      0t0  UDP *:ssdp

Here's my code block where the bind fails, it's using GCDAsyncSockets:

// MARK: Socket Setup

        let multicastSocket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
        multicastSocket.setIPv6Enabled(false)

        do {
            try multicastSocket.enableReusePort(true)
            try multicastSocket.bind(toPort: 1900) // On AppleTV 4K tvOS14.6 this throws with 'Address in Use' error
            try multicastSocket.joinMulticastGroup("239.255.255.250")
            try multicastSocket.enableBroadcast(true)
            try multicastSocket.beginReceiving()
        } catch {
            multicastSocket.close()
            DispatchQueue.main.async {
                for observer in self.observers {
                    observer.multicastSocketInitialisationFailed(error: error)
                }
            }
        }

        self.multicastSocket = multicastSocket

First up, when working with gnarly networking problems you should always test on a real device. The simulator is a great tool for day-to-day debugging but it not relevant for problems like this.

Second, the fact that changing the port fixes things confirms my understanding that tvOS does not require the multicast entitlement.

It also suggests, along with the EADDRINUSE error, that the real issue here is that something else on the Apple TV is using that port. It sounds like that’s a major source of grief for your app, in which case you should definitely file a bug about it.

Please post your bug number, just for the record.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I can't help with the port 1900 part — I haven't reproduced that, and Quinn's reading looks right to me: EADDRINUSE means something on the box already has it, which is a conflict rather than a permission problem. But your second question never got answered, and it has a definite answer now, so for anyone who lands here from a search:

Also there seems to be no 'local networking' permission dialog in tvOS - is that also correct?

Correct. tvOS has no Local Network permission at all. Apple's TN3179 "Understanding local network privacy" lists the platforms that support the feature, and the table reads:

iOS       yes   iOS 14
iPadOS    yes   iPadOS 14
macOS     yes   macOS 15
tvOS      no    -
visionOS  yes   visionOS 1
watchOS   no    -

https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy

So it isn't that the dialog is suppressed, or granted silently, or moved somewhere else in Settings. The privilege doesn't exist on tvOS or watchOS, which also means there is nothing to check and nothing to request. That technote is the 2024 rewrite of the Local Network Privacy FAQ first posted here on the forums in 2020; the table is the part worth bookmarking.

The reason people keep asking is that Apple's own documentation contradicts itself, and the page you find first is the wrong one. The reference page for NSLocalNetworkUsageDescription carries an availability badge reading tvOS 14.0+, and its description reads "Any app that uses the local network, directly or indirectly, should include this description. This includes apps that use Bonjour and services implemented with Bonjour, as well as direct unicast or multicast connections to local hosts." My tvOS app makes both of those last two kinds of connection. NSBonjourServices says tvOS 14.0+ as well. Both are current as I write this. If you search the key name rather than the feature name, you get told to declare a key for a mechanism your platform doesn't have.

A data point on the multicast half, since it's the same area and confirms what Quinn told you: I run SSDP-style multicast discovery in a tvOS app — joins a group, listens on a UDP port, finds a device on the LAN — with no multicast entitlement, no NSLocalNetworkUsageDescription and no NSBonjourServices in the bundle. On tvOS 27 on an Apple TV 4K it joins the group and hears both of my printers announce themselves every few seconds — 689 announcements in a 33-minute run. Nothing asks and nothing blocks.

The part I'd pass on as a warning, because it cost me something. I had the purpose string declared on my tvOS target for months, harmlessly. What it did was work as evidence. I had NWConnection sometimes sitting in .waiting for a few seconds at launch, and two comments in my own code explained that as the user not having answered the Local Network prompt yet — an explanation that sat right next to a plist key appearing to confirm the prompt existed. It stood for nearly three months in the one file someone would open to decide whether that patience was still needed.

And the correction is the dangerous direction, not the error. .waiting at tvOS launch is real — Wi-Fi associating, DNS settling after the device wakes — and the connection does reach .ready on its own. "tvOS has no prompt, so nothing is waiting on one" reads as an argument for deleting the timeout that bounds the wait, which is the only thing standing between an unreachable host and a connect call that never returns. The patience was right; the reason I had written down for it wasn't.

So: delete the key rather than keeping it as harmless, and keep it on your iOS target, where it really is the text the user reads.

Longer write-up, including a second plist key on the same target that turned out to be inert for a different reason: https://mcmizzle.com/blog/tvos-no-local-network-permission/