From 842971da1372623253658c75e21f84aed5c676b6 Mon Sep 17 00:00:00 2001 From: Joe Diragi Date: Sat, 30 Apr 2022 16:28:16 -0400 Subject: [PATCH 1/4] Fixes some compiler warnings --- Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift | 2 +- Swiftfin.xcodeproj/project.pbxproj | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index 78af9836..ffe7a4b1 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -324,7 +324,7 @@ struct SmallMediaStreamSelectionView: View { ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { reader in HStack { - ForEach(0 ..< viewModel.chapters.count) { chapterIndex in + ForEach(0 ..< viewModel.chapters.count, id: \.self) { chapterIndex in VStack(alignment: .leading) { Button { viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 537b1932..09fdcef5 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -191,7 +191,6 @@ 62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2027E501E400EC0ECD /* CoreVideo.framework */; }; 62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2227E501EB00EC0ECD /* Foundation.framework */; }; 62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5362E4BC267D40D8000E2F71 /* Foundation.framework */; }; - 62666E2A27E5020A00EC0ECD /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2927E5020A00EC0ECD /* OpenGLES.framework */; }; 62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2B27E5021000EC0ECD /* QuartzCore.framework */; }; 62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2D27E5021400EC0ECD /* Security.framework */; }; 62666E3027E5021800EC0ECD /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2F27E5021800EC0ECD /* VideoToolbox.framework */; }; @@ -895,7 +894,6 @@ 62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */, 62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */, E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, - 62666E2A27E5020A00EC0ECD /* OpenGLES.framework in Frameworks */, E1002B6B2793E36600E47059 /* Algorithms in Frameworks */, 62666E1D27E501DB00EC0ECD /* CoreMedia.framework in Frameworks */, 62666E3027E5021800EC0ECD /* VideoToolbox.framework in Frameworks */, From a6bcd668d5ad931acc079a0593b1f51437efc75d Mon Sep 17 00:00:00 2001 From: Joe Diragi Date: Sat, 30 Apr 2022 16:50:14 -0400 Subject: [PATCH 2/4] Fixes dangling pointer warning --- Shared/ServerDiscovery/UDPBroadCastConnection.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Shared/ServerDiscovery/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift index 8261fc41..d917ca72 100644 --- a/Shared/ServerDiscovery/UDPBroadCastConnection.swift +++ b/Shared/ServerDiscovery/UDPBroadCastConnection.swift @@ -131,9 +131,11 @@ open class UDPBroadcastConnection { var socketAddressLength = socklen_t(MemoryLayout.size) let response = [UInt8](repeating: 0, count: 4096) let UDPSocket = Int32(source.handle) + let pointer = UnsafeMutablePointer<[UInt8]>.allocate(capacity: response.capacity) + pointer.initialize(to: response) let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { - recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, + recvfrom(UDPSocket, pointer, response.count, 0, UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) } From b43abf1548c55e3b217c5283b1fa7a10d4e870ba Mon Sep 17 00:00:00 2001 From: Joe Diragi Date: Sat, 30 Apr 2022 19:34:11 -0400 Subject: [PATCH 3/4] Runs SwiftLint and adds back OpenGLES --- .../UDPBroadCastConnection.swift | 485 +++++++------ .../Overlays/SmallMenuOverlay.swift | 642 +++++++++--------- Swiftfin.xcodeproj/project.pbxproj | 2 + 3 files changed, 562 insertions(+), 567 deletions(-) diff --git a/Shared/ServerDiscovery/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift index d917ca72..abb8677a 100644 --- a/Shared/ServerDiscovery/UDPBroadCastConnection.swift +++ b/Shared/ServerDiscovery/UDPBroadCastConnection.swift @@ -16,301 +16,298 @@ 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 - // MARK: Properties + /// The address of the UDP socket. + var address: sockaddr_in - /// 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 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? - /// 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? - /// 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 - /// 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 - /// Bind to port to start listening without first sending a message - var shouldBeBound: Bool = false + // MARK: Initializers - // MARK: Initializers + /// Initializes the UDP connection with the correct port address. - /// 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)) - /// - 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() + } + } - self.handler = handler - self.errorHandler = errorHandler - self.shouldBeBound = bindIt - if bindIt { - try createSocket() - } - } + deinit { + if responseSource != nil { + responseSource!.cancel() + } + } - deinit { - if responseSource != nil { - responseSource!.cancel() - } - } + // MARK: Interface - // 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 } - /// 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 { + // 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 + } - // Create new socket - let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) - guard newSocket > 0 else { throw ConnectionError.createSocketFailed } + // 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 + } + } - // 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 - } + // Disable global SIGPIPE handler so that the app doesn't crash + setNoSigPipe(socket: newSocket) - // 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 - } - } + // Set up a dispatch source + let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) - // Disable global SIGPIPE handler so that the app doesn't crash - setNoSigPipe(socket: newSocket) + // Set up cancel handler + newResponseSource.setCancelHandler { + // debugPrint("Closing UDP socket") + let UDPSocket = Int32(newResponseSource.handle) + shutdown(UDPSocket, SHUT_RDWR) + close(UDPSocket) + } - // Set up a dispatch source - let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) + // 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 } - // 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) + var socketAddress = sockaddr_storage() + var socketAddressLength = socklen_t(MemoryLayout.size) + let response = [UInt8](repeating: 0, count: 4096) + let UDPSocket = Int32(source.handle) let pointer = UnsafeMutablePointer<[UInt8]>.allocate(capacity: response.capacity) pointer.initialize(to: response) - let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { - recvfrom(UDPSocket, pointer, response.count, 0, - UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) - } + let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { + recvfrom(UDPSocket, pointer, 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) - } - } + 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 - } + 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)") + // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") - let responseBytes = Data(response[0 ..< bytesRead]) + 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)) - } - } - } + // 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 - } + 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 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() - } + /// 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 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) - } - } - } + 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)) - } - } - } - } + /// 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 + // 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 + /// 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) + 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) + 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 - } - } + default: + return nil + } + } - // MARK: - Private + // 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)) - } + /// 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 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) - } + 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 - enum ConnectionError: Error { - // Creating socket - case createSocketFailed - case enableBroadcastFailed - case bindSocketFailed + // Sending message + case messageEncodingFailed + case sendingMessageFailed(code: Int32) - // Sending message - case messageEncodingFailed - case sendingMessageFailed(code: Int32) + // Receiving data + case receivedEndOfFile + case receiveFailed(code: Int32) - // Receiving data - case receivedEndOfFile - case receiveFailed(code: Int32) + // Closing socket + case reopeningSocketFailed(error: Error) - // Closing socket - case reopeningSocketFailed(error: Error) - - // Underlying - case underlying(error: Error) - } + // Underlying + case underlying(error: Error) + } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index ffe7a4b1..1e6c9660 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -11,356 +11,352 @@ import SwiftUI // TODO: Needs replacement/reworking struct SmallMediaStreamSelectionView: View { + enum Layer: Hashable { + case subtitles + case audio + case playbackSpeed + case chapters + } - enum Layer: Hashable { - case subtitles - case audio - case playbackSpeed - case chapters - } + enum MediaSection: Hashable { + case titles + case items + } - enum MediaSection: Hashable { - case titles - case items - } + @ObservedObject + var viewModel: VideoPlayerViewModel + private let chapterImages: [URL] - @ObservedObject - var viewModel: VideoPlayerViewModel - private let chapterImages: [URL] + @State + private var updateFocusedLayer: Layer = .subtitles + @State + private var lastFocusedLayer: Layer = .subtitles - @State - private var updateFocusedLayer: Layer = .subtitles - @State - private var lastFocusedLayer: Layer = .subtitles + @FocusState + private var subtitlesFocused: Bool + @FocusState + private var audioFocused: Bool + @FocusState + private var playbackSpeedFocused: Bool + @FocusState + private var chaptersFocused: Bool + @FocusState + private var focusedSection: MediaSection? + @FocusState + private var focusedLayer: Layer? { + willSet { + updateFocusedLayer = newValue! - @FocusState - private var subtitlesFocused: Bool - @FocusState - private var audioFocused: Bool - @FocusState - private var playbackSpeedFocused: Bool - @FocusState - private var chaptersFocused: Bool - @FocusState - private var focusedSection: MediaSection? - @FocusState - private var focusedLayer: Layer? { - willSet { - updateFocusedLayer = newValue! + if focusedSection == .titles { + lastFocusedLayer = newValue! + } + } + } - if focusedSection == .titles { - lastFocusedLayer = newValue! - } - } - } + init(viewModel: VideoPlayerViewModel) { + self.viewModel = viewModel + self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) + } - init(viewModel: VideoPlayerViewModel) { - self.viewModel = viewModel - self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) - } + var body: some View { + ZStack(alignment: .bottom) { + LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]), + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + .frame(height: 300) - var body: some View { - ZStack(alignment: .bottom) { - LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]), - startPoint: .top, - endPoint: .bottom) - .ignoresSafeArea() - .frame(height: 300) + VStack { + Spacer() - VStack { + HStack { + // MARK: Subtitle Header - Spacer() + Button { + updateFocusedLayer = .subtitles + focusedLayer = .subtitles + } label: { + if updateFocusedLayer == .subtitles { + HStack(spacing: 15) { + Image(systemName: "captions.bubble") + L10n.subtitles.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "captions.bubble") + L10n.subtitles.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .subtitles) + .focused($subtitlesFocused) + .onChange(of: subtitlesFocused) { isFocused in + if isFocused { + focusedLayer = .subtitles + } + } - HStack { + // MARK: Audio Header - // MARK: Subtitle Header + Button { + updateFocusedLayer = .audio + focusedLayer = .audio + } label: { + if updateFocusedLayer == .audio { + HStack(spacing: 15) { + Image(systemName: "speaker.wave.3") + L10n.audio.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "speaker.wave.3") + L10n.audio.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .audio) + .focused($audioFocused) + .onChange(of: audioFocused) { isFocused in + if isFocused { + focusedLayer = .audio + } + } - Button { - updateFocusedLayer = .subtitles - focusedLayer = .subtitles - } label: { - if updateFocusedLayer == .subtitles { - HStack(spacing: 15) { - Image(systemName: "captions.bubble") - L10n.subtitles.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "captions.bubble") - L10n.subtitles.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .subtitles) - .focused($subtitlesFocused) - .onChange(of: subtitlesFocused) { isFocused in - if isFocused { - focusedLayer = .subtitles - } - } + // MARK: Playback Speed Header - // MARK: Audio Header + Button { + updateFocusedLayer = .playbackSpeed + focusedLayer = .playbackSpeed + } label: { + if updateFocusedLayer == .playbackSpeed { + HStack(spacing: 15) { + Image(systemName: "speedometer") + L10n.playbackSpeed.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "speedometer") + L10n.playbackSpeed.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .playbackSpeed) + .focused($playbackSpeedFocused) + .onChange(of: playbackSpeedFocused) { isFocused in + if isFocused { + focusedLayer = .playbackSpeed + } + } - Button { - updateFocusedLayer = .audio - focusedLayer = .audio - } label: { - if updateFocusedLayer == .audio { - HStack(spacing: 15) { - Image(systemName: "speaker.wave.3") - L10n.audio.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "speaker.wave.3") - L10n.audio.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .audio) - .focused($audioFocused) - .onChange(of: audioFocused) { isFocused in - if isFocused { - focusedLayer = .audio - } - } + // MARK: Chapters Header - // MARK: Playback Speed Header + if !viewModel.chapters.isEmpty { + Button { + updateFocusedLayer = .chapters + focusedLayer = .chapters + } label: { + if updateFocusedLayer == .chapters { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .chapters) + .focused($chaptersFocused) + .onChange(of: chaptersFocused) { isFocused in + if isFocused { + focusedLayer = .chapters + } + } + } - Button { - updateFocusedLayer = .playbackSpeed - focusedLayer = .playbackSpeed - } label: { - if updateFocusedLayer == .playbackSpeed { - HStack(spacing: 15) { - Image(systemName: "speedometer") - L10n.playbackSpeed.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "speedometer") - L10n.playbackSpeed.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .playbackSpeed) - .focused($playbackSpeedFocused) - .onChange(of: playbackSpeedFocused) { isFocused in - if isFocused { - focusedLayer = .playbackSpeed - } - } + Spacer() + } + .padding() + .focusSection() + .focused($focusedSection, equals: .titles) + .onChange(of: focusedSection) { _ in + if focusedSection == .titles { + if lastFocusedLayer == .subtitles { + subtitlesFocused = true + } else if lastFocusedLayer == .audio { + audioFocused = true + } else if lastFocusedLayer == .playbackSpeed { + playbackSpeedFocused = true + } + } + } - // MARK: Chapters Header + if updateFocusedLayer == .subtitles, lastFocusedLayer == .subtitles { + // MARK: Subtitles - if !viewModel.chapters.isEmpty { - Button { - updateFocusedLayer = .chapters - focusedLayer = .chapters - } label: { - if updateFocusedLayer == .chapters { - HStack(spacing: 15) { - Image(systemName: "list.dash") - L10n.chapters.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "list.dash") - L10n.chapters.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .chapters) - .focused($chaptersFocused) - .onChange(of: chaptersFocused) { isFocused in - if isFocused { - focusedLayer = .chapters - } - } - } + subtitleMenuView + } else if updateFocusedLayer == .audio, lastFocusedLayer == .audio { + // MARK: Audio - Spacer() - } - .padding() - .focusSection() - .focused($focusedSection, equals: .titles) - .onChange(of: focusedSection) { _ in - if focusedSection == .titles { - if lastFocusedLayer == .subtitles { - subtitlesFocused = true - } else if lastFocusedLayer == .audio { - audioFocused = true - } else if lastFocusedLayer == .playbackSpeed { - playbackSpeedFocused = true - } - } - } + audioMenuView + } else if updateFocusedLayer == .playbackSpeed, lastFocusedLayer == .playbackSpeed { + // MARK: Playback Speed - if updateFocusedLayer == .subtitles && lastFocusedLayer == .subtitles { - // MARK: Subtitles + playbackSpeedMenuView + } else if updateFocusedLayer == .chapters, lastFocusedLayer == .chapters { + // MARK: Chapters - subtitleMenuView - } else if updateFocusedLayer == .audio && lastFocusedLayer == .audio { - // MARK: Audio + chaptersMenuView + } + } + } + } - audioMenuView - } else if updateFocusedLayer == .playbackSpeed && lastFocusedLayer == .playbackSpeed { - // MARK: Playback Speed + @ViewBuilder + private var subtitleMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.subtitleStreams.isEmpty { + Button {} label: { + L10n.none.text + } + } else { + ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in + Button { + viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 + } label: { + if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { + Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(subtitleStream.displayTitle ?? L10n.noTitle) + } + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } - playbackSpeedMenuView - } else if updateFocusedLayer == .chapters && lastFocusedLayer == .chapters { - // MARK: Chapters + @ViewBuilder + private var audioMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.audioStreams.isEmpty { + Button {} label: { + Text("None") + } + } else { + ForEach(viewModel.audioStreams, id: \.self) { audioStream in + Button { + viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 + } label: { + if audioStream.index == viewModel.selectedAudioStreamIndex { + Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(audioStream.displayTitle ?? L10n.noTitle) + } + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } - chaptersMenuView - } - } - } - } + @ViewBuilder + private var playbackSpeedMenuView: some View { + ScrollView(.horizontal) { + HStack { + ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in + Button { + viewModel.playbackSpeed = playbackSpeed + } label: { + if playbackSpeed == viewModel.playbackSpeed { + Label(playbackSpeed.displayTitle, systemImage: "checkmark") + } else { + Text(playbackSpeed.displayTitle) + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } - @ViewBuilder - private var subtitleMenuView: some View { - ScrollView(.horizontal) { - HStack { - if viewModel.subtitleStreams.isEmpty { - Button {} label: { - L10n.none.text - } - } else { - ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in - Button { - viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 - } label: { - if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { - Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(subtitleStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } - - @ViewBuilder - private var audioMenuView: some View { - ScrollView(.horizontal) { - HStack { - if viewModel.audioStreams.isEmpty { - Button {} label: { - Text("None") - } - } else { - ForEach(viewModel.audioStreams, id: \.self) { audioStream in - Button { - viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 - } label: { - if audioStream.index == viewModel.selectedAudioStreamIndex { - Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(audioStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } - - @ViewBuilder - private var playbackSpeedMenuView: some View { - ScrollView(.horizontal) { - HStack { - ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in - Button { - viewModel.playbackSpeed = playbackSpeed - } label: { - if playbackSpeed == viewModel.playbackSpeed { - Label(playbackSpeed.displayTitle, systemImage: "checkmark") - } else { - Text(playbackSpeed.displayTitle) - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } - - @ViewBuilder - private var chaptersMenuView: some View { - ScrollView(.horizontal, showsIndicators: false) { - ScrollViewReader { reader in - HStack { + @ViewBuilder + private var chaptersMenuView: some View { + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { reader in + HStack { ForEach(0 ..< viewModel.chapters.count, id: \.self) { chapterIndex in - VStack(alignment: .leading) { - Button { - viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) - } label: { - ImageView(chapterImages[chapterIndex]) - .cornerRadius(10) - .frame(width: 350, height: 210) - } - .buttonStyle(CardButtonStyle()) + VStack(alignment: .leading) { + Button { + viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) + } label: { + ImageView(chapterImages[chapterIndex]) + .cornerRadius(10) + .frame(width: 350, height: 210) + } + .buttonStyle(CardButtonStyle()) - VStack(alignment: .leading, spacing: 5) { + VStack(alignment: .leading, spacing: 5) { + Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.white) - Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.white) - - Text(viewModel.chapters[chapterIndex].timestampLabel) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(Color(UIColor.systemBlue)) - .padding(.vertical, 2) - .padding(.horizontal, 4) - .background { - Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) - } - } - } - .id(viewModel.chapters[chapterIndex]) - } - } - .padding(.top) - .onAppear { - reader.scrollTo(viewModel.currentChapter) - } - } - } - } + Text(viewModel.chapters[chapterIndex].timestampLabel) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.systemBlue)) + .padding(.vertical, 2) + .padding(.horizontal, 4) + .background { + Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) + } + } + } + .id(viewModel.chapters[chapterIndex]) + } + } + .padding(.top) + .onAppear { + reader.scrollTo(viewModel.currentChapter) + } + } + } + } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 09fdcef5..537b1932 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ 62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2027E501E400EC0ECD /* CoreVideo.framework */; }; 62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2227E501EB00EC0ECD /* Foundation.framework */; }; 62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5362E4BC267D40D8000E2F71 /* Foundation.framework */; }; + 62666E2A27E5020A00EC0ECD /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2927E5020A00EC0ECD /* OpenGLES.framework */; }; 62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2B27E5021000EC0ECD /* QuartzCore.framework */; }; 62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2D27E5021400EC0ECD /* Security.framework */; }; 62666E3027E5021800EC0ECD /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2F27E5021800EC0ECD /* VideoToolbox.framework */; }; @@ -894,6 +895,7 @@ 62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */, 62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */, E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, + 62666E2A27E5020A00EC0ECD /* OpenGLES.framework in Frameworks */, E1002B6B2793E36600E47059 /* Algorithms in Frameworks */, 62666E1D27E501DB00EC0ECD /* CoreMedia.framework in Frameworks */, 62666E3027E5021800EC0ECD /* VideoToolbox.framework in Frameworks */, From 0ca924c8f205c1394aaabf3408a871a89d9eaa9e Mon Sep 17 00:00:00 2001 From: Joe Diragi Date: Sun, 1 May 2022 15:23:52 -0400 Subject: [PATCH 4/4] Updates swiftformat --- .../UDPBroadCastConnection.swift | 485 +++++++------ .../Overlays/SmallMenuOverlay.swift | 644 +++++++++--------- 2 files changed, 566 insertions(+), 563 deletions(-) diff --git a/Shared/ServerDiscovery/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift index abb8677a..8ce69534 100644 --- a/Shared/ServerDiscovery/UDPBroadCastConnection.swift +++ b/Shared/ServerDiscovery/UDPBroadCastConnection.swift @@ -16,298 +16,297 @@ 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 + // MARK: Properties - /// The address of the UDP socket. - var address: sockaddr_in + /// 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 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? + /// 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? + /// 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 + /// 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 + /// Bind to port to start listening without first sending a message + var shouldBeBound: Bool = false - // MARK: Initializers + // MARK: Initializers - /// Initializes the UDP connection with the correct port address. + /// 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)) + /// - 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() - } - } + self.handler = handler + self.errorHandler = errorHandler + self.shouldBeBound = bindIt + if bindIt { + try createSocket() + } + } - deinit { - if responseSource != nil { - responseSource!.cancel() - } - } + deinit { + if responseSource != nil { + responseSource!.cancel() + } + } - // MARK: Interface + // 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 } + /// 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 - } + // 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 - } - } + // 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) + // 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 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 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 } + // 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 pointer = UnsafeMutablePointer<[UInt8]>.allocate(capacity: response.capacity) - pointer.initialize(to: response) + var socketAddress = sockaddr_storage() + var socketAddressLength = socklen_t(MemoryLayout.size) + let response = [UInt8](repeating: 0, count: 4096) + let UDPSocket = Int32(source.handle) + let pointer = UnsafeMutablePointer<[UInt8]>.allocate(capacity: response.capacity) + pointer.initialize(to: response) - let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { - recvfrom(UDPSocket, pointer, response.count, 0, - UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) - } + let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { + recvfrom(UDPSocket, pointer, 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) - } - } + 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 - } + 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)") + // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") - let responseBytes = Data(response[0 ..< bytesRead]) + 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)) - } - } - } + // 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 - } + 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 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() - } + /// 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 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) - } - } - } + 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)) - } - } - } - } + /// 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 + // 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 + /// 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) + 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) + 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 - } - } + default: + return nil + } + } - // MARK: - Private + // 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)) - } + /// 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 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) - } + 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 + enum ConnectionError: Error { + // Creating socket + case createSocketFailed + case enableBroadcastFailed + case bindSocketFailed - // Sending message - case messageEncodingFailed - case sendingMessageFailed(code: Int32) + // Sending message + case messageEncodingFailed + case sendingMessageFailed(code: Int32) - // Receiving data - case receivedEndOfFile - case receiveFailed(code: Int32) + // Receiving data + case receivedEndOfFile + case receiveFailed(code: Int32) - // Closing socket - case reopeningSocketFailed(error: Error) + // Closing socket + case reopeningSocketFailed(error: Error) - // Underlying - case underlying(error: Error) - } + // Underlying + case underlying(error: Error) + } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index 1e6c9660..1bebee07 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -11,352 +11,356 @@ import SwiftUI // TODO: Needs replacement/reworking struct SmallMediaStreamSelectionView: View { - enum Layer: Hashable { - case subtitles - case audio - case playbackSpeed - case chapters - } - enum MediaSection: Hashable { - case titles - case items - } + enum Layer: Hashable { + case subtitles + case audio + case playbackSpeed + case chapters + } - @ObservedObject - var viewModel: VideoPlayerViewModel - private let chapterImages: [URL] + enum MediaSection: Hashable { + case titles + case items + } - @State - private var updateFocusedLayer: Layer = .subtitles - @State - private var lastFocusedLayer: Layer = .subtitles + @ObservedObject + var viewModel: VideoPlayerViewModel + private let chapterImages: [URL] - @FocusState - private var subtitlesFocused: Bool - @FocusState - private var audioFocused: Bool - @FocusState - private var playbackSpeedFocused: Bool - @FocusState - private var chaptersFocused: Bool - @FocusState - private var focusedSection: MediaSection? - @FocusState - private var focusedLayer: Layer? { - willSet { - updateFocusedLayer = newValue! + @State + private var updateFocusedLayer: Layer = .subtitles + @State + private var lastFocusedLayer: Layer = .subtitles - if focusedSection == .titles { - lastFocusedLayer = newValue! - } - } - } + @FocusState + private var subtitlesFocused: Bool + @FocusState + private var audioFocused: Bool + @FocusState + private var playbackSpeedFocused: Bool + @FocusState + private var chaptersFocused: Bool + @FocusState + private var focusedSection: MediaSection? + @FocusState + private var focusedLayer: Layer? { + willSet { + updateFocusedLayer = newValue! - init(viewModel: VideoPlayerViewModel) { - self.viewModel = viewModel - self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) - } + if focusedSection == .titles { + lastFocusedLayer = newValue! + } + } + } - var body: some View { - ZStack(alignment: .bottom) { - LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]), - startPoint: .top, - endPoint: .bottom) - .ignoresSafeArea() - .frame(height: 300) + init(viewModel: VideoPlayerViewModel) { + self.viewModel = viewModel + self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) + } - VStack { - Spacer() + var body: some View { + ZStack(alignment: .bottom) { + LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]), + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + .frame(height: 300) - HStack { - // MARK: Subtitle Header + VStack { - Button { - updateFocusedLayer = .subtitles - focusedLayer = .subtitles - } label: { - if updateFocusedLayer == .subtitles { - HStack(spacing: 15) { - Image(systemName: "captions.bubble") - L10n.subtitles.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "captions.bubble") - L10n.subtitles.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .subtitles) - .focused($subtitlesFocused) - .onChange(of: subtitlesFocused) { isFocused in - if isFocused { - focusedLayer = .subtitles - } - } + Spacer() - // MARK: Audio Header + HStack { - Button { - updateFocusedLayer = .audio - focusedLayer = .audio - } label: { - if updateFocusedLayer == .audio { - HStack(spacing: 15) { - Image(systemName: "speaker.wave.3") - L10n.audio.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "speaker.wave.3") - L10n.audio.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .audio) - .focused($audioFocused) - .onChange(of: audioFocused) { isFocused in - if isFocused { - focusedLayer = .audio - } - } + // MARK: Subtitle Header - // MARK: Playback Speed Header + Button { + updateFocusedLayer = .subtitles + focusedLayer = .subtitles + } label: { + if updateFocusedLayer == .subtitles { + HStack(spacing: 15) { + Image(systemName: "captions.bubble") + L10n.subtitles.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "captions.bubble") + L10n.subtitles.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .subtitles) + .focused($subtitlesFocused) + .onChange(of: subtitlesFocused) { isFocused in + if isFocused { + focusedLayer = .subtitles + } + } - Button { - updateFocusedLayer = .playbackSpeed - focusedLayer = .playbackSpeed - } label: { - if updateFocusedLayer == .playbackSpeed { - HStack(spacing: 15) { - Image(systemName: "speedometer") - L10n.playbackSpeed.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "speedometer") - L10n.playbackSpeed.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .playbackSpeed) - .focused($playbackSpeedFocused) - .onChange(of: playbackSpeedFocused) { isFocused in - if isFocused { - focusedLayer = .playbackSpeed - } - } + // MARK: Audio Header - // MARK: Chapters Header + Button { + updateFocusedLayer = .audio + focusedLayer = .audio + } label: { + if updateFocusedLayer == .audio { + HStack(spacing: 15) { + Image(systemName: "speaker.wave.3") + L10n.audio.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "speaker.wave.3") + L10n.audio.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .audio) + .focused($audioFocused) + .onChange(of: audioFocused) { isFocused in + if isFocused { + focusedLayer = .audio + } + } - if !viewModel.chapters.isEmpty { - Button { - updateFocusedLayer = .chapters - focusedLayer = .chapters - } label: { - if updateFocusedLayer == .chapters { - HStack(spacing: 15) { - Image(systemName: "list.dash") - L10n.chapters.text - } - .padding() - .background(Color.white) - .foregroundColor(.black) - } else { - HStack(spacing: 15) { - Image(systemName: "list.dash") - L10n.chapters.text - } - .padding() - } - } - .buttonStyle(PlainButtonStyle()) - .background(Color.clear) - .focused($focusedLayer, equals: .chapters) - .focused($chaptersFocused) - .onChange(of: chaptersFocused) { isFocused in - if isFocused { - focusedLayer = .chapters - } - } - } + // MARK: Playback Speed Header - Spacer() - } - .padding() - .focusSection() - .focused($focusedSection, equals: .titles) - .onChange(of: focusedSection) { _ in - if focusedSection == .titles { - if lastFocusedLayer == .subtitles { - subtitlesFocused = true - } else if lastFocusedLayer == .audio { - audioFocused = true - } else if lastFocusedLayer == .playbackSpeed { - playbackSpeedFocused = true - } - } - } + Button { + updateFocusedLayer = .playbackSpeed + focusedLayer = .playbackSpeed + } label: { + if updateFocusedLayer == .playbackSpeed { + HStack(spacing: 15) { + Image(systemName: "speedometer") + L10n.playbackSpeed.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "speedometer") + L10n.playbackSpeed.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .playbackSpeed) + .focused($playbackSpeedFocused) + .onChange(of: playbackSpeedFocused) { isFocused in + if isFocused { + focusedLayer = .playbackSpeed + } + } - if updateFocusedLayer == .subtitles, lastFocusedLayer == .subtitles { - // MARK: Subtitles + // MARK: Chapters Header - subtitleMenuView - } else if updateFocusedLayer == .audio, lastFocusedLayer == .audio { - // MARK: Audio + if !viewModel.chapters.isEmpty { + Button { + updateFocusedLayer = .chapters + focusedLayer = .chapters + } label: { + if updateFocusedLayer == .chapters { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .chapters) + .focused($chaptersFocused) + .onChange(of: chaptersFocused) { isFocused in + if isFocused { + focusedLayer = .chapters + } + } + } - audioMenuView - } else if updateFocusedLayer == .playbackSpeed, lastFocusedLayer == .playbackSpeed { - // MARK: Playback Speed + Spacer() + } + .padding() + .focusSection() + .focused($focusedSection, equals: .titles) + .onChange(of: focusedSection) { _ in + if focusedSection == .titles { + if lastFocusedLayer == .subtitles { + subtitlesFocused = true + } else if lastFocusedLayer == .audio { + audioFocused = true + } else if lastFocusedLayer == .playbackSpeed { + playbackSpeedFocused = true + } + } + } - playbackSpeedMenuView - } else if updateFocusedLayer == .chapters, lastFocusedLayer == .chapters { - // MARK: Chapters + if updateFocusedLayer == .subtitles && lastFocusedLayer == .subtitles { + // MARK: Subtitles - chaptersMenuView - } - } - } - } + subtitleMenuView + } else if updateFocusedLayer == .audio && lastFocusedLayer == .audio { + // MARK: Audio - @ViewBuilder - private var subtitleMenuView: some View { - ScrollView(.horizontal) { - HStack { - if viewModel.subtitleStreams.isEmpty { - Button {} label: { - L10n.none.text - } - } else { - ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in - Button { - viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 - } label: { - if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { - Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(subtitleStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } + audioMenuView + } else if updateFocusedLayer == .playbackSpeed && lastFocusedLayer == .playbackSpeed { + // MARK: Playback Speed - @ViewBuilder - private var audioMenuView: some View { - ScrollView(.horizontal) { - HStack { - if viewModel.audioStreams.isEmpty { - Button {} label: { - Text("None") - } - } else { - ForEach(viewModel.audioStreams, id: \.self) { audioStream in - Button { - viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 - } label: { - if audioStream.index == viewModel.selectedAudioStreamIndex { - Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(audioStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } + playbackSpeedMenuView + } else if updateFocusedLayer == .chapters && lastFocusedLayer == .chapters { + // MARK: Chapters - @ViewBuilder - private var playbackSpeedMenuView: some View { - ScrollView(.horizontal) { - HStack { - ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in - Button { - viewModel.playbackSpeed = playbackSpeed - } label: { - if playbackSpeed == viewModel.playbackSpeed { - Label(playbackSpeed.displayTitle, systemImage: "checkmark") - } else { - Text(playbackSpeed.displayTitle) - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } - } + chaptersMenuView + } + } + } + } - @ViewBuilder - private var chaptersMenuView: some View { - ScrollView(.horizontal, showsIndicators: false) { - ScrollViewReader { reader in - HStack { - ForEach(0 ..< viewModel.chapters.count, id: \.self) { chapterIndex in - VStack(alignment: .leading) { - Button { - viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) - } label: { - ImageView(chapterImages[chapterIndex]) - .cornerRadius(10) - .frame(width: 350, height: 210) - } - .buttonStyle(CardButtonStyle()) + @ViewBuilder + private var subtitleMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.subtitleStreams.isEmpty { + Button {} label: { + L10n.none.text + } + } else { + ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in + Button { + viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 + } label: { + if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { + Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(subtitleStream.displayTitle ?? L10n.noTitle) + } + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } - VStack(alignment: .leading, spacing: 5) { - Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.white) + @ViewBuilder + private var audioMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.audioStreams.isEmpty { + Button {} label: { + Text("None") + } + } else { + ForEach(viewModel.audioStreams, id: \.self) { audioStream in + Button { + viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 + } label: { + if audioStream.index == viewModel.selectedAudioStreamIndex { + Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(audioStream.displayTitle ?? L10n.noTitle) + } + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } - Text(viewModel.chapters[chapterIndex].timestampLabel) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(Color(UIColor.systemBlue)) - .padding(.vertical, 2) - .padding(.horizontal, 4) - .background { - Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) - } - } - } - .id(viewModel.chapters[chapterIndex]) - } - } - .padding(.top) - .onAppear { - reader.scrollTo(viewModel.currentChapter) - } - } - } - } + @ViewBuilder + private var playbackSpeedMenuView: some View { + ScrollView(.horizontal) { + HStack { + ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in + Button { + viewModel.playbackSpeed = playbackSpeed + } label: { + if playbackSpeed == viewModel.playbackSpeed { + Label(playbackSpeed.displayTitle, systemImage: "checkmark") + } else { + Text(playbackSpeed.displayTitle) + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } + + @ViewBuilder + private var chaptersMenuView: some View { + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { reader in + HStack { + ForEach(0 ..< viewModel.chapters.count, id: \.self) { chapterIndex in + VStack(alignment: .leading) { + Button { + viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) + } label: { + ImageView(chapterImages[chapterIndex]) + .cornerRadius(10) + .frame(width: 350, height: 210) + } + .buttonStyle(CardButtonStyle()) + + VStack(alignment: .leading, spacing: 5) { + + Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.white) + + Text(viewModel.chapters[chapterIndex].timestampLabel) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.systemBlue)) + .padding(.vertical, 2) + .padding(.horizontal, 4) + .background { + Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) + } + } + } + .id(viewModel.chapters[chapterIndex]) + } + } + .padding(.top) + .onAppear { + reader.scrollTo(viewModel.currentChapter) + } + } + } + } }