mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-12-13 17:16:00 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
29d77add70
@ -104,25 +104,57 @@ 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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading || uri.isEmpty)
|
||||
.onAppear(perform: self.viewModel.discoverServers)
|
||||
}
|
||||
}
|
||||
else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 90)
|
||||
|
@ -17,6 +17,10 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@ -199,6 +203,8 @@
|
||||
09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
09389CBD26814DF600AE350E /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = "<group>"; };
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = "<group>"; };
|
||||
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
|
||||
091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadCastConnection.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@ -375,6 +381,14 @@
|
||||
09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */,
|
||||
);
|
||||
path = VideoPlayer;
|
||||
};
|
||||
091B5A852683142E00D78B61 /* ServerLocator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
091B5A872683142E00D78B61 /* ServerDiscovery.swift */,
|
||||
091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */,
|
||||
);
|
||||
path = ServerLocator;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
532175392671BCED005491E6 /* ViewModels */ = {
|
||||
@ -436,6 +450,7 @@
|
||||
535870752669D60C00D05A09 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
091B5A852683142E00D78B61 /* ServerLocator */,
|
||||
62EC352A26766657000E9F2D /* Singleton */,
|
||||
532175392671BCED005491E6 /* ViewModels */,
|
||||
621338912660106C00A81A2A /* Extensions */,
|
||||
@ -904,6 +919,7 @@
|
||||
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 */,
|
||||
@ -922,6 +938,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 */,
|
||||
@ -979,6 +996,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 */,
|
||||
@ -991,6 +1009,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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
92
Shared/ServerLocator/ServerDiscovery.swift
Normal file
92
Shared/ServerLocator/ServerDiscovery.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
327
Shared/ServerLocator/UDPBroadCastConnection.swift
Normal file
327
Shared/ServerLocator/UDPBroadCastConnection.swift
Normal file
@ -0,0 +1,327 @@
|
||||
//
|
||||
// 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<sockaddr_in>.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<UInt32>.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<sockaddr_in>.size)
|
||||
self.address.sin_addr = INADDR_BROADCAST
|
||||
let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.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<sockaddr_storage>.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..<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 {
|
||||
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<sockaddr>) -> (host: String, port: Int)? {
|
||||
let socketAddress = UnsafePointer<sockaddr>(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<Int32>.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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user