├── .gitattributes ├── SmartView.xcframework ├── ios-arm64_x86_64-maccatalyst │ └── SmartView.framework │ │ ├── Versions │ │ ├── Current │ │ └── A │ │ │ ├── SmartView │ │ │ ├── Resources │ │ │ ├── ca_crt.cer │ │ │ ├── ca_crt_new.cer │ │ │ ├── Info.plist │ │ │ └── LICENSE │ │ │ ├── Modules │ │ │ ├── SmartView.swiftmodule │ │ │ │ ├── arm64-apple-ios-macabi.swiftdoc │ │ │ │ └── x86_64-apple-ios-macabi.swiftdoc │ │ │ └── module.modulemap │ │ │ └── Headers │ │ │ └── SmartView.h │ │ ├── Headers │ │ ├── Modules │ │ ├── Resources │ │ ├── SmartView │ │ └── PrivateHeaders ├── ios-arm64 │ └── SmartView.framework │ │ ├── SmartView │ │ ├── Info.plist │ │ ├── ca_crt.cer │ │ ├── ca_crt_new.cer │ │ ├── Modules │ │ ├── SmartView.swiftmodule │ │ │ ├── arm64-apple-ios.swiftdoc │ │ │ └── arm64-apple-ios.swiftinterface │ │ └── module.modulemap │ │ ├── Headers │ │ └── SmartView.h │ │ └── LICENSE ├── ios-arm64_x86_64-simulator │ └── SmartView.framework │ │ ├── Info.plist │ │ ├── SmartView │ │ ├── ca_crt.cer │ │ ├── ca_crt_new.cer │ │ ├── Modules │ │ ├── SmartView.swiftmodule │ │ │ ├── arm64-apple-ios-simulator.swiftdoc │ │ │ └── x86_64-apple-ios-simulator.swiftdoc │ │ └── module.modulemap │ │ ├── Headers │ │ └── SmartView.h │ │ ├── _CodeSignature │ │ └── CodeResources │ │ └── LICENSE └── Info.plist ├── TVCommanderKitDemo ├── TVCommanderKitDemo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── TVCommanderKitDemoApp.swift │ ├── ContentViewModel.swift │ └── ContentView.swift └── TVCommanderKitDemo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Tests └── TVCommanderKitTests │ ├── TVConnectionConfigurationTests.swift │ ├── TVTests.swift │ ├── MockURLProtocol.swift │ ├── TVSearcherTests.swift │ ├── TVFetcherTests.swift │ ├── TVWebSocketHandlerTests.swift │ └── TVCommanderTests.swift ├── Sources └── TVCommanderKit │ ├── TVWebSocketCreator.swift │ ├── TVFetcher+Errors.swift │ ├── TVFetcher.swift │ ├── TVWebSocketBuilder.swift │ ├── TVWebSocketHandler.swift │ ├── TVCommander+Errors.swift │ ├── TVSearcher.swift │ ├── TVAppManager.swift │ ├── TVCommanderKit+Extensions.swift │ ├── TVCommander.swift │ └── TVCommanderKit+Models.swift ├── LICENSE ├── Package.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xcframework/* linguist-vendored 2 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/Current: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Headers: -------------------------------------------------------------------------------- 1 | Versions/Current/Headers -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Modules: -------------------------------------------------------------------------------- 1 | Versions/Current/Modules -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Resources: -------------------------------------------------------------------------------- 1 | Versions/Current/Resources -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/SmartView: -------------------------------------------------------------------------------- 1 | Versions/Current/SmartView -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/PrivateHeaders: -------------------------------------------------------------------------------- 1 | Versions/Current/PrivateHeaders -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/SmartView: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64/SmartView.framework/SmartView -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64/SmartView.framework/Info.plist -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/ca_crt.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64/SmartView.framework/ca_crt.cer -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/ca_crt_new.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64/SmartView.framework/ca_crt_new.cer -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Info.plist -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/SmartView: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/SmartView -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/ca_crt.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/ca_crt.cer -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/ca_crt_new.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/ca_crt_new.cer -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/SmartView: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/SmartView -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/ca_crt.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/ca_crt.cer -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/Modules/SmartView.swiftmodule/arm64-apple-ios.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64/SmartView.framework/Modules/SmartView.swiftmodule/arm64-apple-ios.swiftdoc -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/ca_crt_new.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/ca_crt_new.cer -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Modules/SmartView.swiftmodule/arm64-apple-ios-macabi.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Modules/SmartView.swiftmodule/arm64-apple-ios-macabi.swiftdoc -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Modules/SmartView.swiftmodule/x86_64-apple-ios-macabi.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdesimini/TVCommanderKit/HEAD/SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Modules/SmartView.swiftmodule/x86_64-apple-ios-macabi.swiftdoc -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "starscream", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/daltoniam/Starscream.git", 7 | "state" : { 8 | "branch" : "master", 9 | "revision" : "d3a0b107328e3bc1601dccbfbcd556bd53aed767" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/TVCommanderKitDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVCommanderKitDemoApp.swift 3 | // TVCommanderKitDemo 4 | // 5 | // Created by Wilson Desimini on 10/24/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TVCommanderKitDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | 2 | framework module SmartView { 3 | umbrella header "SmartView.h" 4 | 5 | 6 | export * 7 | module * { export * } 8 | 9 | explicit module PrivateCode { 10 | header "GCDAsyncUdpSocket.h" 11 | export * 12 | } 13 | } 14 | 15 | module SmartView.Swift { 16 | header "SmartView-Swift.h" 17 | requires objc 18 | } 19 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | 2 | framework module SmartView { 3 | umbrella header "SmartView.h" 4 | 5 | 6 | export * 7 | module * { export * } 8 | 9 | explicit module PrivateCode { 10 | header "GCDAsyncUdpSocket.h" 11 | export * 12 | } 13 | } 14 | 15 | module SmartView.Swift { 16 | header "SmartView-Swift.h" 17 | requires objc 18 | } 19 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | 2 | framework module SmartView { 3 | umbrella header "SmartView.h" 4 | 5 | 6 | export * 7 | module * { export * } 8 | 9 | explicit module PrivateCode { 10 | header "GCDAsyncUdpSocket.h" 11 | export * 12 | } 13 | } 14 | 15 | module SmartView.Swift { 16 | header "SmartView-Swift.h" 17 | requires objc 18 | } 19 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "starscream", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/daltoniam/Starscream.git", 7 | "state" : { 8 | "branch" : "master", 9 | "revision" : "d3a0b107328e3bc1601dccbfbcd556bd53aed767" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVConnectionConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVConnectionConfigurationTests.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/17/24. 6 | // 7 | 8 | import XCTest 9 | @testable import TVCommanderKit 10 | 11 | final class TVConnectionConfigurationTests: XCTestCase { 12 | func testWSSURL_fromTVConnectionConfiguration() { 13 | // given 14 | let appName = "Test" 15 | let token = "1234567" 16 | let ipAddress = "192.168.0.1" 17 | // when 18 | let tvConfig = TVConnectionConfiguration( 19 | id: nil, 20 | app: appName, 21 | path: "/api/v2/channels/samsung.remote.control", 22 | ipAddress: ipAddress, 23 | port: 8002, 24 | scheme: "wss", 25 | token: token 26 | ) 27 | // then 28 | XCTAssertEqual(tvConfig.wssURL(), URL(string: "wss://192.168.0.1:8002/api/v2/channels/samsung.remote.control?name=VGVzdA==&token=1234567")) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVWebSocketCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVWebSocketCreator.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 5/11/24. 6 | // 7 | 8 | import Foundation 9 | import Starscream 10 | 11 | class TVWebSocketCreator { 12 | let builder: TVWebSocketBuilder 13 | 14 | init(builder: TVWebSocketBuilder = .init()) { 15 | self.builder = builder 16 | } 17 | 18 | func createTVWebSocket( 19 | url: URL, 20 | certPinner: CertificatePinning?, 21 | delegate: WebSocketDelegate 22 | ) -> WebSocket { 23 | builder.setURLRequest(.init(url: url)) 24 | builder.setCertPinner(certPinner ?? TVDefaultWebSocketCertPinner()) 25 | builder.setDelegate(delegate) 26 | return builder.getWebSocket()! 27 | } 28 | } 29 | 30 | /// Default cert-pinning implementation that trusts all connections 31 | private class TVDefaultWebSocketCertPinner: CertificatePinning { 32 | func evaluateTrust(trust: SecTrust, domain: String?, completion: ((PinningState) -> ())) { 33 | completion(.success) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2023 Wilson Desimini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVTests.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/17/24. 6 | // 7 | 8 | import XCTest 9 | @testable import TVCommanderKit 10 | 11 | final class TVTests: XCTestCase { 12 | func testIPAddress_fromTVURL() { 13 | // given 14 | let httpURL = "http://192.168.0.1:8001/api/v2/" 15 | // when 16 | let tv = TV(id: "", name: "", type: "", uri: httpURL) 17 | // then 18 | XCTAssertEqual(tv.ipAddress, "192.168.0.1") 19 | } 20 | 21 | func testIPAddress_fromTVDeviceIP() { 22 | // given 23 | let device = TV.Device(ip: "192.168.0.1", tokenAuthSupport: "", wifiMac: "") 24 | // when 25 | let tv = TV(device: device, id: "", name: "", type: "", uri: "") 26 | // then 27 | XCTAssertEqual(tv.ipAddress, "192.168.0.1") 28 | } 29 | 30 | func testNoIPAddress_fromInvalidTVURL() { 31 | // when 32 | let tv = TV(id: "", name: "", type: "", uri: "") 33 | // then 34 | XCTAssertNil(tv.ipAddress) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVFetcher+Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVFetcher+Errors.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 3/30/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum TVFetcherError: Error, Equatable { 11 | public static func == (lhs: TVFetcherError, rhs: TVFetcherError) -> Bool { 12 | switch (lhs, rhs) { 13 | case (.invalidURL, .invalidURL): 14 | return true 15 | case (.failedRequest(let error1, let response1), .failedRequest(let error2, let response2)): 16 | return "\(String(describing: error1)), \(String(describing: response1))" == "\(String(describing: error2)), \(String(describing: response2))" 17 | case (.unexpectedResponseBody(let data1), .unexpectedResponseBody(let data2)): 18 | return data1 == data2 19 | default: 20 | return false 21 | } 22 | } 23 | 24 | // invalid tv uri 25 | case invalidURL 26 | // http request failed or received failure response 27 | case failedRequest(Error?, HTTPURLResponse?) 28 | // unexpected response body returned 29 | case unexpectedResponseBody(Data) 30 | } 31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TVCommanderKit", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_14), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "TVCommanderKit", 16 | targets: ["TVCommanderKit"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | .package(url: "https://github.com/daltoniam/Starscream.git", branch: "master"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "TVCommanderKit", 27 | dependencies: ["SmartView", "Starscream"]), 28 | .binaryTarget( 29 | name: "SmartView", 30 | path: "SmartView.xcframework"), 31 | .testTarget( 32 | name: "TVCommanderKitTests", 33 | dependencies: ["TVCommanderKit"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/Headers/SmartView.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2014 Samsung Electronics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | #import 26 | 27 | //! Project version number for SmartView framework. 28 | FOUNDATION_EXPORT double SVFVersionNumber; 29 | 30 | //! Project version string for SmartView framework. 31 | FOUNDATION_EXPORT const unsigned char SVFVersionString[]; 32 | 33 | 34 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/Headers/SmartView.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2014 Samsung Electronics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | #import 26 | 27 | //! Project version number for SmartView framework. 28 | FOUNDATION_EXPORT double SVFVersionNumber; 29 | 30 | //! Project version string for SmartView framework. 31 | FOUNDATION_EXPORT const unsigned char SVFVersionString[]; 32 | 33 | 34 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Headers/SmartView.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2014 Samsung Electronics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | #import 26 | 27 | //! Project version number for SmartView framework. 28 | FOUNDATION_EXPORT double SVFVersionNumber; 29 | 30 | //! Project version string for SmartView framework. 31 | FOUNDATION_EXPORT const unsigned char SVFVersionString[]; 32 | 33 | 34 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVFetcher.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 3/30/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public class TVFetcher { 11 | private let session: URLSession 12 | private var dataTask: URLSessionDataTask? 13 | 14 | public init(session: URLSession = .shared) { 15 | self.session = session 16 | } 17 | 18 | public func fetchDevice(for tv: TV, completion: @escaping (Result) -> Void) { 19 | guard let url = URL(string: tv.uri) else { 20 | completion(.failure(.invalidURL)) 21 | return 22 | } 23 | cancelFetch() 24 | dataTask = session.dataTask(with: url) { data, response, error in 25 | guard error == nil, 26 | let httpResponse = response as? HTTPURLResponse, 27 | (200...299).contains(httpResponse.statusCode), 28 | let data = data else { 29 | completion(.failure(.failedRequest(error, response as? HTTPURLResponse))) 30 | return 31 | } 32 | do { 33 | let updatedTV = try JSONDecoder().decode(TV.self, from: data) 34 | completion(.success(updatedTV)) 35 | } catch { 36 | completion(.failure(.unexpectedResponseBody(data))) 37 | } 38 | } 39 | dataTask?.resume() 40 | } 41 | 42 | public func cancelFetch() { 43 | dataTask?.cancel() 44 | } 45 | 46 | deinit { 47 | cancelFetch() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SmartView.xcframework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AvailableLibraries 6 | 7 | 8 | LibraryIdentifier 9 | ios-arm64_x86_64-simulator 10 | LibraryPath 11 | SmartView.framework 12 | SupportedArchitectures 13 | 14 | arm64 15 | x86_64 16 | 17 | SupportedPlatform 18 | ios 19 | SupportedPlatformVariant 20 | simulator 21 | 22 | 23 | LibraryIdentifier 24 | ios-arm64_x86_64-maccatalyst 25 | LibraryPath 26 | SmartView.framework 27 | SupportedArchitectures 28 | 29 | arm64 30 | x86_64 31 | 32 | SupportedPlatform 33 | ios 34 | SupportedPlatformVariant 35 | maccatalyst 36 | 37 | 38 | LibraryIdentifier 39 | ios-arm64 40 | LibraryPath 41 | SmartView.framework 42 | SupportedArchitectures 43 | 44 | arm64 45 | 46 | SupportedPlatform 47 | ios 48 | 49 | 50 | CFBundlePackageType 51 | XFWK 52 | XCFrameworkFormatVersion 53 | 1.0 54 | 55 | 56 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVWebSocketBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVWebSocketBuilder.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 5/11/24. 6 | // 7 | 8 | import Foundation 9 | import Starscream 10 | 11 | class TVWebSocketBuilder { 12 | private var urlRequest: URLRequest? 13 | private var certPinner: CertificatePinning? 14 | private var engine: Engine? 15 | private var delegate: WebSocketDelegate? 16 | 17 | func setURLRequest(_ urlRequest: URLRequest) { 18 | self.urlRequest = urlRequest 19 | } 20 | 21 | func setCertPinner(_ certPinner: CertificatePinning) { 22 | self.certPinner = certPinner 23 | } 24 | 25 | func setEngine(_ engine: Engine) { 26 | self.engine = engine 27 | } 28 | 29 | func setDelegate(_ delegate: WebSocketDelegate) { 30 | self.delegate = delegate 31 | } 32 | 33 | func getWebSocket() -> WebSocket? { 34 | let webSocket = createWebSocket() 35 | resetBuilder() 36 | return webSocket 37 | } 38 | 39 | private func createWebSocket() -> WebSocket? { 40 | guard let urlRequest else { return nil } 41 | let webSocket: WebSocket = 42 | // prioritize using custom engine (for tests/mocks/etc) 43 | engine.flatMap { .init(request: urlRequest, engine: $0) } 44 | // otherwise, default to using cert pinner 45 | ?? .init(request: urlRequest, certPinner: certPinner) 46 | webSocket.delegate = delegate 47 | webSocket.respondToPingWithPong = true 48 | return webSocket 49 | } 50 | 51 | private func resetBuilder() { 52 | urlRequest = nil 53 | certPinner = nil 54 | engine = nil 55 | delegate = nil 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 22C65 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | SmartView 11 | CFBundleIdentifier 12 | com.samsung.sta.multiscreen.MSF 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | SmartView 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 3.1.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 1 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 14B47b 33 | DTPlatformName 34 | macosx 35 | DTPlatformVersion 36 | 13.0 37 | DTSDKBuild 38 | 22A372 39 | DTSDKName 40 | macosx13.0 41 | DTXcode 42 | 1410 43 | DTXcodeBuild 44 | 14B47b 45 | LSMinimumSystemVersion 46 | 11.0 47 | NSAppTransportSecurity 48 | 49 | NSAllowsArbitraryLoads 50 | 51 | 52 | UIDeviceFamily 53 | 54 | 2 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/MockURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLProtocol.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/16/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class MockURLProtocol: URLProtocol { 11 | struct Stub { 12 | let data: Data? 13 | let response: HTTPURLResponse? 14 | let error: Error? 15 | 16 | init(data: Data? = nil, response: HTTPURLResponse? = nil, error: Error? = nil) { 17 | self.data = data 18 | self.response = response 19 | self.error = error 20 | } 21 | } 22 | 23 | enum StubError: Error { 24 | case requestNotStubbed 25 | } 26 | 27 | override class func canInit(with request: URLRequest) -> Bool { 28 | true 29 | } 30 | 31 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 32 | request 33 | } 34 | 35 | private static var stubs = [URL: Stub]() 36 | 37 | static func expect(_ stub: Stub, for url: URL) { 38 | stubs[url] = stub 39 | } 40 | 41 | static func reset() { 42 | stubs.removeAll() 43 | } 44 | 45 | override func startLoading() { 46 | guard let url = request.url, let stub = Self.stubs[url] else { 47 | client?.urlProtocol(self, didFailWithError: StubError.requestNotStubbed) 48 | return 49 | } 50 | if let response = stub.response { 51 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 52 | } 53 | if let data = stub.data { 54 | client?.urlProtocol(self, didLoad: data) 55 | } 56 | if let error = stub.error { 57 | client?.urlProtocol(self, didFailWithError: error) 58 | } 59 | client?.urlProtocolDidFinishLoading(self) 60 | } 61 | 62 | override func stopLoading() { 63 | } 64 | } 65 | 66 | extension URLSession { 67 | static func mock() -> URLSession { 68 | let sessionConfiguration = URLSessionConfiguration.ephemeral 69 | sessionConfiguration.protocolClasses = [MockURLProtocol.self] 70 | return URLSession(configuration: sessionConfiguration) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVWebSocketHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVWebSocketHandler.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/17/24. 6 | // 7 | 8 | import Foundation 9 | import Starscream 10 | 11 | protocol TVWebSocketHandlerDelegate: AnyObject { 12 | func webSocketDidConnect() 13 | func webSocketDidDisconnect() 14 | func webSocketDidReadAuthStatus(_ authStatus: TVAuthStatus) 15 | func webSocketDidReadAuthToken(_ authToken: TVAuthToken) 16 | func webSocketError(_ error: TVCommanderError) 17 | } 18 | 19 | class TVWebSocketHandler { 20 | private let decoder = JSONDecoder() 21 | weak var delegate: TVWebSocketHandlerDelegate? 22 | 23 | // MARK: Interact with WebSocket 24 | 25 | func didReceive(event: WebSocketEvent, client: WebSocketClient) { 26 | switch event { 27 | case .connected: 28 | delegate?.webSocketDidConnect() 29 | case .cancelled, .disconnected: 30 | delegate?.webSocketDidDisconnect() 31 | case .text(let text): 32 | handleWebSocketText(text) 33 | case .binary(let data): 34 | webSocketDidReadPacket(data) 35 | case .error(let error): 36 | delegate?.webSocketError(.webSocketError(error)) 37 | default: 38 | break 39 | } 40 | } 41 | 42 | private func handleWebSocketText(_ text: String) { 43 | if let packetData = text.asData { 44 | webSocketDidReadPacket(packetData) 45 | } else { 46 | delegate?.webSocketError(.packetDataParsingFailed) 47 | } 48 | } 49 | 50 | private func webSocketDidReadPacket(_ packet: Data) { 51 | if let authResponse = parseAuthResponse(from: packet) { 52 | handleAuthResponse(authResponse) 53 | } else { 54 | delegate?.webSocketError(.packetDataParsingFailed) 55 | } 56 | } 57 | 58 | // MARK: Receive Auth 59 | 60 | private func parseAuthResponse(from packet: Data) -> TVAuthResponse? { 61 | try? decoder.decode(TVAuthResponse.self, from: packet) 62 | } 63 | 64 | private func handleAuthResponse(_ response: TVAuthResponse) { 65 | switch response.event { 66 | case .connect: 67 | parseTokenFromAuthResponse(response) 68 | delegate?.webSocketDidReadAuthStatus(.allowed) 69 | case .unauthorized: 70 | delegate?.webSocketDidReadAuthStatus(.denied) 71 | case .timeout: 72 | delegate?.webSocketDidReadAuthStatus(.none) 73 | default: 74 | delegate?.webSocketError(.authResponseUnexpectedChannelEvent(response)) 75 | } 76 | } 77 | 78 | private func parseTokenFromAuthResponse(_ response: TVAuthResponse) { 79 | if let newToken = response.data?.token { 80 | delegate?.webSocketDidReadAuthToken(newToken) 81 | } else if let refreshedToken = response.data?.clients.first?.attributes.token { 82 | delegate?.webSocketDidReadAuthToken(refreshedToken) 83 | } else { 84 | delegate?.webSocketError(.noTokenInAuthResponse(response)) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVCommander+Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVCommander+Errors.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 9/4/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum TVCommanderError: LocalizedError { 11 | // invalid app name 12 | case invalidAppNameEntered 13 | // invalid ip address 14 | case invalidIPAddressEntered 15 | // trying to connect, but the connection is already established. 16 | case connectionAlreadyEstablished 17 | // the URL could not be constructed. 18 | case urlConstructionFailed 19 | // WebSocket receives an error. 20 | case webSocketError(Error?) 21 | // parsing for packet data fails. 22 | case packetDataParsingFailed 23 | // response for authentication contains unexpected event 24 | case authResponseUnexpectedChannelEvent(TVAuthResponse) 25 | // no token is found inside an allowed authentication response. 26 | case noTokenInAuthResponse(TVAuthResponse) 27 | // trying to send a command without being connected to a TV 28 | case remoteCommandNotConnectedToTV 29 | // trying to send a command when authentication status is not allowed. 30 | case remoteCommandAuthenticationStatusNotAllowed 31 | // command conversion to a string fails. 32 | case commandConversionToStringFailed 33 | // invalid input to keyboard navigation 34 | case keyboardCharNotFound(String) 35 | // wake on LAN connection error 36 | case wakeOnLANConnectionError(Error) 37 | // wake on LAN content processing error 38 | case wakeOnLANProcessingError(Error) 39 | // an unknown error occurs. 40 | case unknownError(Error?) 41 | 42 | public var errorDescription: String? { 43 | switch self { 44 | case .invalidAppNameEntered: 45 | "Invalid App Name Entered" 46 | case .invalidIPAddressEntered: 47 | "Invalid IP Address Entered" 48 | case .connectionAlreadyEstablished: 49 | "Connection Already Established" 50 | case .urlConstructionFailed: 51 | "URL Construction Failed" 52 | case .webSocketError(let error): 53 | "WebSocket Error: \(String(describing: error))" 54 | case .packetDataParsingFailed: 55 | "Packet Data Parsing Failed" 56 | case .authResponseUnexpectedChannelEvent(let authResponse): 57 | "Auth Response Unexpected Channel Event: \(authResponse)" 58 | case .noTokenInAuthResponse(let authResponse): 59 | "No Token In Auth Response: \(authResponse)" 60 | case .remoteCommandNotConnectedToTV: 61 | "Remote Command Not Connected To TV" 62 | case .remoteCommandAuthenticationStatusNotAllowed: 63 | "Remote Command Authentication Status Not Allowed" 64 | case .commandConversionToStringFailed: 65 | "Command Conversion To String Failed" 66 | case .keyboardCharNotFound(let char): 67 | "Keyboard Char Not Found: \(char)" 68 | case .wakeOnLANConnectionError(let error): 69 | "Wake On LAN Connection Error: \(error.localizedDescription)" 70 | case .wakeOnLANProcessingError(let error): 71 | "Wake On LAN Processing Error: \(error.localizedDescription)" 72 | case .unknownError(let error): 73 | "Unknown Error: \(error?.localizedDescription ?? "")" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVSearcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVSearcher.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/14/24. 6 | // 7 | 8 | import Foundation 9 | import SmartView 10 | 11 | public protocol TVSearching: AnyObject { 12 | func addSearchObserver(_ observer: any TVSearchObserving) 13 | func removeSearchObserver(_ observer: any TVSearchObserving) 14 | func removeAllSearchObservers() 15 | func configureTargetTVId(_ targetTVId: TV.ID?) 16 | func startSearch() 17 | func stopSearch() 18 | } 19 | 20 | public protocol TVSearchRemoteInterfacing: AnyObject { 21 | func setDelegate(_ observer: TVSearchObserving) 22 | func startSearch() 23 | func stopSearch() 24 | } 25 | 26 | public protocol TVSearchObserving: AnyObject { 27 | func tvSearchDidStart() 28 | func tvSearchDidStop() 29 | func tvSearchDidFindTV(_ tv: TV) 30 | func tvSearchDidLoseTV(_ tv: TV) 31 | } 32 | 33 | public class TVSearcher: TVSearching, TVSearchObserving { 34 | private let remote: TVSearchRemoteInterfacing 35 | private var observers = [TVSearchObserving]() 36 | private var targetTVId: TV.ID? 37 | 38 | public init(remote: TVSearchRemoteInterfacing? = nil) { 39 | self.remote = remote ?? TVSearchAdaptor() 40 | self.remote.setDelegate(self) 41 | } 42 | 43 | // MARK: Add / Remove Observers 44 | 45 | public func addSearchObserver(_ observer: any TVSearchObserving) { 46 | observers.append(observer) 47 | } 48 | 49 | public func removeSearchObserver(_ observer: any TVSearchObserving) { 50 | observers.removeAll { $0 === observer } 51 | } 52 | 53 | public func removeAllSearchObservers() { 54 | observers.removeAll() 55 | } 56 | 57 | // MARK: Search for TVs 58 | 59 | public func configureTargetTVId(_ targetTVId: TV.ID?) { 60 | self.targetTVId = targetTVId 61 | } 62 | 63 | public func startSearch() { 64 | remote.startSearch() 65 | } 66 | 67 | public func stopSearch() { 68 | remote.stopSearch() 69 | } 70 | 71 | public func tvSearchDidStart() { 72 | observers.forEach { 73 | $0.tvSearchDidStart() 74 | } 75 | } 76 | 77 | public func tvSearchDidStop() { 78 | observers.forEach { 79 | $0.tvSearchDidStop() 80 | } 81 | } 82 | 83 | public func tvSearchDidFindTV(_ tv: TV) { 84 | observers.forEach { 85 | $0.tvSearchDidFindTV(tv) 86 | } 87 | if tv.id == targetTVId { 88 | stopSearch() 89 | } 90 | } 91 | 92 | public func tvSearchDidLoseTV(_ tv: TV) { 93 | observers.forEach { 94 | $0.tvSearchDidLoseTV(tv) 95 | } 96 | } 97 | } 98 | 99 | class TVSearchAdaptor: TVSearchRemoteInterfacing, ServiceSearchDelegate { 100 | private let search: ServiceSearch 101 | private weak var delegate: TVSearchObserving? 102 | 103 | init() { 104 | self.search = Service.search() 105 | self.search.delegate = self 106 | } 107 | 108 | func setDelegate(_ observer: any TVSearchObserving) { 109 | self.delegate = observer 110 | } 111 | 112 | func startSearch() { 113 | search.start() 114 | } 115 | 116 | func stopSearch() { 117 | search.stop() 118 | } 119 | 120 | func onStart() { 121 | delegate?.tvSearchDidStart() 122 | } 123 | 124 | func onStop() { 125 | delegate?.tvSearchDidStop() 126 | } 127 | 128 | func onServiceFound(_ service: Service) { 129 | delegate?.tvSearchDidFindTV(.init(service: service)) 130 | } 131 | 132 | func onServiceLost(_ service: Service) { 133 | delegate?.tvSearchDidLoseTV(.init(service: service)) 134 | } 135 | } 136 | 137 | extension TV { 138 | init(service: Service) { 139 | self.init( 140 | id: service.id, 141 | name: service.name, 142 | type: service.type, 143 | uri: service.uri 144 | ) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVSearcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVSearcherTests.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/14/24. 6 | // 7 | 8 | import XCTest 9 | @testable import TVCommanderKit 10 | 11 | final class TVSearcherTests: XCTestCase { 12 | private var service: TVSearching! 13 | private var mockRemote: MockTVSearchRemoteInterface! 14 | private var mockObserver: MockTVSearchObserver! 15 | 16 | override func setUp() { 17 | mockRemote = MockTVSearchRemoteInterface() 18 | mockObserver = MockTVSearchObserver() 19 | service = TVSearcher(remote: mockRemote) 20 | service.addSearchObserver(mockObserver) 21 | } 22 | 23 | override func tearDown() { 24 | service.removeAllSearchObservers() 25 | service = nil 26 | mockRemote = nil 27 | mockObserver = nil 28 | } 29 | 30 | private func mockTV() -> TV { 31 | .init( 32 | id: "uuid:\(UUID().uuidString)", 33 | name: "Samsung TV", 34 | type: "Type of Samsung TV", 35 | uri: "http://192.168.0.1:8001/api/v2/" 36 | ) 37 | } 38 | 39 | func testStartSearch_notifiesObserver() { 40 | service.startSearch() 41 | XCTAssertTrue(mockObserver.didStartSearch) 42 | } 43 | 44 | func testStopSearch_notifiesObserver() { 45 | service.stopSearch() 46 | XCTAssertTrue(mockObserver.didStopSearch) 47 | } 48 | 49 | func testFindTV_notifiesObserver() { 50 | let expectedTV = mockTV() 51 | mockRemote.findTV = expectedTV 52 | mockObserver.expectTV = expectedTV 53 | service.startSearch() 54 | XCTAssertTrue(mockObserver.didFindTV) 55 | XCTAssertEqual(mockObserver.foundTV?.id, expectedTV.id) 56 | } 57 | 58 | func testLoseTV_notifiesObserver() { 59 | let expectedTV = mockTV() 60 | mockRemote.loseTV = expectedTV 61 | mockObserver.expectTV = expectedTV 62 | service.startSearch() 63 | XCTAssertTrue(mockObserver.didLoseTV) 64 | XCTAssertEqual(mockObserver.lostTV?.id, expectedTV.id) 65 | } 66 | 67 | func testConfigureTargetAndFindTV_notifiesObserverAndStopsSearch() { 68 | let expectedTV = mockTV() 69 | mockRemote.findTV = expectedTV 70 | service.configureTargetTVId(expectedTV.id) 71 | service.startSearch() 72 | XCTAssertTrue(mockObserver.didStartSearch) 73 | XCTAssertTrue(mockObserver.didStopSearch) 74 | } 75 | 76 | func testConfigureTargetAndFindOtherTV_notifiesObserverAndContinuesSearch() { 77 | let targetTV = mockTV() 78 | let otherTV = mockTV() 79 | mockRemote.findTV = otherTV 80 | service.configureTargetTVId(targetTV.id) 81 | service.startSearch() 82 | XCTAssertTrue(mockObserver.didStartSearch) 83 | XCTAssertFalse(mockObserver.didStopSearch) 84 | } 85 | 86 | func testMultipleObservers_notified() { 87 | let anotherObserver = MockTVSearchObserver() 88 | service.addSearchObserver(anotherObserver) 89 | service.startSearch() 90 | XCTAssertTrue(mockObserver.didStartSearch) 91 | XCTAssertTrue(anotherObserver.didStartSearch) 92 | service.stopSearch() 93 | XCTAssertTrue(mockObserver.didStopSearch) 94 | XCTAssertTrue(anotherObserver.didStopSearch) 95 | service.removeSearchObserver(anotherObserver) 96 | } 97 | 98 | func testRemoveObserver_notNotified() { 99 | service.removeSearchObserver(mockObserver) 100 | service.startSearch() 101 | XCTAssertFalse(mockObserver.didStartSearch) 102 | } 103 | } 104 | 105 | private class MockTVSearchRemoteInterface: TVSearchRemoteInterfacing { 106 | private weak var delegate: TVSearchObserving! 107 | var findTV: TV? 108 | var loseTV: TV? 109 | 110 | func setDelegate(_ observer: any TVSearchObserving) { 111 | self.delegate = observer 112 | } 113 | 114 | func startSearch() { 115 | delegate.tvSearchDidStart() 116 | findTV.flatMap { delegate.tvSearchDidFindTV($0) } 117 | loseTV.flatMap { delegate.tvSearchDidLoseTV($0) } 118 | } 119 | 120 | func stopSearch() { 121 | delegate.tvSearchDidStop() 122 | } 123 | } 124 | 125 | private class MockTVSearchObserver: TVSearchObserving { 126 | var didStartSearch = false 127 | var didStopSearch = false 128 | var didFindTV = false 129 | var didLoseTV = false 130 | var foundTV: TV? 131 | var lostTV: TV? 132 | var expectTV: TV? 133 | 134 | func tvSearchDidStart() { 135 | didStartSearch = true 136 | } 137 | 138 | func tvSearchDidStop() { 139 | didStopSearch = true 140 | } 141 | 142 | func tvSearchDidFindTV(_ tv: TV) { 143 | if let expectTV, expectTV.id == tv.id { 144 | foundTV = tv 145 | didFindTV = true 146 | } 147 | } 148 | 149 | func tvSearchDidLoseTV(_ tv: TV) { 150 | if let expectTV, expectTV.id == tv.id { 151 | lostTV = tv 152 | didLoseTV = true 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVAppManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVAppManager.swift 3 | // TVCommanderKit 4 | // 5 | // Created by Wilson Desimini on 10/17/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol TVAppManaging { 11 | func fetchStatus(for tvApp: TVApp, tvIPAddress: String) async throws -> TVAppStatus 12 | func launch(tvApp: TVApp, tvIPAddress: String) async throws 13 | } 14 | 15 | enum TVAppManagerError: LocalizedError { 16 | case badURL(description: String) 17 | case noData 18 | case networkError(description: String) 19 | 20 | var errorDescription: String? { 21 | switch self { 22 | case .badURL(let description): 23 | return "Bad URL: \(description)" 24 | case .noData: 25 | return "No data received from the network." 26 | case .networkError(let description): 27 | return "Network error: \(description)" 28 | } 29 | } 30 | } 31 | 32 | public class TVAppManager: TVAppManaging { 33 | private let urlBuilder: TVAppURLBuilding 34 | private let networkManager: TVAppNetworkManaging 35 | private let decoder: TVAppDecoding 36 | 37 | public init( 38 | urlBuilder: TVAppURLBuilding = TVAppURLBuilder(), 39 | networkManager: TVAppNetworkManaging = TVAppNetworkManager(), 40 | decoder: TVAppDecoding = TVAppDecoder() 41 | ) { 42 | self.urlBuilder = urlBuilder 43 | self.networkManager = networkManager 44 | self.decoder = decoder 45 | } 46 | 47 | public func fetchStatus(for tvApp: TVApp, tvIPAddress: String) async throws -> TVAppStatus { 48 | let url = try buildURL(tvApp: tvApp, tvIPAddress: tvIPAddress) 49 | guard let data = try await networkManager.sendRequest(url: url) else { 50 | throw TVAppManagerError.noData 51 | } 52 | return try decoder.decodeAppStatus(from: data) 53 | } 54 | 55 | public func launch(tvApp: TVApp, tvIPAddress: String) async throws { 56 | let url = try buildURL(tvApp: tvApp, tvIPAddress: tvIPAddress) 57 | try await networkManager.sendRequest(url: url, method: "POST") 58 | } 59 | 60 | private func buildURL(tvApp: TVApp, tvIPAddress: String) throws -> URL { 61 | if let url = urlBuilder.buildURL(tvIPAddress: tvIPAddress, tvAppId: tvApp.id) { 62 | return url 63 | } else { 64 | throw TVAppManagerError.badURL(description: "Unable to build URL for IP: \(tvIPAddress)") 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Build App URL 70 | 71 | public protocol TVAppURLBuilding { 72 | func buildURL(tvIPAddress: String, tvAppId: String) -> URL? 73 | } 74 | 75 | public class TVAppURLBuilder: TVAppURLBuilding { 76 | public init() { 77 | } 78 | 79 | public func buildURL(tvIPAddress: String, tvAppId: String) -> URL? { 80 | var components = URLComponents() 81 | components.scheme = "http" 82 | components.host = tvIPAddress 83 | components.port = 8001 84 | components.path = "/api/v2/applications/\(tvAppId)" 85 | return components.url 86 | } 87 | } 88 | 89 | // MARK: - Send HTTP Request 90 | 91 | public protocol TVAppNetworkManaging { 92 | @discardableResult 93 | func sendRequest(url: URL, method: String) async throws -> Data? 94 | } 95 | 96 | extension TVAppNetworkManaging { 97 | func sendRequest(url: URL) async throws -> Data? { 98 | try await sendRequest(url: url, method: "GET") 99 | } 100 | } 101 | 102 | enum TVAppNetworkError: LocalizedError { 103 | case appNotFound 104 | case invalidResponse 105 | 106 | var errorDescription: String? { 107 | switch self { 108 | case .appNotFound: 109 | return "App Not Found" 110 | case .invalidResponse: 111 | return "Invalid Response" 112 | } 113 | } 114 | } 115 | 116 | public class TVAppNetworkManager: TVAppNetworkManaging { 117 | private let session: URLSession 118 | 119 | public init(session: URLSession = .shared) { 120 | self.session = session 121 | } 122 | 123 | public func sendRequest(url: URL, method: String) async throws -> Data? { 124 | var request = URLRequest(url: url) 125 | request.httpMethod = method 126 | let (data, response) = try await session.data(for: request) 127 | try validate(response: response) 128 | return data 129 | } 130 | 131 | private func validate(response: URLResponse) throws { 132 | switch (response as? HTTPURLResponse)?.statusCode { 133 | case 200: 134 | return 135 | case 404: 136 | throw TVAppNetworkError.appNotFound 137 | default: 138 | throw TVAppNetworkError.invalidResponse 139 | } 140 | } 141 | } 142 | 143 | // MARK: Decode App Status 144 | 145 | public protocol TVAppDecoding { 146 | func decodeAppStatus(from data: Data) throws -> TVAppStatus 147 | } 148 | 149 | public class TVAppDecoder: TVAppDecoding { 150 | private let decoder: JSONDecoder 151 | 152 | public init(decoder: JSONDecoder = .init()) { 153 | self.decoder = decoder 154 | } 155 | 156 | public func decodeAppStatus(from data: Data) throws -> TVAppStatus { 157 | try decoder.decode(TVAppStatus.self, from: data) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVFetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVFetcherTests.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 3/30/24. 6 | // 7 | 8 | import XCTest 9 | @testable import TVCommanderKit 10 | 11 | final class TVFetcherTests: XCTestCase { 12 | private var tvFetcher: TVFetcher! 13 | private var tv: TV! 14 | private var responseData: Data! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | tvFetcher = TVFetcher(session: .mock()) 19 | tv = TV( 20 | id: "uuid:4B0307C2-919B-4613-889A-F2D52F8538BC", 21 | name: "Samsung Q7DAA 55 TV", 22 | remote: "1.0", 23 | type: "Samsung SmartTV", 24 | uri: "http://192.168.0.1:8001/api/v2/", 25 | version: "2.0.25" 26 | ) 27 | } 28 | 29 | override func tearDown() { 30 | tvFetcher = nil 31 | tv = nil 32 | super.tearDown() 33 | } 34 | 35 | func testFetchDevice_Success() throws { 36 | // given 37 | let url = try XCTUnwrap(URL(string: tv.uri)) 38 | let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) 39 | let responseData = #"{"device":{"TokenAuthSupport":"true","wifiMac":"00:00:00:00:0A:AA"},"id":"uuid:4B0307C2-919B-4613-889A-F2D52F8538BC","name":"Samsung Q7DAA 55 TV","remote":"1.0","type":"Samsung SmartTV","uri":"http://192.168.0.1:8001/api/v2/","version":"2.0.25"}"#.data(using: .utf8) 40 | MockURLProtocol.expect(.init(data: responseData, response: response), for: url) 41 | // when 42 | let expectation = XCTestExpectation(description: "Fetch device success") 43 | tvFetcher.fetchDevice(for: tv) { result in 44 | // then 45 | switch result { 46 | case .success(let updatedTV): 47 | XCTAssertEqual(updatedTV.device?.tokenAuthSupport, "true") 48 | XCTAssertEqual(updatedTV.device?.wifiMac, "00:00:00:00:0A:AA") 49 | case .failure(let error): 50 | XCTFail("Expected success, but got failure: \(error)") 51 | } 52 | expectation.fulfill() 53 | } 54 | wait(for: [expectation], timeout: 1) 55 | } 56 | 57 | func testFetchDevice_InvalidURL() { 58 | // given 59 | tv = .init(id: tv.id, name: tv.name, type: tv.type, uri: "") 60 | // when 61 | let expectation = XCTestExpectation(description: "Fetch device invalid URL") 62 | tvFetcher.fetchDevice(for: tv) { result in 63 | // then 64 | switch result { 65 | case .success(_): 66 | XCTFail("Expected failure due to invalid URL, but got success") 67 | case .failure(let error): 68 | XCTAssertEqual(error, .invalidURL) 69 | } 70 | expectation.fulfill() 71 | } 72 | wait(for: [expectation], timeout: 1) 73 | } 74 | 75 | func testFetchDevice_FailedRequest() throws { 76 | // given 77 | let expectedError = NSError(domain: "SampleErrorDomain", code: 404, userInfo: nil) 78 | let url = try XCTUnwrap(URL(string: tv.uri)) 79 | MockURLProtocol.expect(.init(error: expectedError), for: url) 80 | // when 81 | let expectation = XCTestExpectation(description: "Fetch device failed request") 82 | tvFetcher.fetchDevice(for: tv) { result in 83 | // then 84 | switch result { 85 | case .success(_): 86 | XCTFail("Expected failure due to failed request, but got success") 87 | case .failure(let fetchError): 88 | switch fetchError { 89 | case .failedRequest(let error, _): 90 | XCTAssertEqual((error as NSError?)?.code, expectedError.code) 91 | XCTAssertEqual((error as NSError?)?.domain, expectedError.domain) 92 | // useInfo not compare/tested here - generated and filled in 93 | // by native code somewhere between URLProtocol and URLSession handler 94 | default: 95 | XCTFail("Expecting failed request error") 96 | } 97 | } 98 | expectation.fulfill() 99 | } 100 | wait(for: [expectation], timeout: 1) 101 | } 102 | 103 | func testFetchDevice_UnexpectedResponseBody() throws { 104 | // given 105 | let url = try XCTUnwrap(URL(string: tv.uri)) 106 | let invalidResponseData = Data() 107 | let response = HTTPURLResponse( 108 | url: URL(string: tv.uri)!, 109 | statusCode: 200, 110 | httpVersion: nil, 111 | headerFields: nil 112 | ) 113 | MockURLProtocol.expect(.init(data: invalidResponseData, response: response), for: url) 114 | // when 115 | let expectation = XCTestExpectation(description: "Fetch device unexpected response body") 116 | tvFetcher.fetchDevice(for: tv) { result in 117 | // then 118 | switch result { 119 | case .success(_): 120 | XCTFail("Expected failure due to unexpected response body, but got success") 121 | case .failure(let error): 122 | XCTAssertEqual(error, .unexpectedResponseBody(invalidResponseData)) 123 | } 124 | expectation.fulfill() 125 | } 126 | wait(for: [expectation], timeout: 1) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVWebSocketHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVWebSocketHandlerTests.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 4/17/24. 6 | // 7 | 8 | import XCTest 9 | import Starscream 10 | @testable import TVCommanderKit 11 | 12 | final class TVWebSocketHandlerTests: XCTestCase { 13 | private var handler: TVWebSocketHandler! 14 | private var mockClient: MockWebSocketClient! 15 | private var delegate: MockTVWebSocketHandlerDelegate! 16 | 17 | override func setUp() { 18 | handler = TVWebSocketHandler() 19 | mockClient = MockWebSocketClient() 20 | delegate = MockTVWebSocketHandlerDelegate() 21 | handler.delegate = delegate 22 | } 23 | 24 | override func tearDown() { 25 | handler = nil 26 | mockClient = nil 27 | delegate = nil 28 | } 29 | 30 | func testWebSocketDidConnect() { 31 | handler.didReceive(event: .connected([:]), client: mockClient) 32 | XCTAssertTrue(delegate.didConnect) 33 | } 34 | 35 | func testWebSocketDidDisconnect() { 36 | handler.didReceive(event: .disconnected("", 0), client: mockClient) 37 | XCTAssertTrue(delegate.didDisconnect) 38 | } 39 | 40 | func testWebSocketCancelledTreatedAsDisconnect() { 41 | handler.didReceive(event: .cancelled, client: mockClient) 42 | XCTAssertTrue(delegate.didDisconnect) 43 | } 44 | 45 | func testReceiveTextWithValidNewAuthPacket() { 46 | let jsonString = #"{"data":{"clients":[{"attributes":{"name":"VGVzdA=="},"connectTime":1713369027676,"deviceName":"VGVzdA==","id":"502e895e-251f-48ca-b786-0f83b20102c5","isHost":false}],"id":"502e895e-251f-48ca-b786-0f83b20102c5","token":"99999999"},"event":"ms.channel.connect"}"# 47 | handler.didReceive(event: .text(jsonString), client: mockClient) 48 | XCTAssertEqual(delegate.lastAuthToken, "99999999") 49 | XCTAssertEqual(delegate.lastAuthStatus, .allowed) 50 | } 51 | 52 | func testReceiveTextWithValidRefreshedAuthPacket() { 53 | let jsonString = #"{"data":{"clients":[{"attributes":{"name":"VGVzdA==","token":"99999999"},"connectTime":1713369027676,"deviceName":"VGVzdA==","id":"502e895e-251f-48ca-b786-0f83b20102c5","isHost":false}],"id":"502e895e-251f-48ca-b786-0f83b20102c5"},"event":"ms.channel.connect"}"# 54 | handler.didReceive(event: .text(jsonString), client: mockClient) 55 | XCTAssertEqual(delegate.lastAuthToken, "99999999") 56 | XCTAssertEqual(delegate.lastAuthStatus, .allowed) 57 | } 58 | 59 | func testReceiveTextWithInvalidPacket() { 60 | let jsonString = "not json" 61 | handler.didReceive(event: .text(jsonString), client: mockClient) 62 | XCTAssertNotNil(delegate.lastError) 63 | } 64 | 65 | func testReceiveTextWithTimeoutPacket() { 66 | let jsonString = #"{"event":"ms.channel.timeOut"}"# 67 | handler.didReceive(event: .text(jsonString), client: mockClient) 68 | XCTAssertEqual(delegate.lastAuthStatus, TVAuthStatus.none) 69 | } 70 | 71 | func testReceiveTextWithUnauthorizedPacket() { 72 | let jsonString = #"{"event":"ms.channel.unauthorized"}"# 73 | handler.didReceive(event: .text(jsonString), client: mockClient) 74 | XCTAssertEqual(delegate.lastAuthStatus, .denied) 75 | } 76 | 77 | func testReceiveInvalidBinaryData() { 78 | let jsonData = Data() 79 | handler.didReceive(event: .binary(jsonData), client: mockClient) 80 | XCTAssertNotNil(delegate.lastError) 81 | } 82 | 83 | func testWebSocketError() { 84 | let error = NSError(domain: "Test", code: 1001, userInfo: nil) 85 | handler.didReceive(event: .error(error), client: mockClient) 86 | XCTAssertNotNil(delegate.lastError) 87 | } 88 | 89 | func testAuthResponseWithNoToken() { 90 | let jsonString = #"{"event":"ms.channel.connect","data":{}}"# 91 | handler.didReceive(event: .text(jsonString), client: mockClient) 92 | XCTAssertNotNil(delegate.lastError) 93 | } 94 | 95 | func testAuthResponseWithUnexpectedEvent() { 96 | let jsonString = #"{"event":"unknownEvent"}"# 97 | handler.didReceive(event: .text(jsonString), client: mockClient) 98 | XCTAssertNotNil(delegate.lastError) 99 | } 100 | } 101 | 102 | private class MockWebSocketClient: WebSocketClient { 103 | var lastSentText: String? 104 | var lastSentData: Data? 105 | 106 | func connect() { 107 | } 108 | 109 | func disconnect(closeCode: UInt16) { 110 | } 111 | 112 | func write(string: String, completion: (() -> ())?) { 113 | lastSentText = string 114 | } 115 | 116 | func write(stringData: Data, completion: (() -> ())?) { 117 | lastSentData = stringData 118 | } 119 | 120 | func write(data: Data, completion: (() -> ())?) { 121 | lastSentData = data 122 | } 123 | 124 | func write(ping: Data, completion: (() -> ())?) { 125 | } 126 | 127 | func write(pong: Data, completion: (() -> ())?) { 128 | } 129 | } 130 | 131 | private class MockTVWebSocketHandlerDelegate: TVWebSocketHandlerDelegate { 132 | var didConnect = false 133 | var didDisconnect = false 134 | var lastAuthStatus: TVAuthStatus? 135 | var lastAuthToken: String? 136 | var lastError: TVCommanderError? 137 | 138 | func webSocketDidConnect() { 139 | didConnect = true 140 | } 141 | 142 | func webSocketDidDisconnect() { 143 | didDisconnect = true 144 | } 145 | 146 | func webSocketDidReadAuthStatus(_ authStatus: TVAuthStatus) { 147 | lastAuthStatus = authStatus 148 | } 149 | 150 | func webSocketDidReadAuthToken(_ authToken: String) { 151 | lastAuthToken = authToken 152 | } 153 | 154 | func webSocketError(_ error: TVCommanderError) { 155 | lastError = error 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVCommanderKit+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVCommanderKit+Extensions.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 9/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: Data 11 | 12 | extension Data { 13 | static func magicPacket(from device: TVWakeOnLANDevice) -> Data { 14 | var magicPacketRaw = [UInt8](repeating: 0xFF, count: 6) 15 | let macAddressData = device.mac.split(separator: ":").compactMap { UInt8($0, radix: 16) } 16 | for _ in 0..<16 { magicPacketRaw.append(contentsOf: macAddressData) } 17 | return Data(magicPacketRaw) 18 | } 19 | 20 | var asJSON: [String: Any]? { 21 | try? JSONSerialization.jsonObject(with: self) as? [String: Any] 22 | } 23 | 24 | var asString: String? { 25 | String(data: self, encoding: .utf8) 26 | } 27 | } 28 | 29 | // MARK: Dictionary 30 | 31 | extension Dictionary { 32 | var asData: Data? { 33 | try? JSONSerialization.data(withJSONObject: self) 34 | } 35 | 36 | var asString: String? { 37 | asData?.asString 38 | } 39 | } 40 | 41 | // MARK: Encodable 42 | 43 | extension Encodable { 44 | func asString(encoder: JSONEncoder = .init()) throws -> String? { 45 | try encoder.encode(self).asString 46 | } 47 | } 48 | 49 | // MARK: String 50 | 51 | extension String { 52 | var isValidAppName: Bool { 53 | !isEmpty 54 | } 55 | 56 | var isValidIPAddress: Bool { 57 | let regex = #"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"# 58 | let predicate = NSPredicate(format: "SELF MATCHES %@", regex) 59 | return predicate.evaluate(with: self) 60 | } 61 | 62 | var asBase64: String? { 63 | asData?.base64EncodedString() 64 | } 65 | 66 | var asData: Data? { 67 | data(using: .utf8) 68 | } 69 | 70 | var asJSON: [String: Any]? { 71 | asData.flatMap(\.asJSON) 72 | } 73 | } 74 | 75 | // MARK: TV 76 | 77 | extension TV { 78 | var ipAddress: String? { 79 | if let httpURLHost = URLComponents(string: uri)?.host, 80 | httpURLHost.isValidIPAddress { 81 | return httpURLHost 82 | } else if let deviceIPAddress = device?.ip, 83 | deviceIPAddress.isValidIPAddress { 84 | return deviceIPAddress 85 | } 86 | return nil 87 | } 88 | 89 | func addingDevice(_ device: TV.Device) -> TV { 90 | TV( 91 | device: device, 92 | id: id, 93 | isSupport: isSupport, 94 | name: name, 95 | remote: remote, 96 | type: type, 97 | uri: uri, 98 | version: version 99 | ) 100 | } 101 | } 102 | 103 | // MARK: TVApp 104 | 105 | extension TVApp { 106 | public static func allApps() -> [TVApp] { 107 | [ 108 | espn(), 109 | hulu(), 110 | max(), 111 | netflix(), 112 | paramountPlus(), 113 | plutoTV(), 114 | primeVideo(), 115 | spotify(), 116 | youtube() 117 | ] 118 | } 119 | 120 | public static func espn() -> TVApp { 121 | TVApp(id: "3201708014618", name: "ESPN") 122 | } 123 | 124 | public static func hulu() -> TVApp { 125 | TVApp(id: "3201601007625", name: "Hulu") 126 | } 127 | 128 | public static func max() -> TVApp { 129 | TVApp(id: "3202301029760", name: "Max") 130 | } 131 | 132 | public static func netflix() -> TVApp { 133 | TVApp(id: "3201907018807", name: "Netflix") 134 | } 135 | 136 | public static func paramountPlus() -> TVApp { 137 | TVApp(id: "3201710014981", name: "Paramount +") 138 | } 139 | 140 | public static func plutoTV() -> TVApp { 141 | TVApp(id: "3201808016802", name: "Pluto TV") 142 | } 143 | 144 | public static func primeVideo() -> TVApp { 145 | TVApp(id: "3201910019365", name: "Prime Video") 146 | } 147 | 148 | public static func spotify() -> TVApp { 149 | TVApp(id: "3201606009684", name: "Spotify") 150 | } 151 | 152 | public static func youtube() -> TVApp { 153 | TVApp(id: "111299001912", name: "YouTube") 154 | } 155 | } 156 | 157 | // MARK: TVConnectionConfiguration 158 | 159 | extension TVConnectionConfiguration { 160 | func wssURL() -> URL? { 161 | var components = URLComponents() 162 | components.path = path 163 | components.host = ipAddress 164 | components.port = port 165 | components.scheme = scheme 166 | var queryItems = [URLQueryItem]() 167 | app.asBase64.flatMap { queryItems.append(.init(name: "name", value: $0)) } 168 | token.flatMap { queryItems.append(.init(name: "token", value: $0)) } 169 | components.queryItems = queryItems 170 | return components.url?.removingPercentEncoding 171 | } 172 | } 173 | 174 | // MARK: TVKeyboardLayout 175 | 176 | extension TVKeyboardLayout { 177 | public static var qwerty: Self { 178 | [ 179 | ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], 180 | ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], 181 | ["a", "s", "d", "f", "g", "h", "j", "k", "l"], 182 | ["z", "x", "c", "v", "b", "n", "m"] 183 | ] 184 | } 185 | 186 | public static var youtube: Self { 187 | [ 188 | ["a", "b", "c", "d", "e", "f", "g"], 189 | ["h", "i", "j", "k", "l", "m", "n"], 190 | ["o", "p", "q", "r", "s", "t", "u"], 191 | ["v", "w", "x", "y", "z", "-", "'"], 192 | ] 193 | } 194 | } 195 | 196 | // MARK: TVRemoteCommand 197 | 198 | extension TVRemoteCommand { 199 | static func createClickCommand(_ key: TVRemoteCommand.Params.ControlKey) -> TVRemoteCommand { 200 | TVRemoteCommand( 201 | method: .control, 202 | params: .init( 203 | cmd: .click, 204 | dataOfCmd: key, 205 | option: false, 206 | typeOfRemote: .remoteKey 207 | ) 208 | ) 209 | } 210 | 211 | static func createPressCommand(_ key: TVRemoteCommand.Params.ControlKey) -> TVRemoteCommand { 212 | TVRemoteCommand( 213 | method: .control, 214 | params: .init( 215 | cmd: .press, 216 | dataOfCmd: key, 217 | option: false, 218 | typeOfRemote: .remoteKey 219 | ) 220 | ) 221 | } 222 | 223 | static func createReleaseCommand(_ key: TVRemoteCommand.Params.ControlKey) -> TVRemoteCommand { 224 | TVRemoteCommand( 225 | method: .control, 226 | params: .init( 227 | cmd: .release, 228 | dataOfCmd: key, 229 | option: false, 230 | typeOfRemote: .remoteKey 231 | ) 232 | ) 233 | } 234 | 235 | static func createTextInputCommand(_ text: String) -> TVRemoteCommand { 236 | TVRemoteCommand( 237 | method: .control, 238 | params: .init( 239 | cmd: .textInput(Data(text.utf8).base64EncodedString()), 240 | dataOfCmd: .base64, 241 | option: false, 242 | typeOfRemote: .inputString 243 | ) 244 | ) 245 | } 246 | } 247 | 248 | 249 | // MARK: URL 250 | 251 | extension URL { 252 | var removingPercentEncoding: URL? { 253 | absoluteString.removingPercentEncoding.flatMap(URL.init(string:)) 254 | } 255 | } 256 | 257 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/ContentViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentViewModel.swift 3 | // TVCommanderKitDemo 4 | // 5 | // Created by Wilson Desimini on 10/24/23. 6 | // 7 | 8 | import Foundation 9 | import TVCommanderKit 10 | 11 | @Observable 12 | class ContentViewModel: TVCommanderDelegate, TVSearchObserving { 13 | 14 | // MARK: State 15 | 16 | var appName = "sample_app" 17 | var tvIPAddress = "" 18 | var tvAuthToken: TVAuthToken? 19 | var tvWakeOnLANDevice = TVWakeOnLANDevice(mac: "") 20 | var remoteCommandKeySelected = TVRemoteCommand.Params.ControlKey.mute 21 | var keyboardSelected = "qwerty" 22 | var keyboardEntry = "" 23 | var tvApp: TVApp = .netflix() 24 | var tvAppStatus: TVAppStatus? 25 | private(set) var tvIsConnecting = false 26 | private(set) var tvIsConnected = false 27 | private(set) var tvIsDisconnecting = false 28 | private(set) var tvIsWakingOnLAN = false 29 | private(set) var isSearchingForTVs = false 30 | private(set) var tvsFoundInSearch = [TV]() 31 | private(set) var tvAuthStatus = TVAuthStatus.none 32 | private(set) var tvError: Error? 33 | private var tvCommander: TVCommander? 34 | private let tvAppManager: TVAppManaging 35 | private let tvSearcher: TVSearcher 36 | 37 | init() { 38 | tvAppManager = TVAppManager() 39 | tvSearcher = TVSearcher() 40 | tvSearcher.addSearchObserver(self) 41 | } 42 | 43 | var connectEnabled: Bool { 44 | !tvIsConnecting && !tvIsConnected 45 | } 46 | 47 | var authTokenEntryDisabled: Bool { 48 | tvCommander != nil 49 | || tvIsConnecting 50 | || tvIsConnected 51 | || tvIsDisconnecting 52 | } 53 | 54 | var controlsEnabled: Bool { 55 | tvIsConnected && tvAuthStatus == .allowed 56 | } 57 | 58 | var keyboardSendEnabled: Bool { 59 | controlsEnabled && !keyboardEntry.isEmpty 60 | } 61 | 62 | var disconnectEnabled: Bool { 63 | tvIsConnected 64 | } 65 | 66 | var wakeOnLANEnabled: Bool { 67 | !tvIsWakingOnLAN 68 | } 69 | 70 | var keyboards: [String] { ["qwerty", "youtube"] } 71 | 72 | var remoteCommandKeys: [TVRemoteCommand.Params.ControlKey] { 73 | [ 74 | .powerOff, 75 | .up, 76 | .down, 77 | .left, 78 | .right, 79 | .enter, 80 | .returnKey, 81 | .channelList, 82 | .menu, 83 | .source, 84 | .guide, 85 | .tools, 86 | .info, 87 | .colorRed, 88 | .colorGreen, 89 | .colorYellow, 90 | .colorBlue, 91 | .key3D, 92 | .volumeUp, 93 | .volumeDown, 94 | .mute, 95 | .number0, 96 | .number1, 97 | .number2, 98 | .number3, 99 | .number4, 100 | .number5, 101 | .number6, 102 | .number7, 103 | .number8, 104 | .number9, 105 | .sourceTV, 106 | .sourceHDMI, 107 | .contents, 108 | ] 109 | } 110 | 111 | var tvApps: [TVApp] { 112 | TVApp.allApps() 113 | } 114 | 115 | // MARK: User Actions 116 | 117 | func userTappedConnect() { 118 | setupTVCommander() 119 | guard let tvCommander else { return } 120 | tvIsConnecting = true 121 | tvCommander.connectToTV() 122 | } 123 | 124 | func userTappedDismissError() { 125 | tvError = nil 126 | } 127 | 128 | func userTappedSend() { 129 | tvCommander?.sendRemoteCommand(key: remoteCommandKeySelected) 130 | } 131 | 132 | func userTappedKeyboardSend() { 133 | switch keyboardSelected { 134 | case "youtube": 135 | tvCommander?.enterText(keyboardEntry, on: .youtube) 136 | case "qwerty": 137 | tvCommander?.enterText(keyboardEntry, on: .qwerty) 138 | default: 139 | fatalError() 140 | } 141 | } 142 | 143 | func userTappedSendTextInput() { 144 | tvCommander?.sendText(keyboardEntry) 145 | } 146 | 147 | func userTappedDisconnect() { 148 | tvIsDisconnecting = true 149 | tvCommander?.disconnectFromTV() 150 | } 151 | 152 | func userTappedWakeOnLAN() { 153 | tvIsWakingOnLAN = true 154 | TVCommander.wakeOnLAN(device: tvWakeOnLANDevice, queue: .main) { 155 | [weak self] error in 156 | self?.tvIsWakingOnLAN = false 157 | self?.tvError = error 158 | } 159 | } 160 | 161 | func userTappedSearchForTVs() { 162 | if isSearchingForTVs { 163 | tvSearcher.stopSearch() 164 | } else { 165 | tvSearcher.startSearch() 166 | } 167 | } 168 | 169 | func userTappedAppStatus() { 170 | Task { 171 | await fetchAppStatus() 172 | } 173 | } 174 | 175 | func userTappedLaunchApp() { 176 | Task { 177 | await launchApp() 178 | } 179 | } 180 | 181 | // MARK: Lifecycle 182 | 183 | private func setupTVCommander() { 184 | guard tvCommander == nil else { return } 185 | do { 186 | tvCommander = try TVCommander( 187 | tvIPAddress: tvIPAddress, 188 | appName: appName, 189 | authToken: tvAuthToken 190 | ) 191 | tvCommander?.delegate = self 192 | } catch { 193 | tvError = error 194 | } 195 | } 196 | 197 | private func removeTVCommander() { 198 | tvCommander = nil 199 | } 200 | 201 | private func fetchAppStatus() async { 202 | do { 203 | tvAppStatus = try await tvAppManager.fetchStatus(for: tvApp, tvIPAddress: tvIPAddress) 204 | } catch { 205 | tvError = error 206 | } 207 | } 208 | 209 | private func launchApp() async { 210 | do { 211 | try await tvAppManager.launch(tvApp: tvApp, tvIPAddress: tvIPAddress) 212 | } catch { 213 | tvError = error 214 | } 215 | } 216 | 217 | // MARK: TVCommanderDelegate 218 | 219 | func tvCommanderDidConnect(_ tvCommander: TVCommander) { 220 | tvIsConnecting = false 221 | tvIsConnected = true 222 | } 223 | 224 | func tvCommanderDidDisconnect(_ tvCommander: TVCommander) { 225 | tvIsDisconnecting = false 226 | tvIsConnected = false 227 | removeTVCommander() 228 | } 229 | 230 | func tvCommander(_ tvCommander: TVCommander, didUpdateAuthState authStatus: TVAuthStatus) { 231 | tvAuthStatus = authStatus 232 | tvAuthToken = tvCommander.tvConfig.token 233 | } 234 | 235 | func tvCommander(_ tvCommander: TVCommander, didWriteRemoteCommand command: TVRemoteCommand) { 236 | } 237 | 238 | func tvCommander(_ tvCommander: TVCommander, didEncounterError error: TVCommanderError) { 239 | tvError = error 240 | } 241 | 242 | // MARK: TVSearchObserving 243 | 244 | func tvSearchDidStart() { 245 | isSearchingForTVs = true 246 | } 247 | 248 | func tvSearchDidStop() { 249 | tvsFoundInSearch.removeAll() 250 | isSearchingForTVs = false 251 | } 252 | 253 | func tvSearchDidFindTV(_ tv: TV) { 254 | if !tvsFoundInSearch.contains(tv) { 255 | tvsFoundInSearch.append(tv) 256 | } 257 | } 258 | 259 | func tvSearchDidLoseTV(_ tv: TV) { 260 | tvsFoundInSearch.removeAll { $0.id == tv.id } 261 | } 262 | } 263 | 264 | @Observable 265 | class TVViewModel { 266 | private let tvFetcher = TVFetcher() 267 | private(set) var tv: TV 268 | 269 | init(tv: TV) { 270 | self.tv = tv 271 | } 272 | 273 | func fetchTVDevice() { 274 | tvFetcher.fetchDevice(for: tv) { [weak self] result in 275 | guard let self else { return } 276 | DispatchQueue.main.async { 277 | switch result { 278 | case .success(let tvFetched): 279 | self.tv = tvFetched 280 | case .failure(let error): 281 | print(error) 282 | } 283 | } 284 | } 285 | } 286 | 287 | func cancelFetch() { 288 | tvFetcher.cancelFetch() 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TVCommanderKitDemo 4 | // 5 | // Created by Wilson Desimini on 10/24/23. 6 | // 7 | 8 | import SwiftUI 9 | import TVCommanderKit 10 | 11 | struct ContentView: View { 12 | enum Route: Hashable { 13 | case tv(TV) 14 | 15 | func hash(into hasher: inout Hasher) { 16 | switch self { 17 | case .tv(let tv): 18 | hasher.combine(tv.id) 19 | } 20 | } 21 | } 22 | 23 | @State private var contentViewModel = ContentViewModel() 24 | @State private var isPresentingError = false 25 | @State private var path = NavigationPath() 26 | 27 | var body: some View { 28 | NavigationStack(path: $path) { 29 | Form { 30 | Section("Connect / Disconnect TV") { 31 | TextField("App Name", text: $contentViewModel.appName) 32 | TextField("TV IP Address", text: $contentViewModel.tvIPAddress) 33 | if let tvAuthTokenBinding = tvAuthTokenBinding { 34 | HStack { 35 | TextField("Auth Token", text: tvAuthTokenBinding) 36 | Button("Remove") { 37 | contentViewModel.tvAuthToken = nil 38 | } 39 | } 40 | .disabled(contentViewModel.authTokenEntryDisabled) 41 | } else { 42 | Button("Add Auth Token") { 43 | contentViewModel.tvAuthToken = "" 44 | } 45 | .disabled(contentViewModel.authTokenEntryDisabled) 46 | } 47 | if contentViewModel.tvIsConnected { 48 | Button("Disconnect", action: contentViewModel.userTappedDisconnect) 49 | .disabled(!contentViewModel.disconnectEnabled) 50 | } else { 51 | Button("Connect", action: contentViewModel.userTappedConnect) 52 | .disabled(!contentViewModel.connectEnabled) 53 | } 54 | } 55 | Section("TV Auth Status") { 56 | Text(authStatusAsText(contentViewModel.tvAuthStatus)) 57 | } 58 | Section("Send TV Key Commands") { 59 | Picker("Key Command", selection: $contentViewModel.remoteCommandKeySelected) { 60 | ForEach(contentViewModel.remoteCommandKeys, id: \.rawValue) { 61 | Text($0.rawValue).tag($0) 62 | } 63 | } 64 | Button("Send", action: contentViewModel.userTappedSend) 65 | .disabled(!contentViewModel.controlsEnabled) 66 | } 67 | Section("Send Text") { 68 | TextField("Text", text: $contentViewModel.keyboardEntry) 69 | Button("Send via Direct Input", action: contentViewModel.userTappedSendTextInput) 70 | .disabled(!contentViewModel.keyboardSendEnabled) 71 | Picker(selection: $contentViewModel.keyboardSelected) { 72 | ForEach(contentViewModel.keyboards, id: \.self) { 73 | Text($0).tag($0) 74 | } 75 | } label: { 76 | Button("Send via Keyboard Entry", action: contentViewModel.userTappedKeyboardSend) 77 | .disabled(!contentViewModel.keyboardSendEnabled) 78 | } 79 | } 80 | Section("Wake TV On LAN") { 81 | TextField("TV MAC Address", text: $contentViewModel.tvWakeOnLANDevice.mac) 82 | TextField("TV Broadcast Address", text: $contentViewModel.tvWakeOnLANDevice.broadcast) 83 | TextField("TV Port", value: $contentViewModel.tvWakeOnLANDevice.port, formatter: NumberFormatter()) 84 | Button("Wake On LAN", action: contentViewModel.userTappedWakeOnLAN) 85 | .disabled(!contentViewModel.wakeOnLANEnabled) 86 | } 87 | Section("Search for TVs") { 88 | Button(contentViewModel.isSearchingForTVs ? "Stop" : "Start", action: contentViewModel.userTappedSearchForTVs) 89 | List(contentViewModel.tvsFoundInSearch) { tv in 90 | Button { 91 | path.append(Route.tv(tv)) 92 | } label: { 93 | VStack(alignment: .leading) { 94 | Text(tv.name) 95 | Text(tv.uri) 96 | .font(.caption) 97 | } 98 | } 99 | } 100 | } 101 | Section("Installed Apps") { 102 | Picker("Selected TV App", selection: $contentViewModel.tvApp) { 103 | ForEach(contentViewModel.tvApps) { app in 104 | Text(app.name).tag(app) 105 | } 106 | } 107 | Button("Status") { 108 | contentViewModel.userTappedAppStatus() 109 | } 110 | Button("Launch") { 111 | contentViewModel.userTappedLaunchApp() 112 | } 113 | } 114 | } 115 | .navigationBarTitle("TV Controller") 116 | .navigationDestination(for: Route.self) { route in 117 | switch route { 118 | case .tv(let tv): 119 | TVView(tv: tv) 120 | } 121 | } 122 | .sheet( 123 | item: $contentViewModel.tvAppStatus, 124 | content: TVAppStatusView.init 125 | ) 126 | .alert(isPresented: $isPresentingError) { 127 | Alert( 128 | title: Text("Error"), 129 | message: Text(contentViewModel.tvError!.localizedDescription), 130 | dismissButton: .default(Text("Dismiss")) { 131 | contentViewModel.userTappedDismissError() 132 | } 133 | ) 134 | } 135 | .onReceive(contentViewModel.tvError.publisher) { _ in 136 | isPresentingError = true 137 | } 138 | } 139 | } 140 | 141 | private var tvAuthTokenBinding: Binding? { 142 | guard let tvAuthToken = contentViewModel.tvAuthToken else { 143 | return nil 144 | } 145 | return Binding( 146 | get: { tvAuthToken }, 147 | set: { contentViewModel.tvAuthToken = $0 } 148 | ) 149 | } 150 | 151 | private func authStatusAsText(_ authStatus: TVAuthStatus) -> String { 152 | switch authStatus { 153 | case .none: return "None" 154 | case .allowed: return "Allowed" 155 | case .denied: return "Denied" 156 | } 157 | } 158 | } 159 | 160 | struct TVView: View { 161 | @State private var viewModel: TVViewModel 162 | 163 | init(tv: TV) { 164 | _viewModel = .init(wrappedValue: .init(tv: tv)) 165 | } 166 | 167 | var body: some View { 168 | Form { 169 | Section("TV") { 170 | Text("id: \(viewModel.tv.id)") 171 | Text("type: \(viewModel.tv.type)") 172 | Text("uri: \(viewModel.tv.uri)") 173 | } 174 | Section("Device") { 175 | Button("Fetch Device") { 176 | viewModel.fetchTVDevice() 177 | } 178 | Button("Cancel Fetch") { 179 | viewModel.cancelFetch() 180 | } 181 | if let device = viewModel.tv.device { 182 | Text("powerState: \(device.powerState ?? "")") 183 | Text("tokenAuthSupport: \(device.tokenAuthSupport)") 184 | Text("wifiMac: \(device.wifiMac)") 185 | } 186 | } 187 | } 188 | .navigationTitle(viewModel.tv.name) 189 | } 190 | } 191 | 192 | struct TVAppStatusView: View { 193 | let status: TVAppStatus 194 | @Environment(\.dismiss) private var dismiss 195 | 196 | var body: some View { 197 | NavigationStack { 198 | Form { 199 | Text("ID: \(status.id)") 200 | Text("Running: \(status.running ? "Yes" : "No")") 201 | Text("Version: \(status.version)") 202 | Text("Visible: \(status.visible ? "Yes" : "No")") 203 | } 204 | .navigationTitle(status.name) 205 | .toolbar { 206 | Button("Done") { 207 | dismiss() 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | struct ContentView_Previews: PreviewProvider { 215 | static var previews: some View { 216 | ContentView() 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TVCommanderKit 2 | 3 | **TVCommanderKit** is a Swift SDK for controlling Samsung Smart TVs over a WebSocket connection. It provides a simple and convenient way to interact with your TV and send remote control commands from your iOS application. This README.md file will guide you through using the TVCommanderKit SDK in your project. 4 | 5 | ## Table of Contents 6 | - [Usage](#usage) 7 | - [Delegate Methods](#delegate-methods) 8 | - [Searching for TVs](#searching-for-tvs) 9 | - [Fetching TV Device Info](#fetching-tv-device-info) 10 | - [Establishing Connection](#establishing-connection) 11 | - [Authorizing Application with TV](#authorizing-application-with-tv) 12 | - [Sending Remote Control Commands](#sending-remote-control-commands) 13 | - [Disconnecting from TV](#disconnecting-from-tv) 14 | - [Error Handling](#error-handling) 15 | - [Text Entry](#text-entry) 16 | - [Wake on LAN](#wake-on-lan) 17 | - [Managing TV Apps](#managing-tv-apps) 18 | - [License](#license) 19 | 20 | ## Usage 21 | 22 | 1. Import the TVCommanderKit module: 23 | 24 | ```swift 25 | import TVCommanderKit 26 | ``` 27 | 28 | 2. Initialize a `TVCommander` object with your TV's IP address and application name: 29 | 30 | ```swift 31 | let tvCommander = try TVCommander(tvIPAddress: "your_tv_ip_address", appName: "your_app_name") 32 | ``` 33 | 34 | 3. Implement the necessary delegate methods to receive updates and handle events (see [Delegate Methods](#delegate-methods)), then set your delegate to receive updates and events from the TVCommander: 35 | 36 | ```swift 37 | tvCommander.delegate = self 38 | ``` 39 | 40 | 4. Connect to your TV (see [Establishing Connection](#establishing-connection) for further details): 41 | 42 | ```swift 43 | tvCommander.connectToTV() 44 | ``` 45 | 46 | 5. Handle incoming authorization steps to authorize your app to send remote controls (see [Authorizing Application with TV](#authorizing-application-with-tv)). 47 | 48 | 6. Send remote control commands (see [Sending Remote Control Commands](#sending-remote-control-commands)). 49 | 50 | 7. Disconnect from your TV when done: 51 | 52 | ```swift 53 | tvCommander.disconnectFromTV() 54 | ``` 55 | 56 | ## Delegate Methods 57 | 58 | The TVCommanderDelegate protocol provides methods to receive updates and events from the TVCommanderKit SDK: 59 | 60 | - `tvCommanderDidConnect(_ tvCommander: TVCommander)`: Called when the TVCommander successfully connects to the TV. 61 | 62 | - `tvCommanderDidDisconnect(_ tvCommander: TVCommander)`: Called when the TVCommander disconnects from the TV. 63 | 64 | - `tvCommander(_ tvCommander: TVCommander, didUpdateAuthState authStatus: TVAuthStatus)`: Called when the authorization status is updated. 65 | 66 | - `tvCommander(_ tvCommander: TVCommander, didWriteRemoteCommand command: TVRemoteCommand)`: Called when a remote control command is sent to the TV. 67 | 68 | - `tvCommander(_ tvCommander: TVCommander, didEncounterError error: TVCommanderError)`: Called when the TVCommander encounters an error. 69 | 70 | ## Searching for TVs 71 | 72 | The TVCommanderKit SDK includes a `TVSearcher` class that helps you discover Samsung Smart TVs on the network. You can use this class to search for TVs and obtain `TV` instances for each discovered TV. 73 | 74 | ```swift 75 | let tvSearcher = TVSearcher() 76 | tvSearcher.addSearchObserver(self) 77 | tvSearcher.startSearch() 78 | ``` 79 | 80 | You can also specify a TV to search for using its ID. Once the `TVSeacher` finds a TV with a matching ID, it will automatically stop searching: 81 | 82 | ```swift 83 | tvSearcher.configureTargetTVId("your_tv_id") 84 | ``` 85 | 86 | To stop searching for TVs, call the stopFindingTVs() method: 87 | 88 | ```swift 89 | tvSearcher.stopSearch() 90 | ``` 91 | 92 | You need to conform to the `TVSearchObserving` protocol to receive updates on the search state and discovered TVs. 93 | 94 | ## Fetching TV Device Info 95 | 96 | Upon finding `TV`s using the `TVSearcher` class, you can also fetch device info using the `TVFetcher`. 97 | 98 | ```swift 99 | let tvFetcher = TVFetcher(session: .shared) // use custom URLSession for mocking, etc 100 | ``` 101 | 102 | Invoking the `fetchDevice(for:)` method will return either an updated `TV` object (with device info injected) or a `TVFetcherError`. 103 | 104 | ```swift 105 | tvFetcher.fetchDevice(for: tv) { result in 106 | switch result { 107 | case .success(let tvFetched): 108 | // use updated tv 109 | case .failure(let error): 110 | // handle fetch error 111 | } 112 | } 113 | ``` 114 | 115 | ## Establishing Connection 116 | 117 | To establish a connection to your TV, use the `connectToTV()` method. This method will create a WebSocket connection to your TV with the provided IP address and application name. If a connection is already established, it will handle the error gracefully. 118 | 119 | ```swift 120 | tvCommander.connectToTV() 121 | ``` 122 | 123 | If you're worried about man-in-the-middle attacks, it's recommended that you implement a custom cert-pinning type and pass it through the optional `certPinner` parameter to ensure connection to a trusted server (make sure your custom cert-pinning type doesn't also strongly reference the `TVCommander`, as this will result in a retain cycle). 124 | 125 | ## Authorizing Application with TV 126 | 127 | After establishing a connection to the TV, you'll need to authorize your app for sending remote controls. You can access the authorization status of your application via the `authStatus` property. If your TV hasn't already handled your application's authorization, a prompt should appear on your TV for you to choose an authorization option. Once you select an option within the prompt, if the authorization status updated, it will get passed back through the corresponding delegate method: 128 | 129 | ```swift 130 | func tvCommander(_ tvCommander: TVCommander, didUpdateAuthState authStatus: TVAuthStatus) { 131 | switch authStatus { 132 | case .none: // application authorization incomplete 133 | case .allowed: // application allowed to send commands, token stored in tvCommander.tvConfig.token 134 | case .denied: // application not allowed to send commands 135 | } 136 | } 137 | ``` 138 | 139 | ## Sending Remote Control Commands 140 | 141 | You can send remote control commands to your TV using the `sendRemoteCommand(key:)` method. Ensure that you are connected to the TV and the authorization status is allowed before sending commands. 142 | 143 | ```swift 144 | tvCommander.sendRemoteCommand(key: .enter) 145 | ``` 146 | 147 | ## Text Entry 148 | 149 | **TVCommanderKit** provides a convenient text entry feature, allowing you to quickly input text into on-screen keyboards. There are now two ways to send text input to the TV: 150 | 151 | 1. **Direct Text Input** 152 | Use `sendText(_:)` to send text directly to the TV. This is the preferred method for compatible devices. 153 | ```swift 154 | let textToSend = "Hello, World!" 155 | tvCommander.sendText(textToSend) 156 | ``` 157 | 2. **Keyboard Navigation (Legacy)** 158 | The `enterText(_:on:)` method simulates typing by navigating an on-screen keyboard. This approach will likely be removed in future versions. 159 | ```swift 160 | let textToEnter = "Hello, World!" 161 | let keyboardLayout = TVKeyboardLayout.youtube 162 | tvCommander.enterText(textToEnter, on: keyboardLayout) 163 | ``` 164 | 165 | ## Disconnecting from TV 166 | 167 | When you're done with the TVCommander, disconnect from your TV using the `disconnectFromTV()` method: 168 | 169 | ```swift 170 | tvCommander.disconnectFromTV() 171 | ``` 172 | 173 | ## Error Handling 174 | 175 | The TVCommanderKit SDK includes error handling for various scenarios, and errors are reported through the delegate method `tvCommander(_ tvCommander: TVCommander, didEncounterError error: TVCommanderError)`. You should implement this method to handle errors appropriately. 176 | 177 | ## Wake on LAN 178 | 179 | The TVCommanderKit SDK now supports Wake on LAN functionality. You can wake up your TV using the `wakeOnLAN` method: 180 | 181 | ```swift 182 | let device = TVWakeOnLANDevice(mac: "your_tv_mac_address") 183 | TVCommander.wakeOnLAN(device: device) { error in 184 | if let error = error { 185 | print("Wake on LAN failed: \(error.localizedDescription)") 186 | } else { 187 | print("Wake on LAN successful!") 188 | } 189 | } 190 | ``` 191 | 192 | ## Managing TV Apps 193 | 194 | `TVAppManager` allows you to interact with TV applications on Samsung TVs using their IP address and app ID. 195 | 196 | #### Usage 197 | 198 | 1. **Initialization:** 199 | 200 | ```swift 201 | let tvAppManager = TVAppManager() 202 | let tvApp = TVApp(id: "3201907018807", name: "Netflix") 203 | ``` 204 | 205 | 2. **Fetch App Status:** 206 | 207 | ```swift 208 | let tvIPAddress = "192.168.1.100" 209 | let appStatus = try await tvAppManager.fetchStatus(for: tvApp, tvIPAddress: tvIPAddress) 210 | print(appStatus) 211 | ``` 212 | 213 | 3. **Launch App:** 214 | 215 | ```swift 216 | try await tvAppManager.launch(tvApp: tvApp, tvIPAddress: tvIPAddress) 217 | ``` 218 | 219 | Errors like invalid URLs or app not found are handled by `TVAppManagerError` and `TVAppNetworkError`. 220 | 221 | ## License 222 | 223 | The TVCommanderKit SDK is distributed under the MIT license. 224 | 225 | --- 226 | 227 | Feel free to contribute to this SDK, report issues, or provide feedback. I hope you find TVCommanderKit useful in building your Samsung Smart TV applications! 228 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Headers/SmartView-Swift.h 8 | 9 | fCQuQe66AbEkKfM7xuifVejvnKQ= 10 | 11 | Headers/SmartView.h 12 | 13 | YfFzBWX+uLVPRTzD04cUte56moM= 14 | 15 | Info.plist 16 | 17 | iWtFL4AkpGAskw9k8wzV8WiE8y4= 18 | 19 | LICENSE 20 | 21 | PZyoWK4EfgXIwDHiS0GuohQX/Co= 22 | 23 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.abi.json 24 | 25 | Dnn4g1OQemqAmPWhsPgbTlHU7iA= 26 | 27 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface 28 | 29 | jBukoj59/xonpEXlSLipdiqMl7Q= 30 | 31 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftdoc 32 | 33 | xgD8jHgG4LawRyGMdD3YuV9lmMI= 34 | 35 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftinterface 36 | 37 | jBukoj59/xonpEXlSLipdiqMl7Q= 38 | 39 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftmodule 40 | 41 | 6tt786Ee/EdJsCKQ2quPRgqWfR4= 42 | 43 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.abi.json 44 | 45 | Dnn4g1OQemqAmPWhsPgbTlHU7iA= 46 | 47 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface 48 | 49 | pGGlMteZ7lcNWBzWL9zRmzNmjv0= 50 | 51 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftdoc 52 | 53 | 4wMm2lGSB5NcXifg2/MTbgOrUZs= 54 | 55 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftinterface 56 | 57 | pGGlMteZ7lcNWBzWL9zRmzNmjv0= 58 | 59 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftmodule 60 | 61 | iiA0Lmyr6TVjUitk1tssPMMGLD4= 62 | 63 | Modules/module.modulemap 64 | 65 | LbI9X9CJ8RYZbh/ux4EFHhu6ejs= 66 | 67 | PrivateHeaders/GCDAsyncUdpSocket.h 68 | 69 | wXIowtvot5jmH04VG5HEaeCZZ5k= 70 | 71 | ca_crt.cer 72 | 73 | rCi1nK7YIuCO9Nt+Msn3IgGjrJY= 74 | 75 | ca_crt_new.cer 76 | 77 | A/LVO48gOaCESNHO6C3cmutRy3s= 78 | 79 | 80 | files2 81 | 82 | Headers/SmartView-Swift.h 83 | 84 | hash 85 | 86 | fCQuQe66AbEkKfM7xuifVejvnKQ= 87 | 88 | hash2 89 | 90 | 1/UJdpt7IAgCrNKirYctjj5BYoB7omCst4QvBbG/was= 91 | 92 | 93 | Headers/SmartView.h 94 | 95 | hash 96 | 97 | YfFzBWX+uLVPRTzD04cUte56moM= 98 | 99 | hash2 100 | 101 | xgfjbhyCB19yp5y5D2TwfJi8nq5dMoeR5JaxpTtDRzk= 102 | 103 | 104 | LICENSE 105 | 106 | hash 107 | 108 | PZyoWK4EfgXIwDHiS0GuohQX/Co= 109 | 110 | hash2 111 | 112 | bcDgaNzzpbyOBUIFuFt3IOHUkmW7xkv1FdLPeRl99po= 113 | 114 | 115 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.abi.json 116 | 117 | hash 118 | 119 | Dnn4g1OQemqAmPWhsPgbTlHU7iA= 120 | 121 | hash2 122 | 123 | uS9lkeBC4m/gipba3p5ciiLB3EoO8WRpeU4GHZyDH8M= 124 | 125 | 126 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface 127 | 128 | hash 129 | 130 | jBukoj59/xonpEXlSLipdiqMl7Q= 131 | 132 | hash2 133 | 134 | rKCR4bVJzRJwzAL8apms50GZ0OfHU/dNhHP7DYwASC4= 135 | 136 | 137 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftdoc 138 | 139 | hash 140 | 141 | xgD8jHgG4LawRyGMdD3YuV9lmMI= 142 | 143 | hash2 144 | 145 | ujN+LgVyfPQ8VR1rtna4izXH5qm0bFLSxIiYFbqTlMs= 146 | 147 | 148 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftinterface 149 | 150 | hash 151 | 152 | jBukoj59/xonpEXlSLipdiqMl7Q= 153 | 154 | hash2 155 | 156 | rKCR4bVJzRJwzAL8apms50GZ0OfHU/dNhHP7DYwASC4= 157 | 158 | 159 | Modules/SmartView.swiftmodule/arm64-apple-ios-simulator.swiftmodule 160 | 161 | hash 162 | 163 | 6tt786Ee/EdJsCKQ2quPRgqWfR4= 164 | 165 | hash2 166 | 167 | Okekpxgb5fYhgsUsS6I6NKjOpQV7h2HrLIB7++XNCE0= 168 | 169 | 170 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.abi.json 171 | 172 | hash 173 | 174 | Dnn4g1OQemqAmPWhsPgbTlHU7iA= 175 | 176 | hash2 177 | 178 | uS9lkeBC4m/gipba3p5ciiLB3EoO8WRpeU4GHZyDH8M= 179 | 180 | 181 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface 182 | 183 | hash 184 | 185 | pGGlMteZ7lcNWBzWL9zRmzNmjv0= 186 | 187 | hash2 188 | 189 | DqB12XNaK0baIsGRvWzsh+xZ84QPdh6wAzxiFSuk+v0= 190 | 191 | 192 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftdoc 193 | 194 | hash 195 | 196 | 4wMm2lGSB5NcXifg2/MTbgOrUZs= 197 | 198 | hash2 199 | 200 | wfzxJExCMshFHN1CDQmc5PuiTk60QHjx7Iv+uA+FtmM= 201 | 202 | 203 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftinterface 204 | 205 | hash 206 | 207 | pGGlMteZ7lcNWBzWL9zRmzNmjv0= 208 | 209 | hash2 210 | 211 | DqB12XNaK0baIsGRvWzsh+xZ84QPdh6wAzxiFSuk+v0= 212 | 213 | 214 | Modules/SmartView.swiftmodule/x86_64-apple-ios-simulator.swiftmodule 215 | 216 | hash 217 | 218 | iiA0Lmyr6TVjUitk1tssPMMGLD4= 219 | 220 | hash2 221 | 222 | mjmEWHjswVMk/fRRXXCbRHSBSL5NbPGdrV7+3yua5R0= 223 | 224 | 225 | Modules/module.modulemap 226 | 227 | hash 228 | 229 | LbI9X9CJ8RYZbh/ux4EFHhu6ejs= 230 | 231 | hash2 232 | 233 | Ig4ZXbHcSQ2nS7Rmq5ZxPJIbCbCRVhlN+OFqNrKqOsM= 234 | 235 | 236 | PrivateHeaders/GCDAsyncUdpSocket.h 237 | 238 | hash 239 | 240 | wXIowtvot5jmH04VG5HEaeCZZ5k= 241 | 242 | hash2 243 | 244 | XT5UNacdN/Ki83lIUWMRKdwqPFa16GDa3IexQw22HjE= 245 | 246 | 247 | ca_crt.cer 248 | 249 | hash 250 | 251 | rCi1nK7YIuCO9Nt+Msn3IgGjrJY= 252 | 253 | hash2 254 | 255 | XAnzsPmkwLrf21j/a4QwPICH83rLWo0cxCBvQg6Mh2I= 256 | 257 | 258 | ca_crt_new.cer 259 | 260 | hash 261 | 262 | A/LVO48gOaCESNHO6C3cmutRy3s= 263 | 264 | hash2 265 | 266 | u5tJTCxpkl8pJU6m4N49ZDXWtBAuSDwBbzHGRcKv25Q= 267 | 268 | 269 | 270 | rules 271 | 272 | ^.* 273 | 274 | ^.*\.lproj/ 275 | 276 | optional 277 | 278 | weight 279 | 1000 280 | 281 | ^.*\.lproj/locversion.plist$ 282 | 283 | omit 284 | 285 | weight 286 | 1100 287 | 288 | ^Base\.lproj/ 289 | 290 | weight 291 | 1010 292 | 293 | ^version.plist$ 294 | 295 | 296 | rules2 297 | 298 | .*\.dSYM($|/) 299 | 300 | weight 301 | 11 302 | 303 | ^(.*/)?\.DS_Store$ 304 | 305 | omit 306 | 307 | weight 308 | 2000 309 | 310 | ^.* 311 | 312 | ^.*\.lproj/ 313 | 314 | optional 315 | 316 | weight 317 | 1000 318 | 319 | ^.*\.lproj/locversion.plist$ 320 | 321 | omit 322 | 323 | weight 324 | 1100 325 | 326 | ^Base\.lproj/ 327 | 328 | weight 329 | 1010 330 | 331 | ^Info\.plist$ 332 | 333 | omit 334 | 335 | weight 336 | 20 337 | 338 | ^PkgInfo$ 339 | 340 | omit 341 | 342 | weight 343 | 20 344 | 345 | ^embedded\.provisionprofile$ 346 | 347 | weight 348 | 20 349 | 350 | ^version\.plist$ 351 | 352 | weight 353 | 20 354 | 355 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVCommander.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVCommander.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 9/3/23. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | import Starscream 11 | 12 | public protocol TVCommanderDelegate: AnyObject { 13 | func tvCommanderDidConnect(_ tvCommander: TVCommander) 14 | func tvCommanderDidDisconnect(_ tvCommander: TVCommander) 15 | func tvCommander(_ tvCommander: TVCommander, didUpdateAuthState authStatus: TVAuthStatus) 16 | func tvCommander(_ tvCommander: TVCommander, didWriteRemoteCommand command: TVRemoteCommand) 17 | func tvCommander(_ tvCommander: TVCommander, didEncounterError error: TVCommanderError) 18 | } 19 | 20 | public class TVCommander: WebSocketDelegate { 21 | public weak var delegate: TVCommanderDelegate? 22 | private(set) public var tvConfig: TVConnectionConfiguration 23 | private(set) public var authStatus = TVAuthStatus.none 24 | private(set) public var isConnected = false 25 | private let webSocketCreator: TVWebSocketCreator 26 | private let webSocketHandler = TVWebSocketHandler() 27 | private var webSocket: Starscream.WebSocket? 28 | private var commandQueue = [TVRemoteCommand]() 29 | 30 | init(tvConfig: TVConnectionConfiguration, webSocketCreator: TVWebSocketCreator) { 31 | self.tvConfig = tvConfig 32 | self.webSocketCreator = webSocketCreator 33 | self.webSocketHandler.delegate = self 34 | } 35 | 36 | public convenience init(tvId: String? = nil, tvIPAddress: String, appName: String, authToken: TVAuthToken? = nil) throws { 37 | guard appName.isValidAppName else { 38 | throw TVCommanderError.invalidAppNameEntered 39 | } 40 | guard tvIPAddress.isValidIPAddress else { 41 | throw TVCommanderError.invalidIPAddressEntered 42 | } 43 | let tvConfig = TVConnectionConfiguration( 44 | id: tvId, 45 | app: appName, 46 | path: "/api/v2/channels/samsung.remote.control", 47 | ipAddress: tvIPAddress, 48 | port: 8002, 49 | scheme: "wss", 50 | token: authToken 51 | ) 52 | self.init(tvConfig: tvConfig, webSocketCreator: TVWebSocketCreator()) 53 | } 54 | 55 | public convenience init(tv: TV, appName: String, authToken: TVAuthToken? = nil) throws { 56 | guard let ipAddress = tv.ipAddress else { throw TVCommanderError.invalidIPAddressEntered } 57 | try self.init(tvId: tv.id, tvIPAddress: ipAddress, appName: appName, authToken: authToken) 58 | } 59 | 60 | // MARK: Establish WebSocket Connection 61 | 62 | /// **NOTE** 63 | /// make sure any value for `certPinner` inputted here doesn't strongly reference `TVCommander` (will cause a retain cycle if it does) 64 | public func connectToTV(certPinner: CertificatePinning? = nil) { 65 | guard !isConnected else { 66 | handleError(.connectionAlreadyEstablished) 67 | return 68 | } 69 | guard let url = tvConfig.wssURL() else { 70 | handleError(.urlConstructionFailed) 71 | return 72 | } 73 | webSocket = webSocketCreator.createTVWebSocket(url: url, certPinner: certPinner, delegate: self) 74 | webSocket?.connect() 75 | } 76 | 77 | public func didReceive(event: WebSocketEvent, client: WebSocketClient) { 78 | webSocketHandler.didReceive(event: event, client: client) 79 | } 80 | 81 | // MARK: Send Remote Control Commands 82 | 83 | public func sendRemoteCommand(key: TVRemoteCommand.Params.ControlKey) { 84 | guard isConnected else { 85 | handleError(.remoteCommandNotConnectedToTV) 86 | return 87 | } 88 | guard authStatus == .allowed else { 89 | handleError(.remoteCommandAuthenticationStatusNotAllowed) 90 | return 91 | } 92 | sendCommandOverWebSocket(.createClickCommand(key)) 93 | } 94 | 95 | /// Send a text as text field input to the TV. Text will replace existing text in TV textfield. 96 | public func sendText(_ text: String) { 97 | guard isConnected else { 98 | handleError(.remoteCommandNotConnectedToTV) 99 | return 100 | } 101 | guard authStatus == .allowed else { 102 | handleError(.remoteCommandAuthenticationStatusNotAllowed) 103 | return 104 | } 105 | sendCommandOverWebSocket(.createTextInputCommand(text)) 106 | } 107 | 108 | private func sendCommandOverWebSocket(_ command: TVRemoteCommand) { 109 | commandQueue.append(command) 110 | if commandQueue.count == 1 { 111 | sendNextQueuedCommandOverWebSocket() 112 | } 113 | } 114 | 115 | private func sendNextQueuedCommandOverWebSocket() { 116 | guard let command = commandQueue.first else { 117 | return 118 | } 119 | guard let commandStr = try? command.asString() else { 120 | handleError(.commandConversionToStringFailed) 121 | return 122 | } 123 | webSocket?.write(string: commandStr) { [weak self] in 124 | guard let self else { return } 125 | self.commandQueue.removeFirst() 126 | self.delegate?.tvCommander(self, didWriteRemoteCommand: command) 127 | self.sendNextQueuedCommandOverWebSocket() 128 | } 129 | } 130 | 131 | // MARK: Send Keyboard Commands 132 | 133 | public func enterText(_ text: String, on keyboard: TVKeyboardLayout) { 134 | guard isConnected else { 135 | handleError(.remoteCommandNotConnectedToTV) 136 | return 137 | } 138 | guard authStatus == .allowed else { 139 | handleError(.remoteCommandAuthenticationStatusNotAllowed) 140 | return 141 | } 142 | 143 | let keys = controlKeys(toEnter: text, on: keyboard) 144 | keys.forEach(sendRemoteCommand(key:)) 145 | } 146 | 147 | private func controlKeys(toEnter text: String, on keyboard: TVKeyboardLayout) -> [TVRemoteCommand.Params.ControlKey] { 148 | guard !text.isEmpty else { return [] } // Check for empty string, otherwise it will crash on line 145 149 | 150 | let chars = Array(text) 151 | var moves: [TVRemoteCommand.Params.ControlKey] = [.enter] 152 | for i in 0..<(chars.count - 1) { 153 | let currentChar = String(chars[i]) 154 | let nextChar = String(chars[i + 1]) 155 | if let movesToNext = controlKeys(toMoveFrom: currentChar, to: nextChar, on: keyboard) { 156 | moves.append(contentsOf: movesToNext) 157 | moves.append(.enter) 158 | } else { 159 | delegate?.tvCommander(self, didEncounterError: .keyboardCharNotFound(nextChar)) 160 | } 161 | } 162 | return moves 163 | } 164 | 165 | private func controlKeys(toMoveFrom char1: String, to char2: String, on keyboard: TVKeyboardLayout) -> [TVRemoteCommand.Params.ControlKey]? { 166 | guard let (startRow, startCol) = coordinates(of: char1, on: keyboard), 167 | let (endRow, endCol) = coordinates(of: char2, on: keyboard) else { 168 | return nil 169 | } 170 | let rowDiff = endRow - startRow 171 | let colDiff = endCol - startCol 172 | var moves: [TVRemoteCommand.Params.ControlKey] = [] 173 | if rowDiff > 0 { 174 | moves += Array(repeating: .down, count: rowDiff) 175 | } else if rowDiff < 0 { 176 | moves += Array(repeating: .up, count: abs(rowDiff)) 177 | } 178 | if colDiff > 0 { 179 | moves += Array(repeating: .right, count: colDiff) 180 | } else if colDiff < 0 { 181 | moves += Array(repeating: .left, count: abs(colDiff)) 182 | } 183 | return moves 184 | } 185 | 186 | private func coordinates(of char: String, on keyboard: TVKeyboardLayout) -> (Int, Int)? { 187 | for (row, rowChars) in keyboard.enumerated() { 188 | if let colIndex = rowChars.firstIndex(of: char) { 189 | return (row, colIndex) 190 | } 191 | } 192 | return nil 193 | } 194 | 195 | // MARK: Disconnect WebSocket Connection 196 | 197 | public func disconnectFromTV() { 198 | webSocket?.disconnect() 199 | } 200 | 201 | // MARK: Handler Errors 202 | 203 | private func handleError(_ error: TVCommanderError) { 204 | delegate?.tvCommander(self, didEncounterError: error) 205 | } 206 | 207 | // MARK: Wake on LAN 208 | 209 | public static func wakeOnLAN( 210 | device: TVWakeOnLANDevice, 211 | queue: DispatchQueue = .global(), 212 | completion: @escaping (TVCommanderError?) -> Void 213 | ) { 214 | let connection = NWConnection( 215 | host: .init(device.broadcast), 216 | port: .init(rawValue: device.port)!, 217 | using: .udp 218 | ) 219 | connection.stateUpdateHandler = { newState in 220 | switch newState { 221 | case .ready: 222 | connection.send( 223 | content: .magicPacket(from: device), 224 | completion: .contentProcessed({ 225 | connection.cancel() 226 | completion($0.flatMap(TVCommanderError.wakeOnLANProcessingError)) 227 | }) 228 | ) 229 | case .failed(let error): 230 | completion(.wakeOnLANConnectionError(error)) 231 | default: 232 | break 233 | } 234 | } 235 | connection.start(queue: queue) 236 | } 237 | } 238 | 239 | // MARK: TVWebSocketHandlerDelegate 240 | 241 | extension TVCommander: TVWebSocketHandlerDelegate { 242 | func webSocketDidConnect() { 243 | isConnected = true 244 | delegate?.tvCommanderDidConnect(self) 245 | } 246 | 247 | func webSocketDidDisconnect() { 248 | isConnected = false 249 | authStatus = .none 250 | webSocket = nil 251 | delegate?.tvCommanderDidDisconnect(self) 252 | } 253 | 254 | func webSocketDidReadAuthStatus(_ authStatus: TVAuthStatus) { 255 | self.authStatus = authStatus 256 | delegate?.tvCommander(self, didUpdateAuthState: authStatus) 257 | } 258 | 259 | func webSocketDidReadAuthToken(_ authToken: TVAuthToken) { 260 | tvConfig.token = authToken 261 | } 262 | 263 | func webSocketError(_ error: TVCommanderError) { 264 | delegate?.tvCommander(self, didEncounterError: error) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Tests/TVCommanderKitTests/TVCommanderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import Starscream 4 | @testable import TVCommanderKit 5 | 6 | final class TVCommanderTests: XCTestCase { 7 | private var engine: MockTVWebSocketEngine! 8 | private var tvCommander: TVCommander! 9 | private var delegate: MockTVCommanderDelegate! 10 | 11 | override func setUp() { 12 | continueAfterFailure = false 13 | engine = MockTVWebSocketEngine() 14 | delegate = MockTVCommanderDelegate() 15 | tvCommander = TVCommander( 16 | tvConfig: .init( 17 | id: "test-tv-id", app: "test-app", 18 | path: "/api/v2/channels/samsung.remote.control", 19 | ipAddress: "192.168.0.1", port: 8002, 20 | scheme: "wss", token: "123456" 21 | ), 22 | webSocketCreator: MockTVWebSocketCreator(engine: engine) 23 | ) 24 | tvCommander.delegate = delegate 25 | } 26 | 27 | override func tearDown() { 28 | engine = nil 29 | tvCommander = nil 30 | delegate = nil 31 | } 32 | 33 | // MARK: Connects 34 | 35 | func test_connect_connected() { 36 | // given 37 | XCTAssertFalse(tvCommander.isConnected) 38 | XCTAssertNotNil(tvCommander.tvConfig.wssURL()) 39 | // when 40 | let expectation = expectation(description: "connect") 41 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 42 | tvCommander.connectToTV() 43 | wait(for: [expectation], timeout: 0.5) 44 | // then 45 | XCTAssertTrue(tvCommander.isConnected) 46 | } 47 | 48 | func test_connect_alreadyConnected_error() { 49 | // given 50 | var expectation = expectation(description: "connect") 51 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 52 | tvCommander.connectToTV() 53 | wait(for: [expectation], timeout: 0.5) 54 | // when 55 | expectation = self.expectation(description: "error") 56 | var error: TVCommanderError? 57 | delegate.onTVCommanderError = { 58 | error = $0 59 | expectation.fulfill() 60 | } 61 | tvCommander.connectToTV() 62 | wait(for: [expectation], timeout: 0.5) 63 | // then 64 | switch error { 65 | case .connectionAlreadyEstablished: 66 | break 67 | default: 68 | XCTFail() 69 | } 70 | XCTAssertTrue(tvCommander.isConnected) 71 | } 72 | 73 | // MARK: Connection Reads Packet 74 | 75 | func test_onPacket_valid_authStatusUpdated() { 76 | // given 77 | XCTAssertEqual(tvCommander.authStatus, .none) 78 | engine.mockReadPacket = #"{"data":{"clients":[{"attributes":{"name":"VGVzdA=="},"connectTime":1713369027676,"deviceName":"VGVzdA==","id":"502e895e-251f-48ca-b786-0f83b20102c5","isHost":false}],"id":"502e895e-251f-48ca-b786-0f83b20102c5","token":"99999999"},"event":"ms.channel.connect"}"# 79 | // when 80 | let expectation = expectation(description: "authorize") 81 | var authStatus: TVAuthStatus? 82 | delegate.onTVCommanderAuthStatusUpdate = { 83 | authStatus = $0 84 | expectation.fulfill() 85 | } 86 | tvCommander.connectToTV() 87 | wait(for: [expectation], timeout: 0.5) 88 | // then 89 | XCTAssertEqual(tvCommander.authStatus, authStatus) 90 | XCTAssertEqual(authStatus, .allowed) 91 | } 92 | 93 | func test_onPacket_invalid_error() { 94 | // given 95 | engine.mockReadPacket = "asdf" 96 | // when 97 | let expectation = expectation(description: "error") 98 | var error: TVCommanderError? 99 | delegate.onTVCommanderError = { 100 | error = $0 101 | expectation.fulfill() 102 | } 103 | tvCommander.connectToTV() 104 | wait(for: [expectation], timeout: 0.5) 105 | // then 106 | switch error { 107 | case .packetDataParsingFailed: 108 | break 109 | default: 110 | XCTFail() 111 | } 112 | } 113 | 114 | // MARK: Connection Writes Packets 115 | 116 | func test_sendCommand_notConnected_error() { 117 | // given 118 | XCTAssertFalse(tvCommander.isConnected) 119 | // when 120 | let expectation = expectation(description: "error") 121 | var error: TVCommanderError? 122 | delegate.onTVCommanderError = { 123 | error = $0 124 | expectation.fulfill() 125 | } 126 | tvCommander.sendRemoteCommand(key: .mute) 127 | wait(for: [expectation], timeout: 0.5) 128 | // then 129 | switch error { 130 | case .remoteCommandNotConnectedToTV: 131 | break 132 | default: 133 | XCTFail("wrong error received") 134 | } 135 | } 136 | 137 | func test_sendCommand_notAuthed_error() { 138 | // given 139 | var expectation = expectation(description: "connect") 140 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 141 | tvCommander.connectToTV() 142 | wait(for: [expectation], timeout: 0.5) 143 | XCTAssertNotEqual(tvCommander.authStatus, .allowed) 144 | // when 145 | expectation = self.expectation(description: "error") 146 | var error: TVCommanderError? 147 | delegate.onTVCommanderError = { 148 | error = $0 149 | expectation.fulfill() 150 | } 151 | tvCommander.sendRemoteCommand(key: .mute) 152 | wait(for: [expectation], timeout: 0.5) 153 | // then 154 | switch error { 155 | case .remoteCommandAuthenticationStatusNotAllowed: 156 | break 157 | default: 158 | XCTFail("wrong error received") 159 | } 160 | } 161 | 162 | func test_sendCommand_ideal_sent() { 163 | // given 164 | var expectation = expectation(description: "connect") 165 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 166 | tvCommander.connectToTV() 167 | wait(for: [expectation], timeout: 0.5) 168 | tvCommander.webSocketDidReadAuthStatus(.allowed) 169 | // when 170 | expectation = self.expectation(description: "send") 171 | var command: TVRemoteCommand? 172 | delegate.onTVCommanderRemoteCommand = { 173 | command = $0 174 | expectation.fulfill() 175 | } 176 | tvCommander.sendRemoteCommand(key: .mute) 177 | wait(for: [expectation], timeout: 0.5) 178 | // then 179 | XCTAssertEqual(command?.params.dataOfCmd, .mute) 180 | } 181 | 182 | func test_sendCommand_multipleCommands_sentInOrder() { 183 | // given 184 | var expectation = expectation(description: "connect") 185 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 186 | tvCommander.connectToTV() 187 | wait(for: [expectation], timeout: 0.5) 188 | tvCommander.webSocketDidReadAuthStatus(.allowed) 189 | // when 190 | let keys: [TVRemoteCommand.Params.ControlKey] = 191 | [.menu, .up, .down, .down, .channelList, .enter, .mute] 192 | expectation = self.expectation(description: "sends") 193 | expectation.expectedFulfillmentCount = keys.count 194 | var keysSent = [TVRemoteCommand.Params.ControlKey]() 195 | delegate.onTVCommanderRemoteCommand = { 196 | keysSent.append($0.params.dataOfCmd) 197 | expectation.fulfill() 198 | } 199 | keys.forEach { tvCommander.sendRemoteCommand(key: $0) } 200 | wait(for: [expectation], timeout: 0.5) 201 | // then 202 | XCTAssertEqual(keys, keysSent) 203 | } 204 | 205 | // MARK: Disconnects 206 | 207 | func test_disconnect_connectedAuthed_disconnectedAndReset() throws { 208 | // given 209 | var expectation = expectation(description: "connect") 210 | delegate.onTVCommanderDidConnect = { expectation.fulfill() } 211 | tvCommander.connectToTV() 212 | wait(for: [expectation], timeout: 0.5) 213 | tvCommander.webSocketDidReadAuthStatus(.allowed) 214 | // when 215 | expectation = self.expectation(description: "disconnect") 216 | delegate.onTVCommanderDidDisconnect = { expectation.fulfill() } 217 | tvCommander.disconnectFromTV() 218 | wait(for: [expectation], timeout: 0.5) 219 | // then 220 | XCTAssertFalse(tvCommander.isConnected) 221 | XCTAssertEqual(tvCommander.authStatus, .none) 222 | } 223 | } 224 | 225 | // MARK: - Mock WebSocket Creator 226 | 227 | private class MockTVWebSocketCreator: TVWebSocketCreator { 228 | private let engine: MockTVWebSocketEngine 229 | 230 | init(engine: MockTVWebSocketEngine) { 231 | self.engine = engine 232 | } 233 | 234 | override func createTVWebSocket( 235 | url: URL, 236 | certPinner: CertificatePinning?, 237 | delegate: WebSocketDelegate 238 | ) -> WebSocket { 239 | builder.setEngine(engine) 240 | return super.createTVWebSocket( 241 | url: url, certPinner: certPinner, 242 | delegate: delegate 243 | ) 244 | } 245 | } 246 | 247 | // MARK: - Mock WebSocket Engine 248 | 249 | private class MockTVWebSocketEngine: Engine { 250 | private weak var delegate: EngineDelegate? 251 | 252 | var mockReadDelay = TimeInterval(0) 253 | var mockReadPacket: String? 254 | private var mockRead: DispatchWorkItem? 255 | 256 | func register(delegate: EngineDelegate) { 257 | self.delegate = delegate 258 | } 259 | 260 | func start(request: URLRequest) { 261 | delegate?.didReceive(event: .connected([:])) 262 | guard let mockReadPacket else { return } 263 | mockRead?.cancel() 264 | let readWorkItem = DispatchWorkItem { [weak self] in 265 | guard let self else { return } 266 | self.delegate?.didReceive(event: .text(mockReadPacket)) 267 | } 268 | DispatchQueue.main.asyncAfter( 269 | deadline: .now() + mockReadDelay, execute: readWorkItem) 270 | mockRead = readWorkItem 271 | } 272 | 273 | func stop(closeCode: UInt16) { 274 | mockRead?.cancel() 275 | delegate?.didReceive(event: .disconnected("", closeCode)) 276 | } 277 | 278 | func forceStop() { 279 | stop(closeCode: 1006) 280 | } 281 | 282 | func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) { 283 | completion?() 284 | } 285 | 286 | func write(string: String, completion: (() -> ())?) { 287 | completion?() 288 | } 289 | } 290 | 291 | // MARK: - Mock Delegate 292 | 293 | private class MockTVCommanderDelegate: TVCommanderDelegate { 294 | var onTVCommanderDidConnect: (() -> Void)? 295 | var onTVCommanderDidDisconnect: (() -> Void)? 296 | var onTVCommanderAuthStatusUpdate: ((TVAuthStatus) -> Void)? 297 | var onTVCommanderRemoteCommand: ((TVRemoteCommand) -> Void)? 298 | var onTVCommanderError: ((TVCommanderError?) -> Void)? 299 | 300 | func tvCommanderDidConnect(_ tvCommander: TVCommander) { 301 | onTVCommanderDidConnect?() 302 | } 303 | 304 | func tvCommanderDidDisconnect(_ tvCommander: TVCommander) { 305 | onTVCommanderDidDisconnect?() 306 | } 307 | 308 | func tvCommander(_ tvCommander: TVCommander, didUpdateAuthState authStatus: TVAuthStatus) { 309 | onTVCommanderAuthStatusUpdate?(authStatus) 310 | } 311 | 312 | func tvCommander(_ tvCommander: TVCommander, didWriteRemoteCommand command: TVRemoteCommand) { 313 | onTVCommanderRemoteCommand?(command) 314 | } 315 | 316 | func tvCommander(_ tvCommander: TVCommander, didEncounterError error: TVCommanderError) { 317 | onTVCommanderError?(error) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-simulator/SmartView.framework/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64_x86_64-maccatalyst/SmartView.framework/Versions/A/Resources/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Sources/TVCommanderKit/TVCommanderKit+Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVCommanderKit+Models.swift 3 | // 4 | // 5 | // Created by Wilson Desimini on 9/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a TV discovered in a search 11 | public struct TV: Codable, Identifiable, Equatable { 12 | /// Represents detailed device information for a TV 13 | public struct Device: Codable, Equatable { 14 | public let countryCode: String? 15 | public let deviceDescription: String? 16 | public let developerIp: String? 17 | public let developerMode: String? 18 | public let duid: String? 19 | public let firmwareVersion: String? 20 | public let frameTvSupport: String? 21 | public let gamePadSupport: String? 22 | /// Unique identifier for the device, often the same as the TV id 23 | public let id: String? 24 | public let imeSyncedSupport: String? 25 | /// IP address of the TV on the network 26 | public let ip: String? 27 | public let language: String? 28 | public let model: String? 29 | public let modelName: String? 30 | public let name: String? 31 | public let networkType: String? 32 | public let os: String? 33 | /// Current power state of the TV, e.g., "on", "off", "standby" 34 | public let powerState: String? 35 | public let resolution: String? 36 | public let smartHubAgreement: String? 37 | public let ssid: String? 38 | /// Indicates whether the TV supports token-based authorization 39 | public let tokenAuthSupport: String 40 | public let type: String? 41 | public let udn: String? 42 | public let voiceSupport: String? 43 | public let wallScreenRatio: String? 44 | public let wallService: String? 45 | /// MAC address of the TV's Wi-Fi connection 46 | public let wifiMac: String 47 | 48 | public init( 49 | countryCode: String? = nil, 50 | deviceDescription: String? = nil, 51 | developerIp: String? = nil, 52 | developerMode: String? = nil, 53 | duid: String? = nil, 54 | firmwareVersion: String? = nil, 55 | frameTvSupport: String? = nil, 56 | gamePadSupport: String? = nil, 57 | id: String? = nil, 58 | imeSyncedSupport: String? = nil, 59 | ip: String? = nil, 60 | language: String? = nil, 61 | model: String? = nil, 62 | modelName: String? = nil, 63 | name: String? = nil, 64 | networkType: String? = nil, 65 | os: String? = nil, 66 | powerState: String? = nil, 67 | resolution: String? = nil, 68 | smartHubAgreement: String? = nil, 69 | ssid: String? = nil, 70 | tokenAuthSupport: String, 71 | type: String? = nil, 72 | udn: String? = nil, 73 | voiceSupport: String? = nil, 74 | wallScreenRatio: String? = nil, 75 | wallService: String? = nil, 76 | wifiMac: String 77 | ) { 78 | self.countryCode = countryCode 79 | self.deviceDescription = deviceDescription 80 | self.developerIp = developerIp 81 | self.developerMode = developerMode 82 | self.duid = duid 83 | self.firmwareVersion = firmwareVersion 84 | self.frameTvSupport = frameTvSupport 85 | self.gamePadSupport = gamePadSupport 86 | self.id = id 87 | self.imeSyncedSupport = imeSyncedSupport 88 | self.ip = ip 89 | self.language = language 90 | self.model = model 91 | self.modelName = modelName 92 | self.name = name 93 | self.networkType = networkType 94 | self.os = os 95 | self.powerState = powerState 96 | self.resolution = resolution 97 | self.smartHubAgreement = smartHubAgreement 98 | self.ssid = ssid 99 | self.tokenAuthSupport = tokenAuthSupport 100 | self.type = type 101 | self.udn = udn 102 | self.voiceSupport = voiceSupport 103 | self.wallScreenRatio = wallScreenRatio 104 | self.wallService = wallService 105 | self.wifiMac = wifiMac 106 | } 107 | 108 | enum CodingKeys: String, CodingKey { 109 | case countryCode 110 | case deviceDescription = "description" 111 | case developerIp = "developerIP" 112 | case developerMode 113 | case duid 114 | case firmwareVersion 115 | case frameTvSupport = "FrameTVSupport" 116 | case gamePadSupport = "GamePadSupport" 117 | case id 118 | case imeSyncedSupport = "ImeSyncedSupport" 119 | case ip 120 | case language = "Language" 121 | case model 122 | case modelName 123 | case name 124 | case networkType 125 | case os = "OS" 126 | case powerState = "PowerState" 127 | case resolution 128 | case smartHubAgreement 129 | case ssid 130 | case tokenAuthSupport = "TokenAuthSupport" 131 | case type 132 | case udn 133 | case voiceSupport = "VoiceSupport" 134 | case wallScreenRatio = "WallScreenRatio" 135 | case wallService = "WallService" 136 | case wifiMac 137 | } 138 | } 139 | 140 | /// Detailed information about the TV 141 | public let device: Device? 142 | /// Unique identifier for the TV 143 | public let id: String 144 | public let isSupport: String? 145 | /// User-friendly name of the TV 146 | public let name: String 147 | public let remote: String? 148 | public let type: String 149 | /// URI used to query the TV via HTTP 150 | public let uri: String 151 | public let version: String? 152 | 153 | public init( 154 | device: Device? = nil, 155 | id: String, 156 | isSupport: String? = nil, 157 | name: String, 158 | remote: String? = nil, 159 | type: String, 160 | uri: String, 161 | version: String? = nil 162 | ) { 163 | self.device = device 164 | self.id = id 165 | self.isSupport = isSupport 166 | self.name = name 167 | self.remote = remote 168 | self.type = type 169 | self.uri = uri 170 | self.version = version 171 | } 172 | } 173 | 174 | public enum TVAuthStatus { 175 | /// Client hasn't completed authorization with TV 176 | case none 177 | /// Client is authorized to command TV 178 | case allowed 179 | /// Client is denied authorization to command TV 180 | case denied 181 | } 182 | 183 | public typealias TVAuthToken = String 184 | 185 | public struct TVConnectionConfiguration { 186 | public let id: String? 187 | public let app: String 188 | public let path: String 189 | public let ipAddress: String 190 | public let port: Int 191 | public let scheme: String 192 | public var token: TVAuthToken? 193 | 194 | public init(id: String?, app: String, path: String, ipAddress: String, port: Int, scheme: String, token: TVAuthToken?) { 195 | self.id = id 196 | self.app = app 197 | self.path = path 198 | self.ipAddress = ipAddress 199 | self.port = port 200 | self.scheme = scheme 201 | self.token = token 202 | } 203 | } 204 | 205 | /// Defines the overall command to be sent to the TV 206 | public struct TVRemoteCommand: Encodable { 207 | public enum Method: String, Codable { 208 | case control = "ms.remote.control" 209 | } 210 | 211 | /// Contains the specific parameters for a remote command 212 | public struct Params: Encodable { 213 | public enum Command: RawRepresentable, Encodable { 214 | case click 215 | case press 216 | case release 217 | case textInput(String) 218 | 219 | public init?(rawValue: String) { 220 | nil // Never used 221 | } 222 | 223 | public var rawValue: String { 224 | switch self { 225 | case .click: 226 | return "Click" 227 | case .press: 228 | return "Press" 229 | case .release: 230 | return "Release" 231 | case .textInput(let text): 232 | return text 233 | } 234 | } 235 | } 236 | 237 | /// Enum representing the keys on a TV's remote control 238 | public enum ControlKey: String, Codable { 239 | case powerOff = "KEY_POWEROFF" 240 | case powerToggle = "KEY_POWER" 241 | case up = "KEY_UP" 242 | case down = "KEY_DOWN" 243 | case left = "KEY_LEFT" 244 | case right = "KEY_RIGHT" 245 | case enter = "KEY_ENTER" 246 | case returnKey = "KEY_RETURN" 247 | case channelUp = "KEY_CHUP" 248 | case channelDown = "KEY_CHDOWN" 249 | case channelList = "KEY_CH_LIST" 250 | case menu = "KEY_MENU" 251 | case source = "KEY_SOURCE" 252 | case guide = "KEY_GUIDE" 253 | case tools = "KEY_TOOLS" 254 | case info = "KEY_INFO" 255 | case colorRed = "KEY_RED" 256 | case colorGreen = "KEY_GREEN" 257 | case colorYellow = "KEY_YELLOW" 258 | case colorBlue = "KEY_BLUE" 259 | case key3D = "KEY_PANNEL_CHDOWN" 260 | case volumeUp = "KEY_VOLUP" 261 | case volumeDown = "KEY_VOLDOWN" 262 | case mute = "KEY_MUTE" 263 | case number0 = "KEY_0" 264 | case number1 = "KEY_1" 265 | case number2 = "KEY_2" 266 | case number3 = "KEY_3" 267 | case number4 = "KEY_4" 268 | case number5 = "KEY_5" 269 | case number6 = "KEY_6" 270 | case number7 = "KEY_7" 271 | case number8 = "KEY_8" 272 | case number9 = "KEY_9" 273 | case sourceTV = "KEY_DTV" 274 | case sourceHDMI = "KEY_HDMI" 275 | case contents = "KEY_CONTENTS" 276 | case base64 = "base64" // Used for text input 277 | } 278 | 279 | public enum ControlType: String, Codable { 280 | case inputEnd = "SendInputEnd" 281 | case inputString = "SendInputString" 282 | case mouseDevice = "ProcessMouseDevice" 283 | case remoteKey = "SendRemoteKey" 284 | } 285 | 286 | /// Command to be executed, e.g., "Click" 287 | public let cmd: Command 288 | /// Specific key data associated with the command 289 | public let dataOfCmd: ControlKey 290 | /// Additional option that may modify the command's execution 291 | public let option: Bool 292 | /// Type of the remote control that the command applies to, e.g., "SendRemoteKey" 293 | public let typeOfRemote: ControlType 294 | 295 | enum CodingKeys: String, CodingKey { 296 | case cmd = "Cmd" 297 | case dataOfCmd = "DataOfCmd" 298 | case option = "Option" 299 | case typeOfRemote = "TypeOfRemote" 300 | } 301 | 302 | public init(cmd: Command, dataOfCmd: ControlKey, option: Bool, typeOfRemote: ControlType) { 303 | self.cmd = cmd 304 | self.dataOfCmd = dataOfCmd 305 | self.option = option 306 | self.typeOfRemote = typeOfRemote 307 | } 308 | } 309 | 310 | /// The type of method performed via the WebSocket 311 | public let method: Method 312 | /// An object containing parameters needed to execute the command 313 | public let params: Params 314 | 315 | public init(method: Method, params: Params) { 316 | self.method = method 317 | self.params = params 318 | } 319 | } 320 | 321 | public struct TVResponse: Codable { 322 | public let data: Body? 323 | public let event: TVChannelEvent 324 | } 325 | 326 | public typealias TVAuthResponse = TVResponse 327 | 328 | /// Data payload associated with a TVAuthResponse 329 | public struct TVAuthResponseBody: Codable { 330 | /// List of clients connected to the TV 331 | public let clients: [TVClient] 332 | /// Identifier associated with an authorized connection 333 | public let id: String 334 | /// New token passed back with an authorized connection 335 | public let token: TVAuthToken? 336 | } 337 | 338 | public enum TVChannelEvent: String, Codable { 339 | case connect = "ms.channel.connect" 340 | case disconnect = "ms.channel.disconnect" 341 | case clientConnect = "ms.channel.clientConnect" 342 | case clientDisconnect = "ms.channel.clientDisconnect" 343 | case data = "ms.channel.data" 344 | case error = "ms.channel.error" 345 | case message = "ms.channel.message" 346 | case ping = "ms.channel.ping" 347 | case ready = "ms.channel.ready" 348 | case timeout = "ms.channel.timeOut" 349 | case unauthorized = "ms.channel.unauthorized" 350 | } 351 | 352 | /// Represents a client connected to the TV 353 | public struct TVClient: Codable, Identifiable { 354 | /// Attributes of a client connected to the TV 355 | public struct Attributes: Codable { 356 | /// Name of the client (encoded in Base64) 357 | public let name: String? 358 | /// Refreshed token associated with the client 359 | public let token: TVAuthToken? 360 | 361 | public init(name: String?, token: TVAuthToken?) { 362 | self.name = name 363 | self.token = token 364 | } 365 | } 366 | 367 | /// Attributes of the client 368 | public let attributes: Attributes 369 | /// Timestamp when the client connected 370 | public let connectTime: Int 371 | /// Name of the device (encoded in Base64) 372 | public let deviceName: String 373 | /// Unique identifier of the client's authorized connection 374 | public let id: String 375 | /// Indicates whether the client is the host or not 376 | public let isHost: Bool 377 | 378 | public init(attributes: Attributes, connectTime: Int, deviceName: String, id: String, isHost: Bool) { 379 | self.attributes = attributes 380 | self.connectTime = connectTime 381 | self.deviceName = deviceName 382 | self.id = id 383 | self.isHost = isHost 384 | } 385 | } 386 | 387 | public typealias TVKeyboardLayout = [[String]] 388 | 389 | public struct TVWakeOnLANDevice { 390 | public var mac: String 391 | public var broadcast: String 392 | public var port: UInt16 393 | 394 | public init(mac: String, broadcast: String = "255.255.255.255", port: UInt16 = 9) { 395 | self.mac = mac 396 | self.broadcast = broadcast 397 | self.port = port 398 | } 399 | 400 | public init(device: TV.Device, broadcast: String = "255.255.255.255", port: UInt16 = 9) { 401 | self.init(mac: device.wifiMac, broadcast: broadcast, port: port) 402 | } 403 | } 404 | 405 | public struct TVApp: Identifiable, Hashable { 406 | public let id: String 407 | public let name: String 408 | 409 | public init(id: String, name: String) { 410 | self.id = id 411 | self.name = name 412 | } 413 | } 414 | 415 | public struct TVAppStatus: Codable, Identifiable { 416 | public let id: String 417 | public let name: String 418 | public let running: Bool 419 | public let version: String 420 | public let visible: Bool 421 | 422 | public init(id: String, name: String, running: Bool, version: String, visible: Bool) { 423 | self.id = id 424 | self.name = name 425 | self.running = running 426 | self.version = version 427 | self.visible = visible 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /TVCommanderKitDemo/TVCommanderKitDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CF2920832AE855F300B8C25C /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF2920822AE855F300B8C25C /* ContentViewModel.swift */; }; 11 | CF88AD5E2AE854E6009C591B /* TVCommanderKitDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF88AD5D2AE854E6009C591B /* TVCommanderKitDemoApp.swift */; }; 12 | CF88AD602AE854E6009C591B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF88AD5F2AE854E6009C591B /* ContentView.swift */; }; 13 | CF88AD622AE854E7009C591B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CF88AD612AE854E7009C591B /* Assets.xcassets */; }; 14 | CF88AD652AE854E7009C591B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CF88AD642AE854E7009C591B /* Preview Assets.xcassets */; }; 15 | CF88AD6F2AE85533009C591B /* TVCommanderKit in Frameworks */ = {isa = PBXBuildFile; productRef = CF88AD6E2AE85533009C591B /* TVCommanderKit */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | CF2920822AE855F300B8C25C /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 20 | CF88AD5A2AE854E6009C591B /* TVCommanderKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TVCommanderKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | CF88AD5D2AE854E6009C591B /* TVCommanderKitDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCommanderKitDemoApp.swift; sourceTree = ""; }; 22 | CF88AD5F2AE854E6009C591B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | CF88AD612AE854E7009C591B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | CF88AD642AE854E7009C591B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | CF88AD6C2AE8551A009C591B /* TVCommanderKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TVCommanderKit; path = ..; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | CF88AD572AE854E6009C591B /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | CF88AD6F2AE85533009C591B /* TVCommanderKit in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | CF88AD512AE854E6009C591B = { 41 | isa = PBXGroup; 42 | children = ( 43 | CF88AD6B2AE8551A009C591B /* Packages */, 44 | CF88AD5C2AE854E6009C591B /* TVCommanderKitDemo */, 45 | CF88AD5B2AE854E6009C591B /* Products */, 46 | CF88AD6D2AE85533009C591B /* Frameworks */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | CF88AD5B2AE854E6009C591B /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | CF88AD5A2AE854E6009C591B /* TVCommanderKitDemo.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | CF88AD5C2AE854E6009C591B /* TVCommanderKitDemo */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | CF88AD5D2AE854E6009C591B /* TVCommanderKitDemoApp.swift */, 62 | CF88AD5F2AE854E6009C591B /* ContentView.swift */, 63 | CF2920822AE855F300B8C25C /* ContentViewModel.swift */, 64 | CF88AD612AE854E7009C591B /* Assets.xcassets */, 65 | CF88AD632AE854E7009C591B /* Preview Content */, 66 | ); 67 | path = TVCommanderKitDemo; 68 | sourceTree = ""; 69 | }; 70 | CF88AD632AE854E7009C591B /* Preview Content */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | CF88AD642AE854E7009C591B /* Preview Assets.xcassets */, 74 | ); 75 | path = "Preview Content"; 76 | sourceTree = ""; 77 | }; 78 | CF88AD6B2AE8551A009C591B /* Packages */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | CF88AD6C2AE8551A009C591B /* TVCommanderKit */, 82 | ); 83 | name = Packages; 84 | sourceTree = ""; 85 | }; 86 | CF88AD6D2AE85533009C591B /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | ); 90 | name = Frameworks; 91 | sourceTree = ""; 92 | }; 93 | /* End PBXGroup section */ 94 | 95 | /* Begin PBXNativeTarget section */ 96 | CF88AD592AE854E6009C591B /* TVCommanderKitDemo */ = { 97 | isa = PBXNativeTarget; 98 | buildConfigurationList = CF88AD682AE854E7009C591B /* Build configuration list for PBXNativeTarget "TVCommanderKitDemo" */; 99 | buildPhases = ( 100 | CF88AD562AE854E6009C591B /* Sources */, 101 | CF88AD572AE854E6009C591B /* Frameworks */, 102 | CF88AD582AE854E6009C591B /* Resources */, 103 | ); 104 | buildRules = ( 105 | ); 106 | dependencies = ( 107 | ); 108 | name = TVCommanderKitDemo; 109 | packageProductDependencies = ( 110 | CF88AD6E2AE85533009C591B /* TVCommanderKit */, 111 | ); 112 | productName = TVCommanderKitDemo; 113 | productReference = CF88AD5A2AE854E6009C591B /* TVCommanderKitDemo.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | CF88AD522AE854E6009C591B /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | BuildIndependentTargetsInParallel = 1; 123 | LastSwiftUpdateCheck = 1430; 124 | LastUpgradeCheck = 1430; 125 | TargetAttributes = { 126 | CF88AD592AE854E6009C591B = { 127 | CreatedOnToolsVersion = 14.3.1; 128 | }; 129 | }; 130 | }; 131 | buildConfigurationList = CF88AD552AE854E6009C591B /* Build configuration list for PBXProject "TVCommanderKitDemo" */; 132 | compatibilityVersion = "Xcode 14.0"; 133 | developmentRegion = en; 134 | hasScannedForEncodings = 0; 135 | knownRegions = ( 136 | en, 137 | Base, 138 | ); 139 | mainGroup = CF88AD512AE854E6009C591B; 140 | productRefGroup = CF88AD5B2AE854E6009C591B /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | CF88AD592AE854E6009C591B /* TVCommanderKitDemo */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | CF88AD582AE854E6009C591B /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | CF88AD652AE854E7009C591B /* Preview Assets.xcassets in Resources */, 155 | CF88AD622AE854E7009C591B /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXSourcesBuildPhase section */ 162 | CF88AD562AE854E6009C591B /* Sources */ = { 163 | isa = PBXSourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | CF88AD602AE854E6009C591B /* ContentView.swift in Sources */, 167 | CF88AD5E2AE854E6009C591B /* TVCommanderKitDemoApp.swift in Sources */, 168 | CF2920832AE855F300B8C25C /* ContentViewModel.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin XCBuildConfiguration section */ 175 | CF88AD662AE854E7009C591B /* Debug */ = { 176 | isa = XCBuildConfiguration; 177 | buildSettings = { 178 | ALWAYS_SEARCH_USER_PATHS = NO; 179 | CLANG_ANALYZER_NONNULL = YES; 180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 182 | CLANG_ENABLE_MODULES = YES; 183 | CLANG_ENABLE_OBJC_ARC = YES; 184 | CLANG_ENABLE_OBJC_WEAK = YES; 185 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 186 | CLANG_WARN_BOOL_CONVERSION = YES; 187 | CLANG_WARN_COMMA = YES; 188 | CLANG_WARN_CONSTANT_CONVERSION = YES; 189 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 191 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 192 | CLANG_WARN_EMPTY_BODY = YES; 193 | CLANG_WARN_ENUM_CONVERSION = YES; 194 | CLANG_WARN_INFINITE_RECURSION = YES; 195 | CLANG_WARN_INT_CONVERSION = YES; 196 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 198 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 200 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 202 | CLANG_WARN_STRICT_PROTOTYPES = YES; 203 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 204 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 205 | CLANG_WARN_UNREACHABLE_CODE = YES; 206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 207 | COPY_PHASE_STRIP = NO; 208 | DEBUG_INFORMATION_FORMAT = dwarf; 209 | ENABLE_STRICT_OBJC_MSGSEND = YES; 210 | ENABLE_TESTABILITY = YES; 211 | GCC_C_LANGUAGE_STANDARD = gnu11; 212 | GCC_DYNAMIC_NO_PIC = NO; 213 | GCC_NO_COMMON_BLOCKS = YES; 214 | GCC_OPTIMIZATION_LEVEL = 0; 215 | GCC_PREPROCESSOR_DEFINITIONS = ( 216 | "DEBUG=1", 217 | "$(inherited)", 218 | ); 219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 221 | GCC_WARN_UNDECLARED_SELECTOR = YES; 222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 223 | GCC_WARN_UNUSED_FUNCTION = YES; 224 | GCC_WARN_UNUSED_VARIABLE = YES; 225 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; 226 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 227 | MTL_FAST_MATH = YES; 228 | ONLY_ACTIVE_ARCH = YES; 229 | SDKROOT = iphoneos; 230 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 232 | }; 233 | name = Debug; 234 | }; 235 | CF88AD672AE854E7009C591B /* Release */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | ALWAYS_SEARCH_USER_PATHS = NO; 239 | CLANG_ANALYZER_NONNULL = YES; 240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_ENABLE_OBJC_WEAK = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 252 | CLANG_WARN_EMPTY_BODY = YES; 253 | CLANG_WARN_ENUM_CONVERSION = YES; 254 | CLANG_WARN_INFINITE_RECURSION = YES; 255 | CLANG_WARN_INT_CONVERSION = YES; 256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 262 | CLANG_WARN_STRICT_PROTOTYPES = YES; 263 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 265 | CLANG_WARN_UNREACHABLE_CODE = YES; 266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 267 | COPY_PHASE_STRIP = NO; 268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 269 | ENABLE_NS_ASSERTIONS = NO; 270 | ENABLE_STRICT_OBJC_MSGSEND = YES; 271 | GCC_C_LANGUAGE_STANDARD = gnu11; 272 | GCC_NO_COMMON_BLOCKS = YES; 273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 275 | GCC_WARN_UNDECLARED_SELECTOR = YES; 276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 277 | GCC_WARN_UNUSED_FUNCTION = YES; 278 | GCC_WARN_UNUSED_VARIABLE = YES; 279 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; 280 | MTL_ENABLE_DEBUG_INFO = NO; 281 | MTL_FAST_MATH = YES; 282 | SDKROOT = iphoneos; 283 | SWIFT_COMPILATION_MODE = wholemodule; 284 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 285 | VALIDATE_PRODUCT = YES; 286 | }; 287 | name = Release; 288 | }; 289 | CF88AD692AE854E7009C591B /* Debug */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 294 | CODE_SIGN_STYLE = Automatic; 295 | CURRENT_PROJECT_VERSION = 1; 296 | DEVELOPMENT_ASSET_PATHS = "\"TVCommanderKitDemo/Preview Content\""; 297 | DEVELOPMENT_TEAM = 42UG54QP3B; 298 | ENABLE_PREVIEWS = YES; 299 | GENERATE_INFOPLIST_FILE = YES; 300 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 301 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 302 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 305 | LD_RUNPATH_SEARCH_PATHS = ( 306 | "$(inherited)", 307 | "@executable_path/Frameworks", 308 | ); 309 | MARKETING_VERSION = 1.0; 310 | PRODUCT_BUNDLE_IDENTIFIER = com.WilsonDesimini.TVCommanderKitDemo; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SWIFT_EMIT_LOC_STRINGS = YES; 313 | SWIFT_VERSION = 5.0; 314 | TARGETED_DEVICE_FAMILY = "1,2"; 315 | }; 316 | name = Debug; 317 | }; 318 | CF88AD6A2AE854E7009C591B /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 322 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 323 | CODE_SIGN_STYLE = Automatic; 324 | CURRENT_PROJECT_VERSION = 1; 325 | DEVELOPMENT_ASSET_PATHS = "\"TVCommanderKitDemo/Preview Content\""; 326 | DEVELOPMENT_TEAM = 42UG54QP3B; 327 | ENABLE_PREVIEWS = YES; 328 | GENERATE_INFOPLIST_FILE = YES; 329 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 330 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 331 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 333 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 334 | LD_RUNPATH_SEARCH_PATHS = ( 335 | "$(inherited)", 336 | "@executable_path/Frameworks", 337 | ); 338 | MARKETING_VERSION = 1.0; 339 | PRODUCT_BUNDLE_IDENTIFIER = com.WilsonDesimini.TVCommanderKitDemo; 340 | PRODUCT_NAME = "$(TARGET_NAME)"; 341 | SWIFT_EMIT_LOC_STRINGS = YES; 342 | SWIFT_VERSION = 5.0; 343 | TARGETED_DEVICE_FAMILY = "1,2"; 344 | }; 345 | name = Release; 346 | }; 347 | /* End XCBuildConfiguration section */ 348 | 349 | /* Begin XCConfigurationList section */ 350 | CF88AD552AE854E6009C591B /* Build configuration list for PBXProject "TVCommanderKitDemo" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | CF88AD662AE854E7009C591B /* Debug */, 354 | CF88AD672AE854E7009C591B /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | CF88AD682AE854E7009C591B /* Build configuration list for PBXNativeTarget "TVCommanderKitDemo" */ = { 360 | isa = XCConfigurationList; 361 | buildConfigurations = ( 362 | CF88AD692AE854E7009C591B /* Debug */, 363 | CF88AD6A2AE854E7009C591B /* Release */, 364 | ); 365 | defaultConfigurationIsVisible = 0; 366 | defaultConfigurationName = Release; 367 | }; 368 | /* End XCConfigurationList section */ 369 | 370 | /* Begin XCSwiftPackageProductDependency section */ 371 | CF88AD6E2AE85533009C591B /* TVCommanderKit */ = { 372 | isa = XCSwiftPackageProductDependency; 373 | productName = TVCommanderKit; 374 | }; 375 | /* End XCSwiftPackageProductDependency section */ 376 | }; 377 | rootObject = CF88AD522AE854E6009C591B /* Project object */; 378 | } 379 | -------------------------------------------------------------------------------- /SmartView.xcframework/ios-arm64/SmartView.framework/Modules/SmartView.swiftmodule/arm64-apple-ios.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.7.1 (swiftlang-5.7.1.135.3 clang-1400.0.29.51) 3 | // swift-module-flags: -target arm64-apple-ios8.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name SmartView 4 | // swift-module-flags-ignorable: -enable-bare-slash-regex 5 | import SystemConfiguration.CaptiveNetwork 6 | import CoreBluetooth 7 | import CoreFoundation 8 | import Darwin 9 | import Foundation 10 | import SmartView.PrivateCode 11 | import Security 12 | @_exported import SmartView 13 | import Swift 14 | import SystemConfiguration 15 | import UIKit 16 | import _Concurrency 17 | import _StringProcessing 18 | @_hasMissingDesignatedInitializers @objc open class ChannelClient : ObjectiveC.NSObject { 19 | open var id: Swift.String { 20 | get 21 | } 22 | open var connectTime: Foundation.Date? { 23 | get 24 | } 25 | open var attributes: Swift.AnyObject? { 26 | get 27 | } 28 | open var isHost: Swift.Bool { 29 | get 30 | } 31 | @objc override dynamic open var description: Swift.String { 32 | @objc get 33 | } 34 | @objc deinit 35 | } 36 | public func == (lhs: SmartView.ChannelClient, rhs: SmartView.ChannelClient) -> Swift.Bool 37 | @_hasMissingDesignatedInitializers @objc open class Message : ObjectiveC.NSObject { 38 | final public let event: Swift.String! 39 | final public let from: Swift.String! 40 | final public let data: Swift.AnyObject? 41 | @objc deinit 42 | } 43 | @objc public protocol ConnectionDelegate { 44 | @objc optional func onConnect(_ error: Foundation.NSError?) 45 | @objc optional func onDisconnect(_ error: Foundation.NSError?) 46 | @objc optional func onClientConnect(_ client: SmartView.ChannelClient) 47 | @objc optional func onClientDisconnect(_ client: SmartView.ChannelClient) 48 | @objc optional func onError(_ error: Foundation.NSError) 49 | @objc optional func onReady() 50 | } 51 | @_hasMissingDesignatedInitializers @objc open class BasePlayer : ObjectiveC.NSObject { 52 | weak open var connectionDelegate: SmartView.ConnectionDelegate? 53 | @objc deinit 54 | @objc open func disconnect(_ leaveHostRunning: Swift.Bool = true, completionHandler: ((_ error: Foundation.NSError?) -> Swift.Void)? = nil) 55 | @objc open func standbyConnect(_ screenSaverURL1: Foundation.URL? = nil, completionHandler: ((Foundation.NSError?) -> Swift.Void)?) 56 | @objc open func standbyConnect(_ screenSaverURL1: Foundation.URL?, screenSaverURL2: Foundation.URL?, completionHandler: ((Foundation.NSError?) -> Swift.Void)?) 57 | @objc open func standbyConnect(_ screenSaverURL1: Foundation.URL?, screenSaverURL2: Foundation.URL?, screenSaverURL3: Foundation.URL?, completionHandler: ((Foundation.NSError?) -> Swift.Void)?) 58 | @objc open func setPlayerWatermark(_ watermarkUrl: Foundation.URL?) 59 | @objc open func removePlayerWatermark() 60 | @objc open func play() 61 | @objc open func pause() 62 | @objc open func stop() 63 | @objc open func mute() 64 | @objc open func unMute() 65 | @objc open func previous() 66 | @objc open func next() 67 | @objc open func setVolume(_ volume: Swift.UInt8) 68 | @objc open func volumeUp() 69 | @objc open func volumeDown() 70 | @objc open func getControlStatus() 71 | } 72 | @_hasMissingDesignatedInitializers @objc open class MediaPlayer : ObjectiveC.NSObject { 73 | public struct PlayerNotice { 74 | public static let PlayerStateKey: Swift.String 75 | public static let VideoStateKey: Swift.String 76 | public static let ErrorKey: Swift.String 77 | public struct PlayerState { 78 | public static let Play: Swift.String 79 | public static let Pause: Swift.String 80 | public static let Stop: Swift.String 81 | public static let Mute: Swift.String 82 | public static let Unmute: Swift.String 83 | public static let FF: Swift.String 84 | public static let RWD: Swift.String 85 | public static let SeekTo: Swift.String 86 | public static let SetVolume: Swift.String 87 | } 88 | public struct VideoState { 89 | public static let StreamCompleted: Swift.String 90 | public static let CurrentPlayTime: Swift.String 91 | public static let TotalDuration: Swift.String 92 | public static let BufferingStart: Swift.String 93 | public static let BufferingProgress: Swift.String 94 | public static let BufferingComplete: Swift.String 95 | public static let VideoIsCued: Swift.String 96 | } 97 | } 98 | open var service: SmartView.Service { 99 | get 100 | } 101 | open var connected: Swift.Bool { 102 | get 103 | } 104 | @objc deinit 105 | } 106 | extension SmartView.MediaPlayer : SmartView.ChannelDelegate { 107 | @objc dynamic public func onMessage(_ message: SmartView.Message) 108 | @objc dynamic public func onConnect(_ client: SmartView.ChannelClient?, error: Foundation.NSError?) 109 | @objc dynamic public func onDisconnect(_ client: SmartView.ChannelClient?, error: Foundation.NSError?) 110 | @objc dynamic public func onClientConnect(_ client: SmartView.ChannelClient) 111 | @objc dynamic public func onClientDisconnect(_ client: SmartView.ChannelClient) 112 | @objc dynamic public func onError(_ error: Foundation.NSError) 113 | @objc dynamic public func onReady() 114 | } 115 | public let WebsocketDidConnectNotification: Swift.String 116 | public let WebsocketDidDisconnectNotification: Swift.String 117 | public let WebsocketDisconnectionErrorKeyName: Swift.String 118 | public protocol WebSocketDelegate : AnyObject { 119 | func websocketDidConnect(_ socket: SmartView.WebSocket) 120 | func websocketDidDisconnect(_ socket: SmartView.WebSocket, error: Foundation.NSError?) 121 | func websocketDidReceiveMessage(_ socket: SmartView.WebSocket, text: Swift.String) 122 | func websocketDidReceiveData(_ socket: SmartView.WebSocket, data: Foundation.Data) 123 | } 124 | public protocol WebSocketPongDelegate : AnyObject { 125 | func websocketDidReceivePong(socket: SmartView.WebSocket, data: Foundation.Data?) 126 | } 127 | @objc public class WebSocket : ObjectiveC.NSObject, Foundation.StreamDelegate { 128 | public enum CloseCode : Swift.UInt16 { 129 | case normal 130 | case goingAway 131 | case protocolError 132 | case protocolUnhandledType 133 | case noStatusReceived 134 | case encoding 135 | case policyViolated 136 | case messageTooBig 137 | public init?(rawValue: Swift.UInt16) 138 | public typealias RawValue = Swift.UInt16 139 | public var rawValue: Swift.UInt16 { 140 | get 141 | } 142 | } 143 | public static let ErrorDomain: Swift.String 144 | public var callbackQueue: Dispatch.DispatchQueue 145 | weak public var delegate: SmartView.WebSocketDelegate? 146 | weak public var pongDelegate: SmartView.WebSocketPongDelegate? 147 | public var onConnect: ((Swift.Void) -> Swift.Void)? 148 | public var onDisconnect: ((Foundation.NSError?) -> Swift.Void)? 149 | public var onText: ((Swift.String) -> Swift.Void)? 150 | public var onData: ((Foundation.Data) -> Swift.Void)? 151 | public var onPong: ((Foundation.Data?) -> Swift.Void)? 152 | public var headers: [Swift.String : Swift.String] 153 | public var voipEnabled: Swift.Bool 154 | public var disableSSLCertValidation: Swift.Bool 155 | public var security: SmartView.SSLSecurity? 156 | public var enabledSSLCipherSuites: [Security.SSLCipherSuite]? 157 | public var origin: Swift.String? 158 | public var timeout: Swift.Int 159 | public var isConnected: Swift.Bool { 160 | get 161 | } 162 | public var currentURL: Foundation.URL { 163 | get 164 | } 165 | public init(url: Foundation.URL, protocols: [Swift.String]? = nil) 166 | convenience public init(url: Foundation.URL, writeQueueQOS: Foundation.QualityOfService, protocols: [Swift.String]? = nil) 167 | public func connect() 168 | public func disconnect(forceTimeout: Foundation.TimeInterval? = nil, closeCode: Swift.UInt16 = CloseCode.normal.rawValue) 169 | public func write(string: Swift.String, completion: (() -> ())? = nil) 170 | public func write(data: Foundation.Data, completion: (() -> ())? = nil) 171 | public func write(ping: Foundation.Data, completion: (() -> ())? = nil) 172 | @objc public func stream(_ aStream: Foundation.Stream, handle eventCode: Foundation.Stream.Event) 173 | @objc deinit 174 | } 175 | @objc public protocol AudioPlayerDelegate { 176 | @objc optional func onBufferingStart() 177 | @objc optional func onBufferingComplete() 178 | @objc optional func onBufferingProgress(_ progress: Swift.Int) 179 | @objc optional func onCurrentPlayTime(_ progress: Swift.Int) 180 | @objc optional func onStreamingStarted(_ duration: Swift.Int) 181 | @objc optional func onStreamCompleted() 182 | @objc optional func onPlayerInitialized() 183 | @objc optional func onPlayerChange(_ playerType: Swift.String) 184 | @objc optional func onPlay() 185 | @objc optional func onPause() 186 | @objc optional func onStop() 187 | @objc optional func onMute() 188 | @objc optional func onUnMute() 189 | @objc optional func onNext() 190 | @objc optional func onPrevious() 191 | @objc optional func onControlStatus(_ volLevel: Swift.Int, muteStatus: Swift.Bool, shuffleStatus: Swift.Bool, mode: Swift.String) 192 | @objc optional func onVolumeChange(_ volLevel: Swift.Int) 193 | @objc optional func onAddToList(_ enqueuedItem: [Swift.String : Swift.AnyObject]) 194 | @objc optional func onRemoveFromList(_ dequeuedItem: [Swift.String : Swift.AnyObject]) 195 | @objc optional func onClearList() 196 | @objc optional func onGetList(_ queueList: [Swift.String : Swift.AnyObject]) 197 | @objc optional func onShuffle(_ status: Swift.Bool) 198 | @objc optional func onRepeat(_ mode: Swift.String) 199 | @objc optional func onCurrentPlaying(_ currentItem: [Swift.String : Swift.AnyObject]) 200 | @objc optional func onApplicationSuspend() 201 | @objc optional func onApplicationResume() 202 | @objc optional func onError(_ error: Foundation.NSError) 203 | } 204 | @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @objc open class AudioPlayer : SmartView.BasePlayer { 205 | weak open var playerDelegate: SmartView.AudioPlayerDelegate? 206 | @objc public enum AudioRepeatMode : Swift.Int { 207 | case repeatOff 208 | case repeatSingle 209 | case repeatAll 210 | public init?(rawValue: Swift.Int) 211 | public typealias RawValue = Swift.Int 212 | public var rawValue: Swift.Int { 213 | get 214 | } 215 | } 216 | @objc deinit 217 | @objc open func playContent(_ contentURL: Foundation.URL, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 218 | @objc open func playContent(_ contentURL: Foundation.URL?, title: Swift.String, albumName: Swift.String, albumArtUrl: Foundation.URL?, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 219 | @objc open func seek(_ time: Foundation.TimeInterval) 220 | @objc open func resumeApplicationInForeground(_ completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 221 | @objc open func `repeat`() 222 | @objc open func setRepeat(_ mode: SmartView.AudioPlayer.AudioRepeatMode) 223 | @objc open func shuffle() 224 | @objc open func setShuffle(_ mode: Swift.Bool) 225 | @objc open func getList() 226 | @objc open func clearList() 227 | @objc open func removeFromList(_ contentURL: Foundation.URL) 228 | @objc open func addToList(_ contentURL: Foundation.URL, title: Swift.String = "", albumName: Swift.String = "", albumArtUrl: Foundation.URL = URL(fileURLWithPath: "")) 229 | @objc public func addToList(_ arrayDictofData: [[Swift.String : Swift.AnyObject]]) 230 | @objc open func onMessage(_ notification: Foundation.Notification!) 231 | } 232 | public enum MessageTarget : Swift.String { 233 | case All 234 | case Host 235 | case Broadcast 236 | public init?(rawValue: Swift.String) 237 | public typealias RawValue = Swift.String 238 | public var rawValue: Swift.String { 239 | get 240 | } 241 | } 242 | public enum ChannelEvent : Swift.String { 243 | case Connect 244 | case Disconnect 245 | case ClientConnect 246 | case ClientDisconnect 247 | case Message 248 | case Data 249 | case Unauthorized 250 | case Timeout 251 | case Error 252 | case Ready 253 | case Ping 254 | public init?(rawValue: Swift.String) 255 | public typealias RawValue = Swift.String 256 | public var rawValue: Swift.String { 257 | get 258 | } 259 | } 260 | @objc public protocol ChannelDelegate { 261 | @objc optional func onConnect(_ client: SmartView.ChannelClient?, error: Foundation.NSError?) 262 | @objc optional func onReady() 263 | @objc optional func onDisconnect(_ client: SmartView.ChannelClient?, error: Foundation.NSError?) 264 | @objc optional func onMessage(_ message: SmartView.Message) 265 | @objc optional func onData(_ message: SmartView.Message, payload: Foundation.Data) 266 | @objc optional func onClientConnect(_ client: SmartView.ChannelClient) 267 | @objc optional func onClientDisconnect(_ client: SmartView.ChannelClient) 268 | @objc optional func onError(_ error: Foundation.NSError) 269 | } 270 | @_hasMissingDesignatedInitializers @objc open class Channel : ObjectiveC.NSObject { 271 | open var isConnected: Swift.Bool { 272 | get 273 | } 274 | open var uri: Swift.String! { 275 | get 276 | } 277 | open var service: SmartView.Service! { 278 | get 279 | } 280 | open var me: SmartView.ChannelClient! 281 | open var completionQueue: Dispatch.DispatchQueue? 282 | weak open var delegate: SmartView.ChannelDelegate? 283 | open var connectionTimeout: Swift.Double { 284 | get 285 | set 286 | } 287 | open func connect() 288 | open func connect(_ attributes: [Swift.String : Swift.String]?) 289 | open func connect(_ attributes: [Swift.String : Swift.String]?, completionHandler: ((_ client: SmartView.ChannelClient?, _ error: Foundation.NSError?) -> Swift.Void)?) 290 | open func disconnect(_ completionHandler: ((_ client: SmartView.ChannelClient?, _ error: Foundation.NSError?) -> Swift.Void)?) 291 | open func disconnect() 292 | open func publish(event: Swift.String, message: Swift.AnyObject?) 293 | open func publish(event: Swift.String, message: Swift.AnyObject?, data: Foundation.Data) 294 | open func publish(event: Swift.String, message: Swift.AnyObject?, target: Swift.AnyObject) 295 | open func publish(event: Swift.String, message: Swift.AnyObject?, data: Foundation.Data, target: Swift.AnyObject) 296 | open func getClients() -> [SmartView.ChannelClient] 297 | open func on(_ notificationName: Swift.String, performClosure: @escaping (Foundation.Notification?) -> Swift.Void) -> Swift.AnyObject? 298 | open func off(_ observer: Swift.AnyObject) 299 | @objc override dynamic open var description: Swift.String { 300 | @objc get 301 | } 302 | open func setSecurityMode(security: Swift.Bool, completionHandler: @escaping (_ isSupport: Swift.Bool, _ error: Foundation.NSError?) -> Swift.Void) 303 | @objc deinit 304 | } 305 | @objc public protocol VideoPlayerDelegate { 306 | @objc optional func onBufferingStart() 307 | @objc optional func onBufferingComplete() 308 | @objc optional func onBufferingProgress(_ progress: Swift.Int) 309 | @objc optional func onCurrentPlayTime(_ progress: Swift.Int) 310 | @objc optional func onStreamingStarted(_ duration: Swift.Int) 311 | @objc optional func onStreamCompleted() 312 | @objc optional func onPlayerInitialized() 313 | @objc optional func onPlayerChange(_ playerType: Swift.String) 314 | @objc optional func onPlay() 315 | @objc optional func onPause() 316 | @objc optional func onStop() 317 | @objc optional func onForward() 318 | @objc optional func onRewind() 319 | @objc optional func onMute() 320 | @objc optional func onUnMute() 321 | @objc optional func onNext() 322 | @objc optional func onPrevious() 323 | @objc optional func onControlStatus(_ volLevel: Swift.Int, muteStatus: Swift.Bool, mode: Swift.String) 324 | @objc optional func onVolumeChange(_ volLevel: Swift.Int) 325 | @objc optional func onAddToList(_ enqueuedItem: [Swift.String : Swift.AnyObject]) 326 | @objc optional func onRemoveFromList(_ dequeuedItem: [Swift.String : Swift.AnyObject]) 327 | @objc optional func onClearList() 328 | @objc optional func onGetList(_ queueList: [Swift.String : Swift.AnyObject]) 329 | @objc optional func onRepeat(_ mode: Swift.String) 330 | @objc optional func onCurrentPlaying(_ currentItem: [Swift.String : Swift.AnyObject]) 331 | @objc optional func onApplicationSuspend() 332 | @objc optional func onApplicationResume() 333 | @objc optional func onError(_ error: Foundation.NSError) 334 | } 335 | @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @objc open class VideoPlayer : SmartView.BasePlayer { 336 | weak open var playerDelegate: SmartView.VideoPlayerDelegate? 337 | @objc public enum VideoRepeatMode : Swift.Int { 338 | case repeatOff 339 | case repeatSingle 340 | case repeatAll 341 | public init?(rawValue: Swift.Int) 342 | public typealias RawValue = Swift.Int 343 | public var rawValue: Swift.Int { 344 | get 345 | } 346 | } 347 | @objc deinit 348 | @objc open func playContent(_ contentURL: Foundation.URL, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 349 | @objc open func playContent(_ contentURL: Foundation.URL?, title: Swift.String, thumbnailURL: Foundation.URL?, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 350 | @objc open func forward() 351 | @objc open func rewind() 352 | @objc open func seek(_ time: Foundation.TimeInterval) 353 | @objc open func `repeat`() 354 | @objc open func setRepeat(_ mode: SmartView.VideoPlayer.VideoRepeatMode) 355 | @objc open func resumeApplicationInForeground(_ completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 356 | @objc open func getList() 357 | @objc open func clearList() 358 | @objc open func removeFromList(_ contentURL: Foundation.URL) 359 | @objc open func addToList(_ contentURL: Foundation.URL, title: Swift.String = "", thumbnailURL: Foundation.URL = URL(fileURLWithPath: "")) 360 | @objc public func addToList(_ arrayDictofData: [[Swift.String : Swift.AnyObject]]) 361 | @objc open func onMessage(_ notification: Foundation.Notification!) 362 | } 363 | @objc public protocol PhotoPlayerDelegate { 364 | @objc optional func onPlayerInitialized() 365 | @objc optional func onPlayerChange(_ playerType: Swift.String) 366 | @objc optional func onPlay() 367 | @objc optional func onPause() 368 | @objc optional func onStop() 369 | @objc optional func onMute() 370 | @objc optional func onUnMute() 371 | @objc optional func onNext() 372 | @objc optional func onPrevious() 373 | @objc optional func onControlStatus(_ volLevel: Swift.Int, muteStatus: Swift.Bool) 374 | @objc optional func onVolumeChange(_ volLevel: Swift.Int) 375 | @objc optional func onAddToList(_ enqueuedItem: [Swift.String : Swift.AnyObject]) 376 | @objc optional func onRemoveFromList(_ dequeuedItem: [Swift.String : Swift.AnyObject]) 377 | @objc optional func onClearList() 378 | @objc optional func onGetList(_ queueList: [Swift.String : Swift.AnyObject]) 379 | @objc optional func onCurrentPlaying(_ currentItem: [Swift.String : Swift.AnyObject]) 380 | @objc optional func onApplicationSuspend() 381 | @objc optional func onApplicationResume() 382 | @objc optional func onError(_ error: Foundation.NSError) 383 | } 384 | @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @objc open class PhotoPlayer : SmartView.BasePlayer { 385 | weak open var playerDelegate: SmartView.PhotoPlayerDelegate? 386 | @objc deinit 387 | @objc open func playContent(_ contentURL: Foundation.URL, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 388 | @objc open func playContent(_ contentURL: Foundation.URL?, title: Swift.String, completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 389 | @objc open func setBackgroundMusic(_ contentURL: Foundation.URL) 390 | @objc open func stopBackgroundMusic() 391 | @objc open func resumeApplicationInForeground(_ completionHandler: ((Foundation.NSError?) -> Swift.Void)? = nil) 392 | @objc open func getList() 393 | @objc open func clearList() 394 | @objc open func removeFromList(_ contentURL: Foundation.URL) 395 | @objc open func addToList(_ contentURL: Foundation.URL, title: Swift.String = "") 396 | @objc public func addToList(_ arrayDictofData: [[Swift.String : Swift.AnyObject]]) 397 | @objc open func onMessage(_ notification: Foundation.Notification!) 398 | } 399 | public let MSDidFindService: Swift.String 400 | public let MSDidRemoveService: Swift.String 401 | public let MSDidStopSearch: Swift.String 402 | public let MSDidStartSearch: Swift.String 403 | public let MSDidFoundUsingBLE: Swift.String 404 | @objc public enum ServiceSearchDiscoveryType : Swift.Int { 405 | case LAN 406 | case CLOUD 407 | public init?(rawValue: Swift.Int) 408 | public typealias RawValue = Swift.Int 409 | public var rawValue: Swift.Int { 410 | get 411 | } 412 | } 413 | @objc public protocol ServiceSearchDelegate { 414 | @objc optional func onServiceFound(_ service: SmartView.Service) 415 | @objc optional func onServiceLost(_ service: SmartView.Service) 416 | @objc optional func onStop() 417 | @objc optional func onStart() 418 | @objc optional func onFoundOnlyBLE(_ NameOfTV: Swift.String) 419 | @objc optional func onFoundOtherNetwork(_ NameOfTV: Swift.String) 420 | } 421 | @_inheritsConvenienceInitializers @_hasMissingDesignatedInitializers @objc open class ServiceSearch : ObjectiveC.NSObject { 422 | @objc weak open var delegate: SmartView.ServiceSearchDelegate? 423 | @objc open var isSearching: Swift.Bool { 424 | @objc get 425 | } 426 | @objc open func getServices() -> [SmartView.Service] 427 | @objc open func on(_ notificationName: Swift.String, performClosure: @escaping (Foundation.Notification?) -> Swift.Void) -> Swift.AnyObject 428 | @objc open func off(_ observer: Swift.AnyObject) 429 | @objc open func start() 430 | @objc open func start(_ showStandByTv: Swift.Bool = true) 431 | @objc open func isSearchingBLE() -> Swift.Bool 432 | @objc open func startUsingBLE() -> Swift.Bool 433 | @objc open func stopUsingBLE() -> Swift.Bool 434 | @objc open func stop() 435 | @objc open func getStandByMode() -> Swift.Bool 436 | @objc deinit 437 | } 438 | extension SmartView.ServiceSearch { 439 | @objc dynamic open func clearStandbyDevices() 440 | } 441 | public class SSLCert { 442 | public init(data: Foundation.Data) 443 | public init(key: Security.SecKey) 444 | @objc deinit 445 | } 446 | public class SSLSecurity { 447 | public var validatedDN: Swift.Bool 448 | convenience public init(usePublicKeys: Swift.Bool = false) 449 | public init(certs: [SmartView.SSLCert], usePublicKeys: Swift.Bool) 450 | public func isValid(_ trust: Security.SecTrust, domain: Swift.String?) -> Swift.Bool 451 | @objc deinit 452 | } 453 | public typealias GetServiceCompletionHandler = (_ service: SmartView.Service?, _ error: Foundation.NSError?) -> Swift.Void 454 | @_hasMissingDesignatedInitializers @objc open class Service : ObjectiveC.NSObject { 455 | open var discoveryType: SmartView.ServiceSearchDiscoveryType { 456 | get 457 | } 458 | open var id: Swift.String { 459 | get 460 | } 461 | open var uri: Swift.String { 462 | get 463 | } 464 | open var name: Swift.String { 465 | get 466 | } 467 | open var version: Swift.String { 468 | get 469 | } 470 | open var type: Swift.String { 471 | get 472 | } 473 | @objc override dynamic open var description: Swift.String { 474 | @objc get 475 | } 476 | open func getDeviceInfo(_ timeout: Swift.Int, completionHandler: @escaping (_ deviceInfo: [Swift.String : Swift.AnyObject]?, _ error: Foundation.NSError?) -> Swift.Void) 477 | open func createApplication(_ id: Swift.AnyObject, channelURI: Swift.String, args: [Swift.String : Swift.AnyObject]?) -> SmartView.Application? 478 | open func createChannel(_ channelURI: Swift.String) -> SmartView.Channel 479 | open func createVideoPlayer(_ appName: Swift.String) -> SmartView.VideoPlayer 480 | open func createAudioPlayer(_ appName: Swift.String) -> SmartView.AudioPlayer 481 | open func createPhotoPlayer(_ appName: Swift.String) -> SmartView.PhotoPlayer 482 | @objc open class func search() -> SmartView.ServiceSearch 483 | open class func getByURI(_ uri: Swift.String, timeout: Foundation.TimeInterval, completionHandler: @escaping (_ service: SmartView.Service?, _ error: Foundation.NSError?) -> Swift.Void) 484 | open class func getById(_ id: Swift.String, completionHandler: @escaping (_ service: SmartView.Service?, _ error: Foundation.NSError?) -> Swift.Void) 485 | open class func WakeOnWirelessLan(_ macAddr: Swift.String) 486 | open class func WakeOnWirelessAndConnect(_ macAddr: Swift.String, uri: Swift.String, completionHandler: @escaping (_ service: SmartView.Service?, _ error: Foundation.NSError?) -> Swift.Void) 487 | open class func WakeOnWirelessAndConnect(_ macAddr: Swift.String, uri: Swift.String, timeOut: Foundation.TimeInterval, completionHandler: @escaping (_ service: SmartView.Service?, _ error: Foundation.NSError?) -> Swift.Void) 488 | open func isSecurityModeSupported(completionHandler: @escaping (_ isSupport: Swift.Bool, _ error: Foundation.NSError?) -> Swift.Void) 489 | open func remove() 490 | open func isDMPSupported() -> Swift.Bool 491 | @objc deinit 492 | } 493 | public func == (lhs: SmartView.Service, rhs: SmartView.Service) -> Swift.Bool 494 | @_hasMissingDesignatedInitializers @objc open class Application : SmartView.Channel { 495 | open var id: Swift.String! { 496 | get 497 | } 498 | open var args: [Swift.String : Swift.AnyObject]? { 499 | get 500 | } 501 | public static let BUNDLE_IDENTIFIER: Swift.String 502 | public static let PROPERTY_VALUE_LIBRARY: Swift.String 503 | @objc deinit 504 | open func getInfo(_ completionHandler: @escaping (_ info: [Swift.String : Swift.AnyObject]?, _ error: Foundation.NSError?) -> Swift.Void) 505 | open func start(_ completionHandler: ((_ success: Swift.Bool, _ error: Foundation.NSError?) -> Swift.Void)?) 506 | open func stop(_ completionHandler: ((_ success: Swift.Bool, _ error: Foundation.NSError?) -> Swift.Void)?) 507 | open func install(_ completionHandler: ((_ success: Swift.Bool, _ error: Foundation.NSError?) -> Swift.Void)?) 508 | override open func connect(_ attributes: [Swift.String : Swift.String]?, completionHandler: ((_ client: SmartView.ChannelClient?, _ error: Foundation.NSError?) -> Swift.Void)?) 509 | open func disconnect(leaveHostRunning: Swift.Bool, completionHandler: ((_ client: SmartView.ChannelClient?, _ error: Foundation.NSError?) -> Swift.Void)?) 510 | open func disconnect(leaveHostRunning: Swift.Bool) 511 | override open func disconnect(_ completionHandler: ((_ client: SmartView.ChannelClient?, _ error: Foundation.NSError?) -> Swift.Void)?) 512 | } 513 | extension SmartView.WebSocket.CloseCode : Swift.Equatable {} 514 | extension SmartView.WebSocket.CloseCode : Swift.Hashable {} 515 | extension SmartView.WebSocket.CloseCode : Swift.RawRepresentable {} 516 | extension SmartView.AudioPlayer.AudioRepeatMode : Swift.Equatable {} 517 | extension SmartView.AudioPlayer.AudioRepeatMode : Swift.Hashable {} 518 | extension SmartView.AudioPlayer.AudioRepeatMode : Swift.RawRepresentable {} 519 | extension SmartView.MessageTarget : Swift.Equatable {} 520 | extension SmartView.MessageTarget : Swift.Hashable {} 521 | extension SmartView.MessageTarget : Swift.RawRepresentable {} 522 | extension SmartView.ChannelEvent : Swift.Equatable {} 523 | extension SmartView.ChannelEvent : Swift.Hashable {} 524 | extension SmartView.ChannelEvent : Swift.RawRepresentable {} 525 | extension SmartView.VideoPlayer.VideoRepeatMode : Swift.Equatable {} 526 | extension SmartView.VideoPlayer.VideoRepeatMode : Swift.Hashable {} 527 | extension SmartView.VideoPlayer.VideoRepeatMode : Swift.RawRepresentable {} 528 | extension SmartView.ServiceSearchDiscoveryType : Swift.Equatable {} 529 | extension SmartView.ServiceSearchDiscoveryType : Swift.Hashable {} 530 | extension SmartView.ServiceSearchDiscoveryType : Swift.RawRepresentable {} 531 | --------------------------------------------------------------------------------