WebOSClient

Swift Version SPM Compatible Cocoapods Compatible License

WebOSClient 是一个 Swift 库,旨在方便与运行 WebOS 的智能电视(如 LG 电视)进行通信。它提供了一个方便的接口来连接到电视、发送命令和管理各种与电视相关的功能。

要使用此软件包,请确保客户端设备和电视都连接到同一 Wi-Fi 网络。

手动 IP 地址输入

您需要手动输入电视的 IP 地址才能使此软件包正常运行。要自动发现局域网中的设备,请考虑使用 SSDPClient package 软件包或类似的工具。

功能

要求

安装

Swift Package Manager

要将 WebOSClient 作为依赖项添加,请将其包含在 Package.swift 的 dependencies 值中

dependencies: [
    .package(url: "https://github.com/jareksedy/WebOSClient.git"))
]

CocoaPods

要使用 CocoaPods 将 WebOSClient 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它

pod 'WebOSClient'

版本历史

1.5.1 - 电源状态订阅

功能

有关完整的版本历史记录,请参阅 CHANGELOG.md

用法

基本设置

下面是一个基本示例,演示了 WebOSClient 的设置和与电视的连接。

import UIKit
import WebOSClient

// MARK: - Constants
fileprivate enum Constants {
    static let registrationTokenKey = "clientKey"
}

// MARK: - ViewController
class ViewController: UIViewController {
    // The URL of the WebOS service on the TV.
    let url = URL(string: "wss://192.168.1.10:3001")!

    // The client responsible for communication with the WebOS service.
    var client: WebOSClientProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Instantiate WebOSClient with the specified URL and set the current view controller as the delegate.
        // Enable activity logging by setting shouldLogActivity to true.
        client = WebOSClient(url: url, delegate: self, shouldLogActivity: true)

        // Establish a connection to the TV.
        client?.connect()

        // Retrieve the registration token from UserDefaults.
        let registrationToken = UserDefaults.standard.string(forKey: Constants.registrationTokenKey)

        // Send a registration request to the TV with the stored or nil registration token.
        // The PairingType option should be set to .pin for PIN-based pairing. The default value is .prompt.
        client?.send(.register(pairingType: .pin, clientKey: registrationToken))
    }
}

// MARK: - WebOSClientDelegate
extension ViewController: WebOSClientDelegate {
    // Callback triggered upon displaying the PIN to the user.
    func didDisplayPin() {
        // Send the correct PIN displayed on the TV screen to the TV here.
        client?.send(.setPin("12345678"))
    }

    // Callback triggered upon successful registration with the TV.
    func didRegister(with clientKey: String) {
        // Store the received registration token in UserDefaults for future use.
        UserDefaults.standard.setValue(clientKey, forKey: Constants.registrationTokenKey)

        // Additional commands can be sent after successfull registration.
        client?.send(.volumeUp)
        client?.sendKey(.home)
    }


    // Callback triggered upon receiving a network error.
    func didReceiveNetworkError(_ error: Error?) {
        if let error = error as NSError? {

            // Print details of the received network error.
            print("Received network error. Code: \(error.code). Reconnect suggested.")

            // Attempt to reconnect and re-register with the TV.
            client?.connect()
            let registrationToken = UserDefaults.standard.string(forKey: Constants.registrationTokenKey)
            client?.send(.register(clientKey: registrationToken))
        }
    }
}

客户端方法

这些是 WebOSClient 的核心方法,允许与电视连接并发送按键和各种命令。

public protocol WebOSClientProtocol {
    /// Establishes a connection to the TV.
    func connect()

    /// Sends a specified request to the TV and returns the unique identifier of the request.
    /// - Parameters:
    ///   - target: Type of request and it's parameters if any.
    ///   - id: The unique identifier of the request (can be omitted).
    /// - Returns: The identifier of sent request, or nil if the request couldn't be sent.
    @discardableResult func send(_ target: WebOSTarget, id: String) -> String?

    /// Sends a key press event to the service using the specified WebOSKeyTarget.
    /// - Parameter key: The target key to be pressed.
    func sendKey(_ key: WebOSKeyTarget)

    /// Disconnects the WebOS client from the WebOS service.
    func disconnect()
}

代理方法

这些是用于处理各种 WebOSClient 事件的方法。

public protocol WebOSClientDelegate: AnyObject {
    /// Invoked when the client successfully establishes a connection.
    func didConnect()

    /// Invoked when the TV displays a PIN code for pairing.
    func didDisplayPin()

    /// Invoked when the TV prompts for registration.
    func didPrompt()

    /// Invoked when the client successfully registers with a client key.
    /// - Parameter clientKey: The client key for registration.
    func didRegister(with clientKey: String)

    /// Invoked when the client receives a response from the WebOS service.
    /// - Parameter result: The result containing either a WebOSResponse or an error.
    func didReceive(_ result: Result<WebOSResponse, Error>)

    /// Invoked when the client encounters a network-related error, i.e. abnormal disconnect.
    /// - Parameter error: The error object representing the network error, if any.
    func didReceiveNetworkError(_ error: Error?)

    /// Invoked when the client disconnects from the WebOS websocket service.
    func didDisconnect()
}

处理配对错误

通过通知用户并提供重试或解决连接问题的选项,优雅地处理配对错误。在 didReceive 代理方法中捕获配对错误。

// MARK: - WebOSClientDelegate
extension ViewController: WebOSClientDelegate {
    func didReceive(_ result: Result<WebOSResponse, Error>) {
        if case .failure(let error) = result {
            let errorMessage = error.localizedDescription

            if errorMessage.contains("rejected pairing") {
            // Pairing rejected by the user or invalid pin.
            }

            if errorMessage.contains("cancelled") {
            // Pairing cancelled due to a timeout.
            }
        }
    }
}

常用 API 命令

这些命令涵盖了基本功能,例如调整音量、检索当前音量级别、静音或取消静音、关闭和打开电视屏幕等。

client?.send(.setPin("12345678"))                                           // Sets the PIN for pairing.
client?.send(.volumeUp)                                                     // Increases the volume by 1 unit.
client?.send(.volumeDown)                                                   // Decreases the volume by 1 unit.
client?.send(.getVolume(subscribe: true))                                   // Retrieves the current volume level with optional subscription.
client?.send(.setVolume(25))                                                // Sets the volume to the specified level.
client?.send(.setMute(true))                                                // Mutes or unmutes the audio.
client?.send(.play)                                                         // Initiates playback.
client?.send(.pause)                                                        // Pauses the current media playback.
client?.send(.stop)                                                         // Stops the current media playback.
client?.send(.rewind)                                                       // Rewinds the current media playback.
client?.send(.fastForward)                                                  // Fast-forwards the current media playback.
client?.send(.getSoundOutput(subscribe: true))                              // Retrieves the current sound output with optional subscription.
client?.send(.changeSoundOutput(.soundbar))                                 // Changes the sound output to the specified type.
client?.send(.toast(message: "Hello, world!"))                              // Shows a message on the screen.
client?.send(.getPowerState(subscribe: true))                               // Retrieves the TV power state (on/off) with optional subscription.
client?.send(.screenOff)                                                    // Turns off the TV screen.
client?.send(.screenOn)                                                     // Turns on the TV screen.
client?.send(.systemInfo)                                                   // Retrieves system information.
client?.send(.turnOff)                                                      // Turns off the TV.
client?.send(.listApps)                                                     // Retrieves a list of installed apps.
client?.send(.getForegroundApp(subscribe: true))                            // Retrieves the foreground app with optional subscription.
client?.send(.getForegroundAppMediaStatus(subscribe: true))                 // Retrieves the foreground app with media status with optional subscription.
client?.send(.getPictureSettings(subscribe: true))                          // Retrieves the picture setting (color, brightness, backlight, contrast). Only tested on > 2022 models.
client?.send(.getSoundMode(subscribe: true))                                // Retrieves the sound mode. Available sound modes (not all are available on all TVs): aiSoundPlus, standard, movie, news, sports, music, game.
client?.send(.launchApp(appId: "netflix"))                                  // Launches an app with the specified ID, content ID, and parameters (optional).
client?.send(.closeApp(appId: "netflix"))                                   // Closes the app with the specified ID.
client?.send(.insertText(text: "text_to_insert", replace: Bool = true))     // Inserts text in the text input field (keyboard must be open). If 'replace' is true, replaces any existing text in field.
client?.send(.sendEnterKey)                                                 // Sends an enter key press to the TV.
client?.send(.deleteCharacters(count: 1))                                   // Deletes a specified number of characters from the text input (keyboard must be open).
client?.send(.registerRemoteKeyboard)                                       // Subscribes to current text field changes.
client?.send(.channelUp)                                                    // Increases the TV channel.
client?.send(.channelDown)                                                  // Decreases the TV channel.
client?.send(.listSources)                                                  // Retrieves a list of available input sources.
client?.send(.setSource("HDMI2"))                                           // Sets the TV source to the specified input ID.

订阅

以下命令允许您持续监视电视状态的变化,并在您的应用程序中做出相应的反应。

client?.send(.getPowerState(subscribe: true))                               // Retrieves the TV power state (on/off) with optional subscription.
client?.send(.getForegroundApp(subscribe: true))                            // Retrieves the foreground app with optional subscription.
client?.send(.getForegroundAppMediaStatus(subscribe: true))                 // Retrieves the foreground app with media status with optional subscription.
client?.send(.getVolume(subscribe: true))                                   // Retrieves the current volume level with optional subscription.
client?.send(.getSoundOutput(subscribe: true))                              // Retrieves the current sound output with optional subscription.

如果 subscribe 标志设置为 true,则客户端订阅持续更新。如果设置为 false,则取消订阅更新。如果 subscribe 为 nil,则客户端将检索当前状态一次,而不进行订阅。

// Subscribe to volume changes.
var volumeSubscriptionId: String = ""
volumeSubscriptionId = client?.send(.getVolume(subscribe: true))

didReceive 代理方法中接收状态更改。

// MARK: - WebOSClientDelegate
extension ViewController: WebOSClientDelegate {
    func didReceive(_ result: Result<WebOSResponse, Error>) {
            if case .success(let response) = result, response.id == volumeSubscriptionId {
                dump(response)
            }
        }
    }

按键 API 命令

这些命令使用略有不同的 API,并引入了一组不同的功能,专门用于模拟 LG 电视上的遥控器按键。

client?.sendKey(.move(dx: 10, dy: 10))              // Simulates moving the mouse pointer on the screen.
client?.sendKey(.click)                             // Simulates mouse click action.
client?.sendKey(.scroll(dx: 0, dy: 100)             // Simulates scrolling on the screen.
client?.sendKey(.left)                              // Simulates a left arrow key press.
client?.sendKey(.right)                             // Simulates a right arrow key press.
client?.sendKey(.up)                                // Simulates an up arrow key press.
client?.sendKey(.down)                              // Simulates a down arrow key press.
client?.sendKey(.home)                              // Simulates a home button press.
client?.sendKey(.back)                              // Simulates a back button press.
client?.sendKey(.menu)                              // Simulates a menu button press.
client?.sendKey(.enter)                             // Simulates OK button press.
client?.sendKey(.dash)                              // Simulates a dash button press.
client?.sendKey(.info)                              // Simulates an info button press.
client?.sendKey(.num0)                              // Simulates pressing the number 0—9 key.
client?.sendKey(.asterisk)                          // Simulates pressing the asterisk key.
client?.sendKey(.cc)                                // Simulates pressing the closed caption (CC) key.
client?.sendKey(.exit)                              // Simulates an exit button press.
client?.sendKey(.mute)                              // Simulates a mute button press.
client?.sendKey(.red)                               // Simulates pressing the red color button.
client?.sendKey(.green)                             // Simulates pressing the green color button.
client?.sendKey(.yellow)                            // Simulates pressing the yellow color button.
client?.sendKey(.blue)                              // Simulates pressing the blue color button.
client?.sendKey(.volumeUp)                          // Simulates pressing the volume up button.
client?.sendKey(.volumeDown)                        // Simulates pressing the volume down button.
client?.sendKey(.channelUp)                         // Simulates pressing the channel up button.
client?.sendKey(.channelDown)                       // Simulates pressing the channel down button.
client?.sendKey(.play)                              // Simulates a play button press.
client?.sendKey(.pause)                             // Simulates a pause button press.
client?.sendKey(.stop)                              // Simulates a stop button press.
client?.sendKey(.rewind)                            // Simulates a rewind button press.
client?.sendKey(.fastForward)                       // Simulates a fast-forward button press.

LUNA API 命令

这是一个棘手且间接的方法与 LUNA API 通信,LUNA API 内部被电视应用程序使用 (你可以在这里找到更多信息: https://webostv.developer.lge.com/develop/references/luna-service-introduction)。 基本上,LUNA 请求有效负载嵌入到 Alert 请求中,该请求将发送到电视,当 Alert 弹出时,LUNA 请求有效负载将被触发。 因此,这取决于你的用例,无论你是想让用户手动点击屏幕上的“确定”,还是在 Alert 成功显示后自动发送按键“确定”。

client?.sendLuna(.setPictureSettings(brightness: 100, contrast: 100, color: 100, backlight: 100))              // Set picture settings
client?.sendLuna(.setPictureMode(mode: "cinema"))                              /*
    Set Available picture modes (not all are available on all TVs):
    cinema, eco, expert1, expert2, game, normal, photo, sports, technicolor,
    vivid, hdrEffect,  hdrCinema, hdrCinemaBright, hdrExternal, hdrGame,
    hdrStandard, hdrTechnicolor, hdrVivid, dolbyHdrCinema,dolbyHdrCinemaBright,
    dolbyHdrDarkAmazon, dolbyHdrGame, dolbyHdrStandard, dolbyHdrVivid, dolbyStandard
    */
client?.sendLuna(.setSoundMode(value: "sports"))             /*
    Set Available sound modes (not all are available on all TVs):
    aiSoundPlus, standard, movie, news, sports, music, game
    */

以下是指南,说明如何在显示 Alert 后自动发送按键“确定”

// MARK: - WebOSClientDelegate
extension ViewController: WebOSClientDelegate {
    func didReceive(jsonResponse: String) {
        if let jsonObject = try? JSONSerialization.jsonObject(with: Data(jsonResponse.utf8), options: []) as? [String: Any] {
            // Handle Luna
            if let id = jsonObject["id"] as? String {
                if let _ = ButtonGeometry.allCases.first(where: { $0.rawValue == id }) {
                    // Enter
                    client.sendKey(.enter)
                }
            }
        }
    }
}

文档

文档也包含在源代码中。 有关更多信息,请查看各个文件中的注释。 有关更多详细信息,请参阅此库随附的示例项目。

示例应用

此软件包中包含的配套示例应用程序展示了该库的基本功能,并用作其在 macOS 上的核心功能的演示。

Example App Screenshot

贡献

欢迎通过提交问题或拉取请求来为此库做出贡献。 您的反馈和贡献将不胜感激!

许可证

此库已获得 MIT 许可证的许可。 有关详细信息,请参见 LICENSE 文件。