import Flutter import UIKit import AVFoundation import universal_gamepad import os_media_controls import wakelock_plus @objc class PlezyFlutterViewController: FlutterViewController { private lazy var tvRemoteChannel = FlutterBasicMessageChannel( name: "flutter/gamepadtouchevent", binaryMessenger: binaryMessenger, codec: FlutterJSONMessageCodec.sharedInstance() ) override var canBecomeFirstResponder: Bool { true } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) becomeFirstResponder() } override func viewWillDisappear(_ animated: Bool) { resignFirstResponder() super.viewWillDisappear(animated) } override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { if handlePlayPausePress(presses) { return } super.pressesBegan(presses, with: event) } override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { if containsPlayPausePress(presses) { return } super.pressesEnded(presses, with: event) } override func pressesCancelled(_ presses: Set, with event: UIPressesEvent?) { if containsPlayPausePress(presses) { return } super.pressesCancelled(presses, with: event) } override func remoteControlReceived(with event: UIEvent?) { guard let event = event else { super.remoteControlReceived(with: event) return } let subtype = event.subtype print("PlezyTvRemote: remote control event subtype=\(remoteControlSubtypeName(subtype))") switch subtype { case .remoteControlPlay, .remoteControlPause, .remoteControlTogglePlayPause: sendPlayPauseEvent(source: "remote_control", detail: remoteControlSubtypeName(subtype)) default: super.remoteControlReceived(with: event) } } private func handlePlayPausePress(_ presses: Set) -> Bool { guard containsPlayPausePress(presses) else { return false } sendPlayPauseEvent(source: "presses", detail: "playPause") return true } private func containsPlayPausePress(_ presses: Set) -> Bool { presses.contains { press in press.type == .playPause } } private func sendPlayPauseEvent(source: String, detail: String) { print("PlezyTvRemote: intercepted play/pause source=\(source) detail=\(detail)") tvRemoteChannel.sendMessage(["type": "play_pause", "source": source, "detail": detail]) } private func remoteControlSubtypeName(_ subtype: UIEvent.EventSubtype) -> String { switch subtype { case .remoteControlPlay: return "remoteControlPlay" case .remoteControlPause: return "remoteControlPause" case .remoteControlTogglePlayPause: return "remoteControlTogglePlayPause" case .remoteControlStop: return "remoteControlStop" case .remoteControlNextTrack: return "remoteControlNextTrack" case .remoteControlPreviousTrack: return "remoteControlPreviousTrack" case .remoteControlBeginSeekingForward: return "remoteControlBeginSeekingForward" case .remoteControlEndSeekingForward: return "remoteControlEndSeekingForward" case .remoteControlBeginSeekingBackward: return "remoteControlBeginSeekingBackward" case .remoteControlEndSeekingBackward: return "remoteControlEndSeekingBackward" default: return "unknown(\(subtype.rawValue))" } } } @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Dolby's sequence diagram prescribes exactly this at app launch: the // long-form playback profile, then activation, so the session is eligible // for the system's Dolby decode/render path and its rendering capabilities // can be read before any content is chosen. The mpv AVFoundation audio // output reconfigures and re-activates the same shared session at playback // start; this establishes the launch-time state the guide expects. do { let session = AVAudioSession.sharedInstance() try session.setCategory( .playback, mode: .default, policy: .longFormAudio, options: []) try session.setActive(true) } catch { print("Failed to configure long-form audio session: \(error)") do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) } catch { print("Failed to configure audio session: \(error)") } } application.beginReceivingRemoteControlEvents() if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL { _ = SystemShelfPlugin.handleOpenURL(url) } if let r = self.registrar(forPlugin: "SharedPreferencesPlugin") { SharedPreferencesPlugin.register(with: r) } if let r = self.registrar(forPlugin: "MpvPlayerPlugin") { MpvPlayerPlugin.register(with: r) } if let r = self.registrar(forPlugin: "MpvAudioPlayerPlugin") { MpvAudioPlayerPlugin.register(with: r) } if let r = self.registrar(forPlugin: "PackageInfoPlusPlugin") { PackageInfoPlusPlugin.register(with: r) } if let r = self.registrar(forPlugin: "PathProviderPlugin") { PathProviderPlugin.register(with: r) } if let r = self.registrar(forPlugin: "GamepadPlugin") { GamepadPlugin.register(with: r) } if let r = self.registrar(forPlugin: "DeviceInfoPlusPlugin") { DeviceInfoPlusPlugin.register(with: r) } if let r = self.registrar(forPlugin: "ConnectivityPlusPlugin") { ConnectivityPlusPlugin.register(with: r) } if let r = self.registrar(forPlugin: "OsMediaControlsPlugin") { OsMediaControlsPlugin.register(with: r) } if let r = self.registrar(forPlugin: "WakelockPlusPlugin") { WakelockPlusPlugin.register(with: r) } if let r = self.registrar(forPlugin: "SystemShelfPlugin") { SystemShelfPlugin.register(with: r) } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application( _ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { if SystemShelfPlugin.handleOpenURL(url) { return true } return super.application(application, open: url, options: options) } }