refactor(tvos): cleanup
This commit is contained in:
+4
-4
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
|
||||
Executable
+14
@@ -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 "$@"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user