From df8bf349ebfc12b37c1fab4d69fba901bfed9bc4 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:38:45 +1000 Subject: [PATCH 1/2] Add server discovery --- JellyfinPlayer tvOS/ConnectToServerView.swift | 26 ++ JellyfinPlayer.xcodeproj/project.pbxproj | 26 ++ JellyfinPlayer/ConnectToServerView.swift | 28 ++ Shared/ServerLocator/ServerDiscovery.swift | 92 ++++++ .../UDPBroadCastConnection.swift | 300 ++++++++++++++++++ .../UDPBroadcastConnectionError.swift | 34 ++ .../ViewModels/ConnectToServerViewModel.swift | 38 ++- 7 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 Shared/ServerLocator/ServerDiscovery.swift create mode 100644 Shared/ServerLocator/UDPBroadCastConnection.swift create mode 100644 Shared/ServerLocator/UDPBroadcastConnectionError.swift diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index 8f0e02143..031968cb0 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -122,6 +122,32 @@ struct ConnectToServerView: View { } .disabled(viewModel.isLoading || uri.isEmpty) } + Section(header: Text("Local Servers")) { + if self.viewModel.searching { + ProgressView() + } + ForEach(self.viewModel.servers, id: \.id) { server in + Button(action: { + print(server.url) + viewModel.connectToServer(at: server.url) + }, label: { + HStack { + VStack { + Text(server.name) + .font(.headline) + Text(server.host) + .font(.subheadline) + } + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + + }) + } + } + .onAppear(perform: self.viewModel.discoverServers) } } } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 5d0f03162..ea5c7d252 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 091B5A892683142E00D78B61 /* UDPBroadcastConnectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */; }; + 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; + 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; + 091B5A8C268315D400D78B61 /* UDPBroadcastConnectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */; }; + 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; + 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; @@ -181,6 +187,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadcastConnectionError.swift; sourceTree = ""; }; + 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 = ""; }; 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; @@ -345,6 +354,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 091B5A852683142E00D78B61 /* ServerLocator */ = { + isa = PBXGroup; + children = ( + 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */, + 091B5A872683142E00D78B61 /* ServerDiscovery.swift */, + 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */, + ); + path = ServerLocator; + sourceTree = ""; + }; 532175392671BCED005491E6 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -402,6 +421,7 @@ 535870752669D60C00D05A09 /* Shared */ = { isa = PBXGroup; children = ( + 091B5A852683142E00D78B61 /* ServerLocator */, 62EC352A26766657000E9F2D /* Singleton */, 532175392671BCED005491E6 /* ViewModels */, 621338912660106C00A81A2A /* Extensions */, @@ -864,10 +884,12 @@ 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, + 091B5A8C268315D400D78B61 /* UDPBroadcastConnectionError.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, + 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, @@ -885,6 +907,7 @@ 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, + 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, @@ -925,6 +948,7 @@ 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, + 091B5A892683142E00D78B61 /* UDPBroadcastConnectionError.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, @@ -936,6 +960,7 @@ 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, + 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, @@ -948,6 +973,7 @@ 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, + 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 04af93437..00c42e6eb 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -122,6 +122,34 @@ struct ConnectToServerView: View { } .disabled(viewModel.isLoading || uri.isEmpty) } + + Section(header: Text("Local Servers")) { + if self.viewModel.searching { + ProgressView() + } + ForEach(self.viewModel.servers, id: \.id) { server in + Button(action: { + print(server.url) + viewModel.connectToServer(at: server.url) + }, label: { + HStack { + VStack { + Text(server.name) + .font(.headline) + Text(server.host) + .font(.subheadline) + + } + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + + }) + } + } + .onAppear(perform: self.viewModel.discoverServers) } } } diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerLocator/ServerDiscovery.swift new file mode 100644 index 000000000..50292556a --- /dev/null +++ b/Shared/ServerLocator/ServerDiscovery.swift @@ -0,0 +1,92 @@ +// +// ServerLocator.swift +// ABJC +// +// Created by Noah Kamara on 26.03.21. +// + +import Foundation + +public class ServerDiscovery { + public struct ServerCredential: Codable { + public let host: String + public let port: Int + public let username: String + public let password: String + public let deviceId: String + + public init(_ host: String, _ port: Int, _ username: String, _ password: String, _ deviceId: String = UUID().uuidString) { + self.host = host + self.port = port + self.username = username + self.password = password + self.deviceId = deviceId + } + } + + public struct ServerLookupResponse: Codable, Hashable, Identifiable { + + public func hash(into hasher: inout Hasher) { + return hasher.combine(id) + } + + private let address: String + public let id: String + public let name: String + + public var url: URL { + URL(string: self.address)! + } + public var host: String { + let components = URLComponents(string: self.address) + if let host = components?.host { + return host + } + return self.address + } + + public var port: Int { + let components = URLComponents(string: self.address) + if let port = components?.port { + return port + } + return 8096 + } + + enum CodingKeys: String, CodingKey { + case address = "Address" + case id = "Id" + case name = "Name" + } + } + private let broadcastConn: UDPBroadcastConnection + + public init() { + func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) { + print("RECIEVED \(ipAddress):\(String(port)) \(response)") + } + + func errorHandler(error: UDPBroadcastConnection.ConnectionError) { + print(error) + } + self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) + } + + 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) + completion(response) + } catch { + print(error) + completion(nil) + } + } + self.broadcastConn.handler = receiveHandler + do { + try broadcastConn.sendBroadcast("Who is JellyfinServer?") + } catch { + print(error) + } + } +} diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift new file mode 100644 index 000000000..f30973dc8 --- /dev/null +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -0,0 +1,300 @@ +// +// UDPBroadcastConnection.swift +// UDPBroadcast +// +// Created by Gunter Hager on 10.02.16. +// Copyright © 2016 Gunter Hager. All rights reserved. +// + +import Foundation +import Darwin + +// Addresses + +let INADDR_ANY = in_addr(s_addr: 0) +let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff) + + +/// 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 = 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)) + self.address.sin_addr = INADDR_ANY + memcpy(&saddr, &self.address, MemoryLayout.size) + self.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) + + 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.. 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 { + if let errorString = String(validatingUTF8: strerror(errno)) { + debugPrint("UDP connection failed to send data: \(errorString)") + } + closeConnection() + throw ConnectionError.sendingMessageFailed(code: errno) + } + + if sent == broadcastMessageLength { + // Success + debugPrint("UDP connection sent \(broadcastMessageLength) bytes") + } + } + } + + /// 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 { + return (value << 8) + (value >> 8) + } + +} + + + diff --git a/Shared/ServerLocator/UDPBroadcastConnectionError.swift b/Shared/ServerLocator/UDPBroadcastConnectionError.swift new file mode 100644 index 000000000..29c9dd3b8 --- /dev/null +++ b/Shared/ServerLocator/UDPBroadcastConnectionError.swift @@ -0,0 +1,34 @@ +// +// UDPBroadcastConnectionError.swift +// UDPBroadcast +// +// Created by Gunter Hager on 25.03.19. +// Copyright © 2019 Gunter Hager. All rights reserved. +// + +import Foundation + +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/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index aafa64ab0..0c34b20d9 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -25,7 +25,11 @@ final class ConnectToServerViewModel: ViewModel { var publicUsers = [UserDto]() @Published var selectedPublicUser = UserDto() - + + private let discovery: ServerDiscovery = ServerDiscovery() + @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] + @Published var searching = false + override init() { super.init() getPublicUsers() @@ -70,6 +74,38 @@ final class ConnectToServerViewModel: ViewModel { }) .store(in: &cancellables) } + + func connectToServer(at url : URL) { + ServerEnvironment.current.create(with: url.absoluteString) + .trackActivity(loading) + .sink(receiveCompletion: { result in + switch result { + case let .failure(error): + self.errorMessage = error.localizedDescription + default: + break + } + }, receiveValue: { _ in + self.getPublicUsers() + }) + .store(in: &cancellables) + } + + func discoverServers() { + searching = true + + // Timeout after 5 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + self.searching = false + } + + discovery.locateServer { [self] (server) in + if let server = server, !servers.contains(server) { + servers.append(server) + } + searching = false + } + } func login() { SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) From 0d791c094e430240484ee1708a6fcbc5a6ce8161 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:02:39 +1000 Subject: [PATCH 2/2] Fix tvOS searching on every launch --- JellyfinPlayer tvOS/ConnectToServerView.swift | 82 ++++++++++--------- JellyfinPlayer.xcodeproj/project.pbxproj | 6 -- .../UDPBroadCastConnection.swift | 27 ++++++ .../UDPBroadcastConnectionError.swift | 34 -------- 4 files changed, 71 insertions(+), 78 deletions(-) delete mode 100644 Shared/ServerLocator/UDPBroadcastConnectionError.swift diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index 031968cb0..6bda55eca 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -104,50 +104,56 @@ struct ConnectToServerView: View { } } } else { - Form { - Section(header: Text("Server Information")) { - TextField("Jellyfin Server URL", text: $uri) - .disableAutocorrection(true) - .autocapitalization(.none) - Button { - viewModel.connectToServer() - } label: { - HStack { - Text("Connect") - Spacer() + if !viewModel.isLoading { + + Form { + Section(header: Text("Server Information")) { + TextField("Jellyfin Server URL", text: $uri) + .disableAutocorrection(true) + .autocapitalization(.none) + Button { + viewModel.connectToServer() + } label: { + HStack { + Text("Connect") + Spacer() + } + if viewModel.isLoading { + ProgressView() + } } - if viewModel.isLoading { + .disabled(viewModel.isLoading || uri.isEmpty) + } + Section(header: Text("Local Servers")) { + if self.viewModel.searching { ProgressView() } - } - .disabled(viewModel.isLoading || uri.isEmpty) - } - Section(header: Text("Local Servers")) { - if self.viewModel.searching { - ProgressView() - } - ForEach(self.viewModel.servers, id: \.id) { server in - Button(action: { - print(server.url) - viewModel.connectToServer(at: server.url) - }, label: { - HStack { - VStack { - Text(server.name) - .font(.headline) - Text(server.host) - .font(.subheadline) - } - Spacer() - if viewModel.isLoading { - ProgressView() + ForEach(self.viewModel.servers, id: \.id) { server in + Button(action: { + print(server.url) + viewModel.connectToServer(at: server.url) + }, label: { + HStack { + VStack(alignment: .leading) { + Text(server.name) + .font(.headline) + Text(server.host) + .font(.subheadline) + } + Spacer() + Image(systemName: "chevron.forward") + .padding() } - } - - }) + + }) + .disabled(viewModel.isLoading) + } } + .onAppear(perform: self.viewModel.discoverServers) } - .onAppear(perform: self.viewModel.discoverServers) + } + else { + ProgressView() } } } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index ea5c7d252..41e69b265 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -7,10 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 091B5A892683142E00D78B61 /* UDPBroadcastConnectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */; }; 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; - 091B5A8C268315D400D78B61 /* UDPBroadcastConnectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */; }; 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; @@ -187,7 +185,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadcastConnectionError.swift; sourceTree = ""; }; 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 = ""; }; 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; @@ -357,7 +354,6 @@ 091B5A852683142E00D78B61 /* ServerLocator */ = { isa = PBXGroup; children = ( - 091B5A862683142E00D78B61 /* UDPBroadcastConnectionError.swift */, 091B5A872683142E00D78B61 /* ServerDiscovery.swift */, 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */, ); @@ -884,7 +880,6 @@ 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, - 091B5A8C268315D400D78B61 /* UDPBroadcastConnectionError.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, @@ -948,7 +943,6 @@ 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, - 091B5A892683142E00D78B61 /* UDPBroadcastConnectionError.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift index f30973dc8..6df50bd0e 100644 --- a/Shared/ServerLocator/UDPBroadCastConnection.swift +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -298,3 +298,30 @@ open class UDPBroadcastConnection { +// 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/Shared/ServerLocator/UDPBroadcastConnectionError.swift b/Shared/ServerLocator/UDPBroadcastConnectionError.swift deleted file mode 100644 index 29c9dd3b8..000000000 --- a/Shared/ServerLocator/UDPBroadcastConnectionError.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// UDPBroadcastConnectionError.swift -// UDPBroadcast -// -// Created by Gunter Hager on 25.03.19. -// Copyright © 2019 Gunter Hager. All rights reserved. -// - -import Foundation - -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) - } - -}