refactor(tvos): cleanup

This commit is contained in:
edde746
2026-04-24 20:21:30 +02:00
parent 49a86e402c
commit 1de91a598c
15 changed files with 191 additions and 1036 deletions
+4 -4
View File
@@ -743,8 +743,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: b4006c601e32a6a1fac9e6f26e6ee9923c7b03c9
resolved-ref: b4006c601e32a6a1fac9e6f26e6ee9923c7b03c9
ref: edfe336
resolved-ref: edfe33636bc82c0649c808117651b5c6844cce80
url: "https://github.com/edde746/os-media-controls"
source: git
version: "0.2.1"
@@ -1287,10 +1287,10 @@ packages:
dependency: "direct main"
description:
name: universal_gamepad
sha256: "6177944707639a20dd4f89e7f8d78bf66b40ba03d70a1962754d3bba45e14ccd"
sha256: "3ada9b26e3b3471adc414fd50b8a27d2f890301d9d4cfd7a46732988a9f143d0"
url: "https://pub.dev"
source: hosted
version: "1.5.4"
version: "1.5.5"
url_launcher:
dependency: "direct main"
description:
+1 -1
View File
@@ -31,7 +31,7 @@ dependencies:
os_media_controls:
git:
url: https://github.com/edde746/os-media-controls
ref: b4006c601e32a6a1fac9e6f26e6ee9923c7b03c9
ref: edfe336
rate_limiter: ^1.0.0
wakelock_plus:
git:
+2
View File
@@ -31,3 +31,5 @@ Runner/GeneratedPluginRegistrant.*
!default.mode2v3
!default.pbxuser
!default.perspectivev3
# Flutter tool doesn't generate tvOS Flutter.podspec, so it's hand-maintained.
!Flutter/Flutter.podspec
+19
View File
@@ -0,0 +1,19 @@
#
# This podspec is NOT to be published. It is only used as a local source!
# This is a generated file; do not edit or check into version control.
#
Pod::Spec.new do |s|
s.name = 'Flutter'
s.version = '1.0.0'
s.summary = 'A UI toolkit for beautiful and fast apps.'
s.homepage = 'https://flutter.dev'
s.license = { :type => 'BSD' }
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.ios.deployment_target = '13.0'
s.tvos.deployment_target = '14.0'
# Framework linking is handled by Flutter tooling, not CocoaPods.
# Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs.
s.vendored_frameworks = 'path/to/nothing'
end
+55 -1
View File
@@ -7,6 +7,60 @@ project 'Runner', {
'Release' => :release,
}
# Flutter's tool doesn't generate Podfile logic for tvOS. Hand-wire pods for
# plugins whose upstream podspecs already declare tvOS — the rest remain
# compiled from tvos/Runner/Plugins/ via tvos/scripts/wire_plugins.rb.
require 'json'
tvos_pod_plugins = %w[universal_gamepad os_media_controls]
plugins_deps = File.expand_path('../.flutter-plugins-dependencies', __dir__)
unless File.exist?(plugins_deps)
raise 'Run `flutter pub get` in the project root before `pod install` on tvOS'
end
ios_plugins = JSON.parse(File.read(plugins_deps)).dig('plugins', 'ios') || []
plugin_paths = ios_plugins.each_with_object({}) { |p, h| h[p['name']] = p['path'] }
# Resolve the local tvOS engine root from tvos/Flutter/Generated.xcconfig so
# pod targets can find Flutter.framework at compile time.
def tvos_flutter_engine_path
xcconfig = File.expand_path('Flutter/Generated.xcconfig', __dir__)
raise "tvos/Podfile: missing #{xcconfig}" unless File.exist?(xcconfig)
File.foreach(xcconfig) do |line|
m = line.match(/^FLUTTER_LOCAL_ENGINE=(.+)/)
return m[1].strip if m
end
raise "tvos/Podfile: FLUTTER_LOCAL_ENGINE missing from Flutter/Generated.xcconfig"
end
target 'Runner' do
# No pods.
use_frameworks!
pod 'Flutter', :path => 'Flutter'
tvos_pod_plugins.each do |name|
base = plugin_paths[name] or
raise "tvos/Podfile: plugin '#{name}' not in .flutter-plugins-dependencies"
pod name, :path => File.join(base, 'ios')
end
end
post_install do |installer|
engine = tvos_flutter_engine_path
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
is_debug = config.name.downcase.include?('debug')
sim_variant = is_debug ? 'tvos_debug_sim_unopt_arm64' : 'tvos_release'
dev_variant = is_debug ? 'tvos_debug_unopt' : 'tvos_release'
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvsimulator*]'] = [
'$(inherited)',
File.join(engine, 'out', sim_variant),
]
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]'] = [
'$(inherited)',
File.join(engine, 'out', dev_variant),
]
# Engine Flutter.framework ships arm64-only for simulator; exclude x86_64
# so the link step doesn't look for a slice that doesn't exist.
config.build_settings['EXCLUDED_ARCHS[sdk=appletvsimulator*]'] = 'x86_64'
end
end
end
+87 -107
View File
@@ -7,32 +7,25 @@
objects = {
/* Begin PBXBuildFile section */
0D69E2DEA6EA0538722BC7F0 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */; };
12E72ED3EFA35A8715DE2A52 /* GamepadStreamHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
30E568153D56A59EA5EADCBB /* PathProviderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */; };
32F0056AF154FC4EA76CBEFC /* GamepadPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */; };
0A4E3554C5AD1373495F5434 /* messages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8E3486BF50440024D81B1E /* messages.g.swift */; };
24753D6DD9098E97989137A6 /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C51CAD792E5C69AA4F971A /* PackageInfoPlusPlugin.swift */; };
31477C4FBA7BF70CAFCDC566 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED539F08FAE8E8B964C7BE /* DeviceInfoPlusPlugin.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
477FB0F520224870902B9FD7 /* messages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FF71CD2416844A1FC485D77 /* messages.g.swift */; };
4A6DE7BDEF20A6242A3C6365 /* GCControllerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */; };
5689B077C6EA57D53BBE8D0D /* OsMediaControlsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */; };
4E9365FA67E94D8A289FE862 /* PathMonitorConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA12E2C6063C5799C38CAA95 /* PathMonitorConnectivityProvider.swift */; };
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */; };
5CDBEB7E3FE968FC131E964A /* ConnectivityPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */; };
691577F0EB3F4EB1A5160280 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */; };
6A0510802F9B91CB0090B5FC /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA0F4263E7B912702C490108 /* MPVKit */; };
6F3C0DD6F2F8DA14E7E2F386 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12B8610AE5D580077264851 /* MpvPlayerCore.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
8E5EED3DDAC9455D4DAA9776 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */; };
937279397427A0E8ECC70A3A /* PathProviderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217BA48DC884CB7ADD43CF88 /* PathProviderPlugin.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A7ED1D92E7B15D3F7562F93E /* SharedPreferencesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */; };
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2635E12EB9322B151EE5127 /* MpvPipController.swift */; };
B494441CBB8FB05D04F55DD9 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */; };
BF599A1C25382688DD7F4505 /* ButtonMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */; };
C1E0D7252DE079EDFA234F90 /* PathMonitorConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */; };
C330A1087AA135CCFC0C6203 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F435CB4590A55958615A7AD /* libPods-Runner.a */; };
F36688C4E8DF44BC6BDD99A5 /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */; };
AFCEB532D567638EF6707333 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9575190150A93F97270FC0 /* ConnectivityProvider.swift */; };
C6A15611158B29B0FF43A960 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 824D2C68D1F1206932E118C2 /* Pods_Runner.framework */; };
C94CC73C8037BAA385B7ED5C /* SharedPreferencesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FB5275C65B7F4CC7DD45DF /* SharedPreferencesPlugin.swift */; };
F85DEBB1878182EEE1DC542A /* ConnectivityPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936A41610C7269D52E5E51FF /* ConnectivityPlusPlugin.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -51,24 +44,19 @@
/* Begin PBXFileReference section */
04DD35536DEE7C27FFA53862 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
0C25F4F2367A30B47E945AD6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
0FF71CD2416844A1FC485D77 /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = "<group>"; };
300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = "<group>"; };
0C8E3486BF50440024D81B1E /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = "<group>"; };
217BA48DC884CB7ADD43CF88 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = "<group>"; };
35FB5275C65B7F4CC7DD45DF /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3C9575190150A93F97270FC0 /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = "<group>"; };
420881FB6A648A2AFD39FFF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathMonitorConnectivityProvider.swift; sourceTree = "<group>"; };
5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlusPlugin.swift; sourceTree = "<group>"; };
5F435CB4590A55958615A7AD /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ButtonMapping.swift; sourceTree = "<group>"; };
73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = "<source_root>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OsMediaControlsPlugin.swift; sourceTree = "<group>"; };
87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = "<group>"; };
8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GCControllerManager.swift; sourceTree = "<group>"; };
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
83C51CAD792E5C69AA4F971A /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = "<group>"; };
936A41610C7269D52E5E51FF /* ConnectivityPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityPlusPlugin.swift; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -76,14 +64,12 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
98ED539F08FAE8E8B964C7BE /* DeviceInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlusPlugin.swift; sourceTree = "<group>"; };
9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPlugin.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift; sourceTree = "<source_root>"; };
A12B8610AE5D580077264851 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCore.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerCore.swift; sourceTree = "<source_root>"; };
A2635E12EB9322B151EE5127 /* MpvPipController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPipController.swift; path = ../ios/Runner/MpvPlayer/MpvPipController.swift; sourceTree = "<source_root>"; };
C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityPlusPlugin.swift; sourceTree = "<group>"; };
AA12E2C6063C5799C38CAA95 /* PathMonitorConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathMonitorConnectivityProvider.swift; sourceTree = "<group>"; };
D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = "<source_root>"; };
D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadStreamHandler.swift; sourceTree = "<group>"; };
DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadPlugin.swift; sourceTree = "<group>"; };
E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -91,38 +77,33 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6A0510802F9B91CB0090B5FC /* MPVKit in Frameworks */,
C330A1087AA135CCFC0C6203 /* libPods-Runner.a in Frameworks */,
C6A15611158B29B0FF43A960 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0524C4A636652321237B0884 /* path_provider */ = {
0BB116AB9B63B9251A577795 /* connectivity_plus */ = {
isa = PBXGroup;
children = (
87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */,
936A41610C7269D52E5E51FF /* ConnectivityPlusPlugin.swift */,
3C9575190150A93F97270FC0 /* ConnectivityProvider.swift */,
AA12E2C6063C5799C38CAA95 /* PathMonitorConnectivityProvider.swift */,
);
path = path_provider;
name = connectivity_plus;
path = connectivity_plus;
sourceTree = "<group>";
};
08F5845B7875697970629696 /* package_info_plus */ = {
5167AAFF7134262A07EBBC7C /* package_info_plus */ = {
isa = PBXGroup;
children = (
300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */,
83C51CAD792E5C69AA4F971A /* PackageInfoPlusPlugin.swift */,
);
name = package_info_plus;
path = package_info_plus;
sourceTree = "<group>";
};
19DB3FBCF738894832DA07EF /* device_info_plus */ = {
isa = PBXGroup;
children = (
5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */,
);
path = device_info_plus;
sourceTree = "<group>";
};
53CDD29681161166962B9FA5 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -133,31 +114,34 @@
path = Pods;
sourceTree = "<group>";
};
5815557FAD15751414018354 /* shared_preferences_foundation */ = {
isa = PBXGroup;
children = (
E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */,
0FF71CD2416844A1FC485D77 /* messages.g.swift */,
);
path = shared_preferences_foundation;
sourceTree = "<group>";
};
5833EC5B503BBB4E370BA1B7 /* Frameworks */ = {
isa = PBXGroup;
children = (
5F435CB4590A55958615A7AD /* libPods-Runner.a */,
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
737C2C621E266B83D4011221 /* connectivity_plus */ = {
6C18DC1E07D29FCDF63F3159 /* Plugins */ = {
isa = PBXGroup;
children = (
C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */,
20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */,
4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */,
ACB880260B35BB030208DF38 /* shared_preferences_foundation */,
5167AAFF7134262A07EBBC7C /* package_info_plus */,
9DF350B3C8D3FFF46CF8FEF7 /* path_provider */,
7CEBB3A4438717E61CFCBA1E /* device_info_plus */,
0BB116AB9B63B9251A577795 /* connectivity_plus */,
);
path = connectivity_plus;
name = Plugins;
path = Plugins;
sourceTree = "<group>";
};
7CEBB3A4438717E61CFCBA1E /* device_info_plus */ = {
isa = PBXGroup;
children = (
98ED539F08FAE8E8B964C7BE /* DeviceInfoPlusPlugin.swift */,
);
name = device_info_plus;
path = device_info_plus;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
@@ -197,25 +181,31 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
BF0B7DFE378943E9D7865D18 /* MpvPlayer */,
E6BAF734A5201F83A1B61DCC /* Plugins */,
6C18DC1E07D29FCDF63F3159 /* Plugins */,
);
path = Runner;
sourceTree = "<group>";
};
AD5C822C9001042A47B99FBD /* universal_gamepad */ = {
9DF350B3C8D3FFF46CF8FEF7 /* path_provider */ = {
isa = PBXGroup;
children = (
DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */,
D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */,
8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */,
5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */,
217BA48DC884CB7ADD43CF88 /* PathProviderPlugin.swift */,
);
path = universal_gamepad;
name = path_provider;
path = path_provider;
sourceTree = "<group>";
};
ACB880260B35BB030208DF38 /* shared_preferences_foundation */ = {
isa = PBXGroup;
children = (
35FB5275C65B7F4CC7DD45DF /* SharedPreferencesPlugin.swift */,
0C8E3486BF50440024D81B1E /* messages.g.swift */,
);
name = shared_preferences_foundation;
path = shared_preferences_foundation;
sourceTree = "<group>";
};
BF0B7DFE378943E9D7865D18 /* MpvPlayer */ = {
@@ -231,28 +221,6 @@
path = Runner/MpvPlayer;
sourceTree = "<group>";
};
C538704E1F003452B22FE0C7 /* os_media_controls */ = {
isa = PBXGroup;
children = (
7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */,
);
path = os_media_controls;
sourceTree = "<group>";
};
E6BAF734A5201F83A1B61DCC /* Plugins */ = {
isa = PBXGroup;
children = (
5815557FAD15751414018354 /* shared_preferences_foundation */,
08F5845B7875697970629696 /* package_info_plus */,
0524C4A636652321237B0884 /* path_provider */,
AD5C822C9001042A47B99FBD /* universal_gamepad */,
19DB3FBCF738894832DA07EF /* device_info_plus */,
737C2C621E266B83D4011221 /* connectivity_plus */,
C538704E1F003452B22FE0C7 /* os_media_controls */,
);
path = Plugins;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -267,6 +235,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7C33FC680D67B3859BF9AE6C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -367,6 +336,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
7C33FC680D67B3859BF9AE6C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -389,25 +375,19 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
691577F0EB3F4EB1A5160280 /* MpvPlayerCoreBase.swift in Sources */,
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */,
6F3C0DD6F2F8DA14E7E2F386 /* MpvPlayerCore.swift in Sources */,
8E5EED3DDAC9455D4DAA9776 /* MpvPlayerPlugin.swift in Sources */,
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */,
A7ED1D92E7B15D3F7562F93E /* SharedPreferencesPlugin.swift in Sources */,
477FB0F520224870902B9FD7 /* messages.g.swift in Sources */,
F36688C4E8DF44BC6BDD99A5 /* PackageInfoPlusPlugin.swift in Sources */,
30E568153D56A59EA5EADCBB /* PathProviderPlugin.swift in Sources */,
32F0056AF154FC4EA76CBEFC /* GamepadPlugin.swift in Sources */,
12E72ED3EFA35A8715DE2A52 /* GamepadStreamHandler.swift in Sources */,
4A6DE7BDEF20A6242A3C6365 /* GCControllerManager.swift in Sources */,
BF599A1C25382688DD7F4505 /* ButtonMapping.swift in Sources */,
B494441CBB8FB05D04F55DD9 /* DeviceInfoPlusPlugin.swift in Sources */,
5CDBEB7E3FE968FC131E964A /* ConnectivityPlusPlugin.swift in Sources */,
0D69E2DEA6EA0538722BC7F0 /* ConnectivityProvider.swift in Sources */,
C1E0D7252DE079EDFA234F90 /* PathMonitorConnectivityProvider.swift in Sources */,
5689B077C6EA57D53BBE8D0D /* OsMediaControlsPlugin.swift in Sources */,
C94CC73C8037BAA385B7ED5C /* SharedPreferencesPlugin.swift in Sources */,
0A4E3554C5AD1373495F5434 /* messages.g.swift in Sources */,
24753D6DD9098E97989137A6 /* PackageInfoPlusPlugin.swift in Sources */,
937279397427A0E8ECC70A3A /* PathProviderPlugin.swift in Sources */,
31477C4FBA7BF70CAFCDC566 /* DeviceInfoPlusPlugin.swift in Sources */,
F85DEBB1878182EEE1DC542A /* ConnectivityPlusPlugin.swift in Sources */,
AFCEB532D567638EF6707333 /* ConnectivityProvider.swift in Sources */,
4E9365FA67E94D8A289FE862 /* PathMonitorConnectivityProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+6 -23
View File
@@ -1,6 +1,8 @@
import Flutter
import UIKit
import AVFoundation
import universal_gamepad
import os_media_controls
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -18,47 +20,28 @@ import AVFoundation
application.beginReceivingRemoteControlEvents()
NSLog("[tvos] AppDelegate didFinishLaunching — registering plugins")
if let spRegistrar = self.registrar(forPlugin: "SharedPreferencesPlugin") {
NSLog("[tvos] Registering SharedPreferencesPlugin (Swift-direct)")
SharedPreferencesPlugin.register(with: spRegistrar)
} else {
NSLog("[tvos] SharedPreferencesPlugin registrar nil")
if let r = self.registrar(forPlugin: "SharedPreferencesPlugin") {
SharedPreferencesPlugin.register(with: r)
}
if let mpvRegistrar = self.registrar(forPlugin: "MpvPlayerPlugin") {
NSLog("[tvos] Registering MpvPlayerPlugin")
MpvPlayerPlugin.register(with: mpvRegistrar)
if let r = self.registrar(forPlugin: "MpvPlayerPlugin") {
MpvPlayerPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "PackageInfoPlusPlugin") {
NSLog("[tvos] Registering PackageInfoPlusPlugin")
PackageInfoPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "PathProviderPlugin") {
NSLog("[tvos] Registering PathProviderPlugin")
PathProviderPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "GamepadPlugin") {
NSLog("[tvos] Registering GamepadPlugin")
GamepadPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "DeviceInfoPlusPlugin") {
NSLog("[tvos] Registering DeviceInfoPlusPlugin")
DeviceInfoPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "ConnectivityPlusPlugin") {
NSLog("[tvos] Registering ConnectivityPlusPlugin")
ConnectivityPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "OsMediaControlsPlugin") {
NSLog("[tvos] Registering OsMediaControlsPlugin")
OsMediaControlsPlugin.register(with: r)
}
@@ -1,370 +0,0 @@
import Flutter
import UIKit
import MediaPlayer
import AVFoundation
public class OsMediaControlsPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private let nowPlayingCenter = MPNowPlayingInfoCenter.default()
private let commandCenter = MPRemoteCommandCenter.shared()
private var currentMetadata: [String: Any] = [:]
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(
name: "com.edde746.os_media_controls/methods",
binaryMessenger: registrar.messenger()
)
let eventChannel = FlutterEventChannel(
name: "com.edde746.os_media_controls/events",
binaryMessenger: registrar.messenger()
)
let instance = OsMediaControlsPlugin()
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
}
public override init() {
super.init()
// Ensure app receives remote control events for Now Playing controls
DispatchQueue.main.async {
UIApplication.shared.beginReceivingRemoteControlEvents()
}
setupRemoteCommandCenter()
}
private func setupRemoteCommandCenter() {
// Play command
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "play"])
return .success
}
// Pause command
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "pause"])
return .success
}
// Toggle play/pause command
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "togglePlayPause"])
return .success
}
// Next track command
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "next"])
return .success
}
// Previous track command
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "previous"])
return .success
}
// Change playback position command (seek)
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent {
self?.sendEvent([
"type": "seek",
"position": positionEvent.positionTime
])
}
return .success
}
// Skip forward command
commandCenter.skipForwardCommand.isEnabled = false // Disabled by default
commandCenter.skipForwardCommand.addTarget { [weak self] event in
if let skipEvent = event as? MPSkipIntervalCommandEvent {
self?.sendEvent([
"type": "skipForward",
"interval": skipEvent.interval
])
} else {
self?.sendEvent(["type": "skipForward"])
}
return .success
}
// Skip backward command
commandCenter.skipBackwardCommand.isEnabled = false // Disabled by default
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
if let skipEvent = event as? MPSkipIntervalCommandEvent {
self?.sendEvent([
"type": "skipBackward",
"interval": skipEvent.interval
])
} else {
self?.sendEvent(["type": "skipBackward"])
}
return .success
}
// Change playback rate command
commandCenter.changePlaybackRateCommand.isEnabled = true
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 1.0, 1.5, 2.0]
commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in
if let rateEvent = event as? MPChangePlaybackRateCommandEvent {
self?.sendEvent([
"type": "setSpeed",
"speed": rateEvent.playbackRate
])
}
return .success
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "setMetadata":
setMetadata(arguments: call.arguments as? [String: Any])
result(nil)
case "setPlaybackState":
setPlaybackState(arguments: call.arguments as? [String: Any])
result(nil)
case "enableControls":
enableControls(arguments: call.arguments as? [String])
result(nil)
case "disableControls":
disableControls(arguments: call.arguments as? [String])
result(nil)
case "setSkipIntervals":
setSkipIntervals(arguments: call.arguments as? [String: Any])
result(nil)
case "setQueueInfo":
setQueueInfo(arguments: call.arguments as? [String: Any])
result(nil)
case "clear":
clear()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
private func setMetadata(arguments: [String: Any]?) {
guard let args = arguments else { return }
// Reactivate audio session to ensure media controls work after being cleared
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to reactivate audio session: \(error)")
}
// Store metadata for later use
for (key, value) in args {
if key != "artwork" {
currentMetadata[key] = value
}
}
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
if let title = args["title"] as? String {
nowPlayingInfo[MPMediaItemPropertyTitle] = title
}
if let artist = args["artist"] as? String {
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
}
if let album = args["album"] as? String {
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
}
if let albumArtist = args["albumArtist"] as? String {
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = albumArtist
}
if let duration = args["duration"] as? Double {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
}
// Handle artwork from bytes (takes precedence over URL)
if let artworkData = args["artwork"] as? FlutterStandardTypedData {
if let image = UIImage(data: artworkData.data) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
} else if let artworkUrlString = args["artworkUrl"] as? String, let artworkUrl = URL(string: artworkUrlString) {
// Handle artwork from URL - download asynchronously
URLSession.shared.dataTask(with: artworkUrl) { [weak self] data, response, error in
guard let self = self else { return }
guard error == nil, let data = data, let image = UIImage(data: data) else {
return
}
DispatchQueue.main.async {
var updatedInfo = self.nowPlayingCenter.nowPlayingInfo ?? [:]
updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
self.nowPlayingCenter.nowPlayingInfo = updatedInfo
}
}.resume()
}
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func setPlaybackState(arguments: [String: Any]?) {
guard let args = arguments,
let stateString = args["state"] as? String,
let position = args["position"] as? Double,
let speed = args["speed"] as? Double else { return }
// Reactivate audio session to ensure media controls work after being cleared
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to reactivate audio session: \(error)")
}
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = position
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] =
stateString == "playing" ? speed : 0.0
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func enableControls(arguments: [String]?) {
guard let controls = arguments else { return }
for control in controls {
switch control {
case "play":
commandCenter.playCommand.isEnabled = true
case "pause":
commandCenter.pauseCommand.isEnabled = true
case "stop":
// iOS doesn't have a dedicated stop command
break
case "next":
commandCenter.nextTrackCommand.isEnabled = true
case "previous":
commandCenter.previousTrackCommand.isEnabled = true
case "seek":
commandCenter.changePlaybackPositionCommand.isEnabled = true
case "skipForward":
commandCenter.skipForwardCommand.isEnabled = true
case "skipBackward":
commandCenter.skipBackwardCommand.isEnabled = true
case "changeSpeed":
commandCenter.changePlaybackRateCommand.isEnabled = true
default:
break
}
}
}
private func disableControls(arguments: [String]?) {
guard let controls = arguments else { return }
for control in controls {
switch control {
case "play":
commandCenter.playCommand.isEnabled = false
case "pause":
commandCenter.pauseCommand.isEnabled = false
case "next":
commandCenter.nextTrackCommand.isEnabled = false
case "previous":
commandCenter.previousTrackCommand.isEnabled = false
case "seek":
commandCenter.changePlaybackPositionCommand.isEnabled = false
case "skipForward":
commandCenter.skipForwardCommand.isEnabled = false
case "skipBackward":
commandCenter.skipBackwardCommand.isEnabled = false
case "changeSpeed":
commandCenter.changePlaybackRateCommand.isEnabled = false
default:
break
}
}
}
private func setSkipIntervals(arguments: [String: Any]?) {
guard let args = arguments else { return }
if let forward = args["forward"] as? Int {
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: forward)]
}
if let backward = args["backward"] as? Int {
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: backward)]
}
}
private func setQueueInfo(arguments: [String: Any]?) {
guard let args = arguments,
let currentIndex = args["currentIndex"] as? Int,
let queueLength = args["queueLength"] as? Int else { return }
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueIndex] = currentIndex
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueCount] = queueLength
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func clear() {
nowPlayingCenter.nowPlayingInfo = nil
currentMetadata.removeAll()
// Deactivate audio session to force iOS to remove controls from Control Center
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
// Audio session deactivation failed, but continue with cleanup
}
// Disable all command center buttons
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = false
commandCenter.pauseCommand.isEnabled = false
commandCenter.togglePlayPauseCommand.isEnabled = false
commandCenter.nextTrackCommand.isEnabled = false
commandCenter.previousTrackCommand.isEnabled = false
commandCenter.changePlaybackPositionCommand.isEnabled = false
commandCenter.skipForwardCommand.isEnabled = false
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.changePlaybackRateCommand.isEnabled = false
}
private func sendEvent(_ event: [String: Any]) {
eventSink?(event)
}
// MARK: - FlutterStreamHandler
public func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
@@ -1,148 +0,0 @@
import GameController
/// Maps GCController elements to W3C Standard Gamepad indices.
///
/// W3C Standard Gamepad button mapping:
/// 0 = A (South) 8 = Back/View
/// 1 = B (East) 9 = Start/Menu
/// 2 = X (West) 10 = Left Stick Button
/// 3 = Y (North) 11 = Right Stick Button
/// 4 = Left Shoulder 12 = D-pad Up
/// 5 = Right Shoulder 13 = D-pad Down
/// 6 = Left Trigger 14 = D-pad Left
/// 7 = Right Trigger 15 = D-pad Right
/// 16 = Guide/Home
///
/// W3C Standard Gamepad axis mapping:
/// 0 = Left Stick X (-1 left, +1 right)
/// 1 = Left Stick Y (-1 up, +1 down)
/// 2 = Right Stick X (-1 left, +1 right)
/// 3 = Right Stick Y (-1 up, +1 down)
///
/// Note: GCController reports Y-axis with up = +1, which is the opposite of
/// the W3C convention (up = -1). The axis mapping inverts Y values.
enum ButtonMapping {
/// Returns the W3C button index and the corresponding `GCControllerButtonInput`
/// if `element` matches a known button in `gamepad`.
static func buttonIndex(for element: GCControllerElement,
in gamepad: GCExtendedGamepad) -> (Int, GCControllerButtonInput)? {
// Face buttons
if element == gamepad.buttonA {
return (0, gamepad.buttonA)
}
if element == gamepad.buttonB {
return (1, gamepad.buttonB)
}
if element == gamepad.buttonX {
return (2, gamepad.buttonX)
}
if element == gamepad.buttonY {
return (3, gamepad.buttonY)
}
// Shoulders
if element == gamepad.leftShoulder {
return (4, gamepad.leftShoulder)
}
if element == gamepad.rightShoulder {
return (5, gamepad.rightShoulder)
}
// Triggers
if element == gamepad.leftTrigger {
return (6, gamepad.leftTrigger)
}
if element == gamepad.rightTrigger {
return (7, gamepad.rightTrigger)
}
// Menu buttons
if let buttonOptions = gamepad.buttonOptions, element == buttonOptions {
return (8, buttonOptions)
}
if element == gamepad.buttonMenu {
return (9, gamepad.buttonMenu)
}
// Thumbstick buttons
if let leftThumbstickButton = gamepad.leftThumbstickButton, element == leftThumbstickButton {
return (10, leftThumbstickButton)
}
if let rightThumbstickButton = gamepad.rightThumbstickButton, element == rightThumbstickButton {
return (11, rightThumbstickButton)
}
// D-pad individual buttons
if element == gamepad.dpad.up {
return (12, gamepad.dpad.up)
}
if element == gamepad.dpad.down {
return (13, gamepad.dpad.down)
}
if element == gamepad.dpad.left {
return (14, gamepad.dpad.left)
}
if element == gamepad.dpad.right {
return (15, gamepad.dpad.right)
}
// Guide / Home button (added in tvOS/iOS 14).
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) {
if let buttonHome = gamepad.buttonHome, element == buttonHome {
return (16, buttonHome)
}
}
return nil
}
/// Returns all W3C axis events for the given `element`.
///
/// The `valueChangedHandler` on `GCExtendedGamepad` may fire with the
/// entire `GCControllerDirectionPad` (thumbstick) as the element, or with
/// an individual axis sub-element. When the full thumbstick fires we
/// return both X and Y axis entries. When a single axis fires we return
/// one entry.
///
/// Y-axis values are inverted: GCController uses +1 for up, but the W3C
/// standard uses -1 for up.
static func axisIndices(for element: GCControllerElement,
in gamepad: GCExtendedGamepad) -> [(Int, Double)] {
// Left stick (full thumbstick element)
if element == gamepad.leftThumbstick {
return [
(0, Double(gamepad.leftThumbstick.xAxis.value)),
(1, Double(-gamepad.leftThumbstick.yAxis.value)),
]
}
// Left stick individual axes
if element == gamepad.leftThumbstick.xAxis {
return [(0, Double(gamepad.leftThumbstick.xAxis.value))]
}
if element == gamepad.leftThumbstick.yAxis {
// Invert Y: GC up is +1, W3C up is -1
return [(1, Double(-gamepad.leftThumbstick.yAxis.value))]
}
// Right stick (full thumbstick element)
if element == gamepad.rightThumbstick {
return [
(2, Double(gamepad.rightThumbstick.xAxis.value)),
(3, Double(-gamepad.rightThumbstick.yAxis.value)),
]
}
// Right stick individual axes
if element == gamepad.rightThumbstick.xAxis {
return [(2, Double(gamepad.rightThumbstick.xAxis.value))]
}
if element == gamepad.rightThumbstick.yAxis {
// Invert Y: GC up is +1, W3C up is -1
return [(3, Double(-gamepad.rightThumbstick.yAxis.value))]
}
return []
}
}
@@ -1,244 +0,0 @@
import GameController
/// Manages GCController connections and input, translating events into the
/// compact wire-format arrays expected by the Dart side.
class GCControllerManager {
// MARK: - Properties
private let streamHandler: GamepadStreamHandler
/// Maps a GCController instance to its stable int gamepad ID.
private var controllerIds: [ObjectIdentifier: Int] = [:]
/// Counter for generating unique IDs.
private var nextId: Int = 0
/// Notification observers so we can remove them on dispose.
private var connectObserver: NSObjectProtocol?
private var disconnectObserver: NSObjectProtocol?
// MARK: - Init
init(streamHandler: GamepadStreamHandler) {
self.streamHandler = streamHandler
}
// MARK: - Lifecycle
/// Begin listening for GCController connect/disconnect notifications and
/// register input handlers on any controllers that are already connected.
func startObserving() {
connectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { [weak self] notification in
guard let self = self,
let controller = notification.object as? GCController else { return }
self.controllerDidConnect(controller)
}
disconnectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main
) { [weak self] notification in
guard let self = self,
let controller = notification.object as? GCController else { return }
self.controllerDidDisconnect(controller)
}
// Handle controllers that were already connected before we started
// observing (e.g. wired controllers).
for controller in GCController.controllers() {
controllerDidConnect(controller)
}
}
/// Remove all observers and clear input handlers.
func stopObserving() {
if let observer = connectObserver {
NotificationCenter.default.removeObserver(observer)
connectObserver = nil
}
if let observer = disconnectObserver {
NotificationCenter.default.removeObserver(observer)
disconnectObserver = nil
}
// Remove value changed handlers from all tracked controllers.
for controller in GCController.controllers() {
controller.extendedGamepad?.valueChangedHandler = nil
}
controllerIds.removeAll()
}
// MARK: - listGamepads
/// Returns an array of dictionaries describing currently connected
/// controllers, suitable for returning over a MethodChannel.
func listGamepads() -> [[String: Any]] {
var result: [[String: Any]] = []
for controller in GCController.controllers() {
let key = ObjectIdentifier(controller)
guard let gamepadId = controllerIds[key] else { continue }
var info: [String: Any] = [
"id": gamepadId,
"name": controllerName(controller),
]
if let vendorId = vendorId(for: controller) {
info["vendorId"] = vendorId
}
if let productId = productId(for: controller) {
info["productId"] = productId
}
result.append(info)
}
return result
}
// MARK: - Connection events
private func controllerDidConnect(_ controller: GCController) {
let gamepadId = assignId(for: controller)
// Register input handlers for the extended gamepad profile.
registerInputHandlers(for: controller, gamepadId: gamepadId)
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
let event: [Any] = [
0,
gamepadId,
currentTimestamp(),
true,
controllerName(controller),
vendorId(for: controller) as Any,
productId(for: controller) as Any,
]
streamHandler.send(event: event)
}
private func controllerDidDisconnect(_ controller: GCController) {
let key = ObjectIdentifier(controller)
guard let gamepadId = controllerIds.removeValue(forKey: key) else { return }
controller.extendedGamepad?.valueChangedHandler = nil
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
let event: [Any] = [
0,
gamepadId,
currentTimestamp(),
false,
controllerName(controller),
vendorId(for: controller) as Any,
productId(for: controller) as Any,
]
streamHandler.send(event: event)
}
// MARK: - Input handlers
private func registerInputHandlers(for controller: GCController, gamepadId: Int) {
guard let extendedGamepad = controller.extendedGamepad else { return }
extendedGamepad.valueChangedHandler = { [weak self] (gamepad, element) in
guard let self = self else { return }
self.handleValueChanged(gamepad: gamepad, element: element, gamepadId: gamepadId)
}
}
private func handleValueChanged(gamepad: GCExtendedGamepad,
element: GCControllerElement,
gamepadId: Int) {
let timestamp = currentTimestamp()
// D-pad fires as a single GCControllerDirectionPad element, not as
// individual direction buttons. Expand it into 4 button events.
if element === gamepad.dpad {
sendButtonEvent(gamepadId: gamepadId, index: 12,
pressed: gamepad.dpad.up.isPressed,
value: Double(gamepad.dpad.up.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 13,
pressed: gamepad.dpad.down.isPressed,
value: Double(gamepad.dpad.down.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 14,
pressed: gamepad.dpad.left.isPressed,
value: Double(gamepad.dpad.left.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 15,
pressed: gamepad.dpad.right.isPressed,
value: Double(gamepad.dpad.right.value), timestamp: timestamp)
return
}
// Check buttons.
if let (index, button) = ButtonMapping.buttonIndex(for: element, in: gamepad) {
sendButtonEvent(gamepadId: gamepadId, index: index,
pressed: button.isPressed,
value: Double(button.value), timestamp: timestamp)
return
}
// Check axes (may produce multiple events when a full thumbstick fires).
let axisEvents = ButtonMapping.axisIndices(for: element, in: gamepad)
for (index, axisValue) in axisEvents {
// Wire format: [2, gamepadId, timestamp, axisIndex, value]
streamHandler.send(event: [2, gamepadId, timestamp, index, axisValue])
}
}
private func sendButtonEvent(gamepadId: Int, index: Int,
pressed: Bool, value: Double, timestamp: Int) {
// Wire format: [1, gamepadId, timestamp, buttonIndex, pressed, value]
streamHandler.send(event: [1, gamepadId, timestamp, index, pressed, value])
}
// MARK: - Helpers
private func assignId(for controller: GCController) -> Int {
let key = ObjectIdentifier(controller)
if let existing = controllerIds[key] {
return existing
}
let id = nextId
nextId += 1
controllerIds[key] = id
return id
}
private func controllerName(_ controller: GCController) -> String {
let category = controller.productCategory
if !category.isEmpty {
return category
}
return controller.vendorName ?? "Unknown Controller"
}
/// Returns the USB vendor ID for the controller, if available.
///
/// GCController does not directly expose numeric vendor/product IDs
/// through the public API. We attempt to obtain them from the
/// underlying physical device description on iOS 16+.
private func vendorId(for controller: GCController) -> Int? {
// GCController does not publicly expose numeric vendor IDs on iOS.
// Return nil; the Dart side treats this as optional.
return nil
}
/// Returns the USB product ID for the controller, if available.
private func productId(for controller: GCController) -> Int? {
// GCController does not publicly expose numeric product IDs on iOS.
// Return nil; the Dart side treats this as optional.
return nil
}
/// Returns the current timestamp in milliseconds since epoch.
private func currentTimestamp() -> Int {
return Int(Date().timeIntervalSince1970 * 1000)
}
}
@@ -1,61 +0,0 @@
import Flutter
import UIKit
/// Entry point for the gamepad iOS plugin.
///
/// Registers a MethodChannel (`dev.universal_gamepad/methods`) and an
/// EventChannel (`dev.universal_gamepad/events`) with the Flutter engine.
public class GamepadPlugin: NSObject, FlutterPlugin {
private let controllerManager: GCControllerManager
private let streamHandler: GamepadStreamHandler
init(controllerManager: GCControllerManager, streamHandler: GamepadStreamHandler) {
self.controllerManager = controllerManager
self.streamHandler = streamHandler
super.init()
}
// MARK: - FlutterPlugin
public static func register(with registrar: FlutterPluginRegistrar) {
let streamHandler = GamepadStreamHandler()
let controllerManager = GCControllerManager(streamHandler: streamHandler)
let methodChannel = FlutterMethodChannel(
name: "dev.universal_gamepad/methods",
binaryMessenger: registrar.messenger()
)
let eventChannel = FlutterEventChannel(
name: "dev.universal_gamepad/events",
binaryMessenger: registrar.messenger()
)
let instance = GamepadPlugin(
controllerManager: controllerManager,
streamHandler: streamHandler
)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(streamHandler)
// Start observing controllers immediately so we capture connections
// that happen before the Dart side listens.
controllerManager.startObserving()
}
// MARK: - MethodChannel
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "listGamepads":
result(controllerManager.listGamepads())
case "dispose":
controllerManager.stopObserving()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}
@@ -1,62 +0,0 @@
import Flutter
/// FlutterStreamHandler that forwards native gamepad events to Dart.
///
/// Events are queued if no sink is attached yet; once a listener subscribes
/// all queued events are flushed immediately.
public class GamepadStreamHandler: NSObject, FlutterStreamHandler {
/// The active event sink provided by Flutter's EventChannel.
private var eventSink: FlutterEventSink?
/// Events received before the Dart side starts listening.
private var pendingEvents: [[Any]] = []
/// Thread-safety lock for sink / queue access.
private let lock = NSLock()
// MARK: - FlutterStreamHandler
public func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
lock.lock()
eventSink = events
// Flush any events that arrived before the listener was attached.
for event in pendingEvents {
events(event)
}
pendingEvents.removeAll()
lock.unlock()
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
lock.lock()
eventSink = nil
lock.unlock()
return nil
}
// MARK: - Internal API
/// Sends a gamepad event array to Dart.
///
/// If the sink is not yet available the event is queued.
func send(event: [Any]) {
lock.lock()
if let sink = eventSink {
lock.unlock()
// Dispatch on the main thread to satisfy Flutter platform channel
// requirements.
DispatchQueue.main.async {
sink(event)
}
} else {
pendingEvents.append(event)
lock.unlock()
}
}
}
-1
View File
@@ -1 +0,0 @@
#import "GeneratedPluginRegistrant.h"
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Runs `pod install` for the tvOS target with UTF-8 locale forced — CocoaPods
# 1.16 on Ruby 4.0 crashes with Encoding::CompatibilityError when LANG/LC_ALL
# are unset or non-UTF-8.
set -euo pipefail
TVOS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$TVOS_DIR"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
exec pod install "$@"
+3 -14
View File
@@ -16,15 +16,10 @@ PLUGIN_BASENAMES = %w[
messages.g.swift
PackageInfoPlusPlugin.swift
PathProviderPlugin.swift
GamepadPlugin.swift
GamepadStreamHandler.swift
GCControllerManager.swift
ButtonMapping.swift
DeviceInfoPlusPlugin.swift
ConnectivityPlusPlugin.swift
ConnectivityProvider.swift
PathMonitorConnectivityProvider.swift
OsMediaControlsPlugin.swift
]
# Remove any existing file refs for these basenames from the entire project.
@@ -60,12 +55,6 @@ plugins = {
'path_provider' => %w[
PathProviderPlugin.swift
],
'universal_gamepad' => %w[
GamepadPlugin.swift
GamepadStreamHandler.swift
GCControllerManager.swift
ButtonMapping.swift
],
'device_info_plus' => %w[
DeviceInfoPlusPlugin.swift
],
@@ -74,9 +63,9 @@ plugins = {
ConnectivityProvider.swift
PathMonitorConnectivityProvider.swift
],
'os_media_controls' => %w[
OsMediaControlsPlugin.swift
],
# universal_gamepad + os_media_controls come from CocoaPods (tvos/Podfile)
# — their upstream podspecs declare tvOS support, so no embedded copy
# is needed.
}
sources_phase = runner.source_build_phase