Re-enables Audio Passthrough on Apple TV against the MPVKit EAC3-JOC AVPlayer sink (pin bump to v1.0.8 follows), adds an AVPlayer test harness + diagnostics screen for #1300, and makes loudnorm yield while passthrough is active (fixes a latent spdif conflict on desktop too).
158 lines
5.5 KiB
Swift
158 lines
5.5 KiB
Swift
import Flutter
|
|
import UIKit
|
|
import AVFoundation
|
|
import MediaPlayer
|
|
|
|
@main
|
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
|
private var deviceAdjustmentChannel: FlutterMethodChannel?
|
|
private var originalBrightness: CGFloat?
|
|
private var volumeView: MPVolumeView?
|
|
private weak var volumeSlider: UISlider?
|
|
|
|
override func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
) -> Bool {
|
|
// Configure audio session for media playback
|
|
do {
|
|
let session = AVAudioSession.sharedInstance()
|
|
try session.setCategory(.playback, mode: .default)
|
|
try session.setActive(true)
|
|
} catch {
|
|
print("Failed to configure audio session: \(error)")
|
|
}
|
|
|
|
application.beginReceivingRemoteControlEvents()
|
|
|
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
}
|
|
|
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
|
|
|
// Register MPV player plugin
|
|
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "MpvPlayerPlugin") {
|
|
MpvPlayerPlugin.register(with: registrar)
|
|
}
|
|
|
|
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "AtmosProbePlugin") {
|
|
AtmosProbePlugin.register(with: registrar)
|
|
}
|
|
|
|
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "DeviceAdjustmentChannel") {
|
|
registerDeviceAdjustmentChannel(messenger: registrar.messenger())
|
|
}
|
|
}
|
|
|
|
private func registerDeviceAdjustmentChannel(messenger: FlutterBinaryMessenger) {
|
|
let channel = FlutterMethodChannel(name: "com.plezy/device_adjustment", binaryMessenger: messenger)
|
|
channel.setMethodCallHandler { [weak self] call, result in
|
|
DispatchQueue.main.async {
|
|
self?.handleDeviceAdjustmentCall(call, result: result)
|
|
}
|
|
}
|
|
deviceAdjustmentChannel = channel
|
|
}
|
|
|
|
private func handleDeviceAdjustmentCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "getBrightness":
|
|
result(Double(UIScreen.main.brightness))
|
|
case "setBrightness":
|
|
guard let value = normalizedArgument(call.arguments, result: result) else { return }
|
|
if originalBrightness == nil {
|
|
originalBrightness = UIScreen.main.brightness
|
|
}
|
|
UIScreen.main.brightness = CGFloat(value)
|
|
result(nil)
|
|
case "restoreBrightness":
|
|
if let originalBrightness = originalBrightness {
|
|
UIScreen.main.brightness = originalBrightness
|
|
self.originalBrightness = nil
|
|
}
|
|
result(nil)
|
|
case "getMediaVolume":
|
|
result(Double(AVAudioSession.sharedInstance().outputVolume))
|
|
case "setMediaVolume":
|
|
guard let value = normalizedArgument(call.arguments, result: result) else { return }
|
|
if setMediaVolume(value) {
|
|
result(nil)
|
|
} else {
|
|
result(
|
|
FlutterError(code: "DEVICE_ADJUSTMENT_UNAVAILABLE", message: "System volume slider unavailable", details: nil)
|
|
)
|
|
}
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
private func normalizedArgument(_ arguments: Any?, result: @escaping FlutterResult) -> Double? {
|
|
guard let value = arguments as? NSNumber else {
|
|
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a numeric value", details: nil))
|
|
return nil
|
|
}
|
|
let doubleValue = value.doubleValue
|
|
guard doubleValue.isFinite else {
|
|
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a finite numeric value", details: nil))
|
|
return nil
|
|
}
|
|
return min(1.0, max(0.0, doubleValue))
|
|
}
|
|
|
|
private func setMediaVolume(_ value: Double) -> Bool {
|
|
guard let slider = ensureVolumeSlider() else { return false }
|
|
slider.setValue(Float(value), animated: false)
|
|
slider.sendActions(for: .valueChanged)
|
|
slider.sendActions(for: .touchUpInside)
|
|
return true
|
|
}
|
|
|
|
private func ensureVolumeSlider() -> UISlider? {
|
|
if let volumeSlider = volumeSlider { return volumeSlider }
|
|
|
|
let volumeView = self.volumeView ?? MPVolumeView(frame: CGRect(x: -1000, y: -1000, width: 1, height: 1))
|
|
volumeView.showsRouteButton = false
|
|
volumeView.showsVolumeSlider = true
|
|
volumeView.alpha = 0.01
|
|
volumeView.isUserInteractionEnabled = false
|
|
|
|
if volumeView.superview == nil {
|
|
activeWindow?.addSubview(volumeView)
|
|
}
|
|
volumeView.layoutIfNeeded()
|
|
|
|
self.volumeView = volumeView
|
|
let slider = findVolumeSlider(in: volumeView)
|
|
volumeSlider = slider
|
|
return slider
|
|
}
|
|
|
|
private func findVolumeSlider(in view: UIView) -> UISlider? {
|
|
if let slider = view as? UISlider { return slider }
|
|
for subview in view.subviews {
|
|
if let slider = findVolumeSlider(in: subview) { return slider }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var activeWindow: UIWindow? {
|
|
if #available(iOS 13.0, *) {
|
|
for scene in UIApplication.shared.connectedScenes {
|
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
|
if let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
|
return keyWindow
|
|
}
|
|
}
|
|
for scene in UIApplication.shared.connectedScenes {
|
|
guard let windowScene = scene as? UIWindowScene, let window = windowScene.windows.first else { continue }
|
|
return window
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
|
|
}
|
|
}
|