WebOSClient 是一个 Swift 库,旨在方便与运行 WebOS 的智能电视(如 LG 电视)进行通信。它提供了一个方便的接口来连接到电视、发送命令和管理各种与电视相关的功能。
要使用此软件包,请确保客户端设备和电视都连接到同一 Wi-Fi 网络。
您需要手动输入电视的 IP 地址才能使此软件包正常运行。要自动发现局域网中的设备,请考虑使用 SSDPClient package 软件包或类似的工具。
要将 WebOSClient 作为依赖项添加,请将其包含在 Package.swift 的 dependencies 值中
dependencies: [
.package(url: "https://github.com/jareksedy/WebOSClient.git"))
]
要使用 CocoaPods 将 WebOSClient 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它
pod 'WebOSClient'
getPowerState(subscribe: Bool? = nil)
方法以启用电源状态订阅。有关完整的版本历史记录,请参阅 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.
}
}
}
}
这些命令涵盖了基本功能,例如调整音量、检索当前音量级别、静音或取消静音、关闭和打开电视屏幕等。
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,并引入了一组不同的功能,专门用于模拟 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 内部被电视应用程序使用 (你可以在这里找到更多信息: 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 上的核心功能的演示。
欢迎通过提交问题或拉取请求来为此库做出贡献。 您的反馈和贡献将不胜感激!
此库已获得 MIT 许可证的许可。 有关详细信息,请参见 LICENSE 文件。