diff --git a/.gitignore b/.gitignore index ad40381a..12f3ba5f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dynatraceSymbols.zip Cartfile.resolved Gemfile.lock dynatrace/ +Carthage ## Various settings *.pbxuser diff --git a/Cartfile b/Cartfile index bec652ef..42aa2684 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,3 @@ binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.4.0 -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/TVVLCKit.json" ~> 3.3.0 \ No newline at end of file +binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/TVVLCKit.json" ~> 3.3.0 +github "gunterhager/UDPBroadcastConnection" diff --git a/Shared/ServerDiscovery/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift index 2ee6122b..2a571d80 100644 --- a/Shared/ServerDiscovery/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -7,6 +7,7 @@ // import Foundation +import UDPBroadcast public class ServerDiscovery { public struct ServerLookupResponse: Codable, Hashable, Identifiable { @@ -46,16 +47,12 @@ public class ServerDiscovery { } } - private let broadcastConn: UDPBroadcastConnection + private var connection: UDPBroadcastConnection? - public init() { - func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {} - - func errorHandler(error: UDPBroadcastConnection.ConnectionError) {} - self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) - } + init() {} public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) { + func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) { do { let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data) @@ -65,12 +62,17 @@ public class ServerDiscovery { completion(nil) } } - self.broadcastConn.handler = receiveHandler + + func errorHandler(error: UDPBroadcastConnection.ConnectionError) { + LogManager.log.error("Error handling response: \(error.localizedDescription)", tag: "ServerDiscovery") + } + do { - try broadcastConn.sendBroadcast("Who is JellyfinServer?") + self.connection = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) + try self.connection?.sendBroadcast("Who is JellyfinServer?") LogManager.log.debug("Discovery broadcast sent", tag: "ServerDiscovery") } catch { - print(error) + LogManager.log.error("Error sending discovery broadcast", tag: "ServerDiscovery") } } } diff --git a/Shared/ServerDiscovery/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift deleted file mode 100644 index fe0ec47e..00000000 --- a/Shared/ServerDiscovery/UDPBroadCastConnection.swift +++ /dev/null @@ -1,311 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Darwin -import Foundation - -// Addresses - -let INADDR_ANY = in_addr(s_addr: 0) -let INADDR_BROADCAST = in_addr(s_addr: 0xFFFF_FFFF) - -/// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. -open class UDPBroadcastConnection { - // MARK: Properties - - /// The address of the UDP socket. - var address: sockaddr_in - - /// Type of a closure that handles incoming UDP packets. - public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void - /// Closure that handles incoming UDP packets. - var handler: ReceiveHandler? - - /// Type of a closure that handles errors that were encountered during receiving UDP packets. - public typealias ErrorHandler = (_ error: ConnectionError) -> Void - /// Closure that handles errors that were encountered during receiving UDP packets. - var errorHandler: ErrorHandler? - - /// A dispatch source for reading data from the UDP socket. - var responseSource: DispatchSourceRead? - - /// The dispatch queue to run responseSource & reconnection on - var dispatchQueue = DispatchQueue.main - - /// Bind to port to start listening without first sending a message - var shouldBeBound: Bool = false - - // MARK: Initializers - - /// Initializes the UDP connection with the correct port address. - - /// - Note: This doesn't open a socket! The socket is opened transparently as needed when sending broadcast messages. If you want to open a socket immediately, use the `bindIt` parameter. This will also try to reopen the socket if it gets closed. - /// - /// - Parameters: - /// - port: Number of the UDP port to use. - /// - bindIt: Opens a port immediately if true, on demand if false. Default is false. - /// - handler: Handler that gets called when data is received. - /// - errorHandler: Handler that gets called when an error occurs. - /// - Throws: Throws a `ConnectionError` if an error occurs. - public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws { - self.address = sockaddr_in(sin_len: __uint8_t(MemoryLayout.size), - sin_family: sa_family_t(AF_INET), - sin_port: UDPBroadcastConnection.htonsPort(port: port), - sin_addr: INADDR_BROADCAST, - sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) - - self.handler = handler - self.errorHandler = errorHandler - self.shouldBeBound = bindIt - if bindIt { - try createSocket() - } - } - - deinit { - if responseSource != nil { - responseSource!.cancel() - } - } - - // MARK: Interface - - /// Create a UDP socket for broadcasting and set up cancel and event handlers - /// - /// - Throws: Throws a `ConnectionError` if an error occurs. - fileprivate func createSocket() throws { - // Create new socket - let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) - guard newSocket > 0 else { throw ConnectionError.createSocketFailed } - - // Enable broadcast on socket - var broadcastEnable = Int32(1) - let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout.size)) - if ret == -1 { - debugPrint("Couldn't enable broadcast on socket") - close(newSocket) - throw ConnectionError.enableBroadcastFailed - } - - // Bind socket if needed - if shouldBeBound { - var saddr = sockaddr(sa_len: 0, sa_family: 0, - sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) - address.sin_addr = INADDR_ANY - memcpy(&saddr, &address, MemoryLayout.size) - address.sin_addr = INADDR_BROADCAST - let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout.size)) - if isBound == -1 { - debugPrint("Couldn't bind socket") - close(newSocket) - throw ConnectionError.bindSocketFailed - } - } - - // Disable global SIGPIPE handler so that the app doesn't crash - setNoSigPipe(socket: newSocket) - - // Set up a dispatch source - let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) - - // Set up cancel handler - newResponseSource.setCancelHandler { - // debugPrint("Closing UDP socket") - let UDPSocket = Int32(newResponseSource.handle) - shutdown(UDPSocket, SHUT_RDWR) - close(UDPSocket) - } - - // Set up event handler (gets called when data arrives at the UDP socket) - newResponseSource.setEventHandler { [unowned self] in - guard let source = self.responseSource else { return } - - var socketAddress = sockaddr_storage() - var socketAddressLength = socklen_t(MemoryLayout.size) - let response = [UInt8](repeating: 0, count: 4096) - let UDPSocket = Int32(source.handle) - - // Do not fix pointer warning. It must work this way. - let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { - recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, - UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) - } - - do { - guard bytesRead > 0 else { - self.closeConnection() - if bytesRead == 0 { - debugPrint("recvfrom returned EOF") - throw ConnectionError.receivedEndOfFile - } else { - if let errorString = String(validatingUTF8: strerror(errno)) { - debugPrint("recvfrom failed: \(errorString)") - } - throw ConnectionError.receiveFailed(code: errno) - } - } - - guard let endpoint = withUnsafePointer(to: &socketAddress, { - self - .getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0) - .bindMemory(to: sockaddr.self, capacity: 1)) }) - else { - // debugPrint("Failed to get the address and port from the socket address received from recvfrom") - self.closeConnection() - return - } - - // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") - - let responseBytes = Data(response[0 ..< bytesRead]) - - // Handle response - self.handler?(endpoint.host, endpoint.port, responseBytes) - } catch { - if let error = error as? ConnectionError { - self.errorHandler?(error) - } else { - self.errorHandler?(ConnectionError.underlying(error: error)) - } - } - } - - newResponseSource.resume() - responseSource = newResponseSource - } - - /// Send broadcast message. - /// - /// - Parameter message: Message to send via broadcast. - /// - Throws: Throws a `ConnectionError` if an error occurs. - open func sendBroadcast(_ message: String) throws { - guard let data = message.data(using: .utf8) else { throw ConnectionError.messageEncodingFailed } - try sendBroadcast(data) - } - - /// Send broadcast data. - /// - /// - Parameter data: Data to send via broadcast. - /// - Throws: Throws a `ConnectionError` if an error occurs. - open func sendBroadcast(_ data: Data) throws { - if responseSource == nil { - try createSocket() - } - - guard let source = responseSource else { return } - let UDPSocket = Int32(source.handle) - let socketLength = socklen_t(address.sin_len) - try data.withUnsafeBytes { broadcastMessage in - let broadcastMessageLength = data.count - let sent = withUnsafeMutablePointer(to: &address) { pointer -> Int in - let memory = UnsafeRawPointer(pointer).bindMemory(to: sockaddr.self, capacity: 1) - return sendto(UDPSocket, broadcastMessage.baseAddress, broadcastMessageLength, 0, memory, socketLength) - } - - guard sent > 0 else { - closeConnection() - throw ConnectionError.sendingMessageFailed(code: errno) - } - } - } - - /// Close the connection. - /// - /// - Parameter reopen: Automatically reopens the connection if true. Defaults to true. - open func closeConnection(reopen: Bool = true) { - if let source = responseSource { - source.cancel() - responseSource = nil - } - if shouldBeBound, reopen { - dispatchQueue.async { - do { - try self.createSocket() - } catch { - self.errorHandler?(ConnectionError.reopeningSocketFailed(error: error)) - } - } - } - } - - // MARK: - Helper - - /// Convert a sockaddr structure into an IP address string and port. - /// - /// - Parameter socketAddressPointer: socketAddressPointer: Pointer to a socket address. - /// - Returns: Returns a tuple of the host IP address and the port in the socket address given. - func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer) -> (host: String, port: Int)? { - let socketAddress = UnsafePointer(socketAddressPointer).pointee - - switch Int32(socketAddress.sa_family) { - case AF_INET: - var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self) - let length = Int(INET_ADDRSTRLEN) + 2 - var buffer = [CChar](repeating: 0, count: length) - let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length)) - let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped) - return (String(cString: hostCString!), port) - - case AF_INET6: - var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self) - let length = Int(INET6_ADDRSTRLEN) + 2 - var buffer = [CChar](repeating: 0, count: length) - let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length)) - let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped) - return (String(cString: hostCString!), port) - - default: - return nil - } - } - - // MARK: - Private - - /// Prevents crashes when blocking calls are pending and the app is paused (via Home button). - /// - /// - Parameter socket: The socket for which the signal should be disabled. - fileprivate func setNoSigPipe(socket: CInt) { - var no_sig_pipe: Int32 = 1 - setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)) - } - - fileprivate class func htonsPort(port: in_port_t) -> in_port_t { - let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian - return isLittleEndian ? _OSSwapInt16(port) : port - } - - fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort { - (value << 8) + (value >> 8) - } -} - -// Created by Gunter Hager on 25.03.19. -// Copyright © 2019 Gunter Hager. All rights reserved. -// -public extension UDPBroadcastConnection { - enum ConnectionError: Error { - // Creating socket - case createSocketFailed - case enableBroadcastFailed - case bindSocketFailed - - // Sending message - case messageEncodingFailed - case sendingMessageFailed(code: Int32) - - // Receiving data - case receivedEndOfFile - case receiveFailed(code: Int32) - - // Closing socket - case reopeningSocketFailed(error: Error) - - // Underlying - case underlying(error: Error) - } -} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 50d35fd9..f9215cf2 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -8,9 +8,7 @@ /* Begin PBXBuildFile section */ 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; - 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; - 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; @@ -249,6 +247,8 @@ 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; + 637FCAF4287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; + 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; }; C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; @@ -543,7 +543,7 @@ }; 62666DF927E5012C00EC0ECD /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 10; files = ( @@ -579,7 +579,6 @@ /* Begin PBXFileReference section */ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; - 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadCastConnection.swift; sourceTree = ""; }; 09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = ""; }; 53116A16268B919A003024C9 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; 53116A18268B947A003024C9 /* PlainLinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLinkButton.swift; sourceTree = ""; }; @@ -743,6 +742,7 @@ 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; + 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemWideElement.swift; sourceTree = ""; }; @@ -925,6 +925,7 @@ 62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */, 62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, + 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, 62666E1B27E501D400EC0ECD /* CoreGraphics.framework in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, @@ -967,6 +968,7 @@ 62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */, C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */, 62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */, + 637FCAF4287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */, E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */, 62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, @@ -1008,7 +1010,6 @@ isa = PBXGroup; children = ( 091B5A872683142E00D78B61 /* ServerDiscovery.swift */, - 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */, ); path = ServerDiscovery; sourceTree = ""; @@ -1353,6 +1354,7 @@ 53D5E3DB264B47EE00BADDC8 /* Frameworks */ = { isa = PBXGroup; children = ( + 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */, 62666E3A27E503E400EC0ECD /* GoogleCastSDK.xcframework */, 62666E3127E5021E00EC0ECD /* UIKit.framework */, 62666E2F27E5021800EC0ECD /* VideoToolbox.framework */, @@ -2248,7 +2250,6 @@ 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, - 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */, @@ -2472,7 +2473,6 @@ E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, - 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */,