A device with no hardware HEVC decoder could still be handed an HEVC transcode: the device profile advertised a fixed codec list that assumed every device decodes everything. Prepending AV1 to reach the AV1 encoders issue #2131 asks for would have made that worse - an Apple TV 4K and every iPhone before the A17 Pro have no AV1 decoder at all. Probe the platform instead. Android enumerates MediaCodecList for a hardware decoder and iOS/tvOS ask VideoToolbox, both feeding one latched VideoDecodeCapabilities that the device profile reads when it builds its codec lists. Desktop deliberately answers nothing: a pre-Kaby-Lake Mac has no hardware HEVC decoder and an M1 no AV1 one, yet both software-decode in real time, so narrowing there would force transcodes for nothing. An unanswered or failed probe advertises everything, so the list never narrows on missing data. The transcode target becomes av1,hevc,h264 filtered by the probe. Leading with AV1 is safe because the server rotates codecs its admin has not enabled ("Allow encoding in HEVC/AV1 format", both off by default) to the back before picking one, so it costs nothing on a server that will not emit AV1. Audio now accepts everything the path can carry. The direct-play profile drops its AudioCodec list entirely - an omitted list means "any codec" to Jellyfin - so an audio stream can no longer be what blocks direct play. The transcode target lists every codec Jellyfin can put in an fMP4 segment, so a video-only transcode copies DTS or TrueHD instead of re-encoding it. Two limits bound that string: the server validates it against ^[a-zA-Z0-9\-\._,|]{0,40}$ when it echoes the list into the transcode URL, so alac does not fit and * is not a wildcard; and omitting the key is not "accept everything" here the way it is for direct play, because the server substitutes the source codec and then ships no audio at all for a source fMP4 cannot carry. close #2131
194 lines
6.1 KiB
Swift
194 lines
6.1 KiB
Swift
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<UIPress>, with event: UIPressesEvent?) {
|
|
if handlePlayPausePress(presses) {
|
|
return
|
|
}
|
|
|
|
super.pressesBegan(presses, with: event)
|
|
}
|
|
|
|
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
|
if containsPlayPausePress(presses) {
|
|
return
|
|
}
|
|
|
|
super.pressesEnded(presses, with: event)
|
|
}
|
|
|
|
override func pressesCancelled(_ presses: Set<UIPress>, 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<UIPress>) -> Bool {
|
|
guard containsPlayPausePress(presses) else { return false }
|
|
|
|
sendPlayPauseEvent(source: "presses", detail: "playPause")
|
|
return true
|
|
}
|
|
|
|
private func containsPlayPausePress(_ presses: Set<UIPress>) -> 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 {
|
|
// Set the long-form profile before activation so Dolby capabilities are
|
|
// available before playback.
|
|
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)
|
|
}
|
|
if let r = self.registrar(forPlugin: "VideoDecodeCapabilitiesPlugin") {
|
|
VideoDecodeCapabilitiesPlugin.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)
|
|
}
|
|
}
|