import Foundation #if canImport(Darwin) import Darwin #endif /// A UDP socket for receiving RTP. Binds to a preferred port (or an ephemeral /// port when it is busy), then delivers complete datagrams one at a time. /// Mirrors the C++ `UdpRtpTransport` receive path and the Kotlin `DatagramSocket` /// reader. Uses poll(2) with a timeout so `close()` from another thread cannot /// strand a blocked recv (a close does not reliably unblock a POSIX recvfrom). final class UdpTransport { private var fd: Int32 = -1 private(set) var port: UInt16 = 0 /// Binds to [preferredPort] (or an ephemeral port when it is busy). @discardableResult func bind(preferredPort: UInt16, receiveBufferSize: Int32) -> Bool { guard fd < 0 else { return true } for p in [preferredPort, UInt16(0)] { if let f = SocketUtils.makeUdpReceiver(port: p, receiveBufferSize: receiveBufferSize), f >= 0 { fd = f port = SocketUtils.boundPort(fd) return true } } return false } /// Waits up to [timeoutMs] for a datagram. Returns 1 when one is ready to /// read, 0 on timeout, and -1 when the socket is closed/errored. func poll(timeoutMs: Int32) -> Int32 { guard fd >= 0 else { return -1 } var pfd = pollfd(fd: fd, events: poll_events_t(POLLIN), revents: 0) let r = withUnsafeMutablePointer(to: &pfd) { poll($0, 1, timeoutMs) } if r > 0 { if pfd.revents & poll_events_t(POLLERR) != 0 { return -1 } return 1 } return r == 0 ? 0 : -1 } /// Reads one ready datagram. Returns nil on error or empty read. func receiveDatagram() -> [UInt8]? { guard fd >= 0 else { return nil } var buf = [UInt8](repeating: 0, count: 4096) let read = buf.withUnsafeMutableBytes { raw in recvfrom(fd, raw.baseAddress, raw.count, 0, nil, nil) } guard read > 0 else { return nil } return Array(buf.prefix(read)) } func close() { if fd >= 0 { close(fd) fd = -1 } } }