├── .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 |
--------------------------------------------------------------------------------