diff --git a/pubspec.lock b/pubspec.lock index f519abc6..e9ca8079 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index bbd739b0..4c42758e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/tvos/.gitignore b/tvos/.gitignore index 6e658ed7..50253c2b 100644 --- a/tvos/.gitignore +++ b/tvos/.gitignore @@ -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 diff --git a/tvos/Flutter/Flutter.podspec b/tvos/Flutter/Flutter.podspec new file mode 100644 index 00000000..399de69c --- /dev/null +++ b/tvos/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 diff --git a/tvos/Podfile b/tvos/Podfile index 92552ef6..c8628034 100644 --- a/tvos/Podfile +++ b/tvos/Podfile @@ -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 diff --git a/tvos/Runner.xcodeproj/project.pbxproj b/tvos/Runner.xcodeproj/project.pbxproj index 084b8b82..c51474d4 100644 --- a/tvos/Runner.xcodeproj/project.pbxproj +++ b/tvos/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; - 0FF71CD2416844A1FC485D77 /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = ""; }; - 300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = ""; }; + 0C8E3486BF50440024D81B1E /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = ""; }; + 217BA48DC884CB7ADD43CF88 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = ""; }; + 35FB5275C65B7F4CC7DD45DF /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3C9575190150A93F97270FC0 /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = ""; }; 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 = ""; }; - 4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathMonitorConnectivityProvider.swift; sourceTree = ""; }; - 5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlusPlugin.swift; sourceTree = ""; }; - 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 = ""; }; 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OsMediaControlsPlugin.swift; sourceTree = ""; }; - 87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = ""; }; - 8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GCControllerManager.swift; sourceTree = ""; }; + 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 = ""; }; + 936A41610C7269D52E5E51FF /* ConnectivityPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityPlusPlugin.swift; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 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 = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 98ED539F08FAE8E8B964C7BE /* DeviceInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlusPlugin.swift; sourceTree = ""; }; 9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPlugin.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift; sourceTree = ""; }; A12B8610AE5D580077264851 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCore.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerCore.swift; sourceTree = ""; }; A2635E12EB9322B151EE5127 /* MpvPipController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPipController.swift; path = ../ios/Runner/MpvPlayer/MpvPipController.swift; sourceTree = ""; }; - C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityPlusPlugin.swift; sourceTree = ""; }; + AA12E2C6063C5799C38CAA95 /* PathMonitorConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathMonitorConnectivityProvider.swift; sourceTree = ""; }; D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = ""; }; - D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadStreamHandler.swift; sourceTree = ""; }; - DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadPlugin.swift; sourceTree = ""; }; - E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = ""; }; /* 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 = ""; }; - 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 = ""; }; - 19DB3FBCF738894832DA07EF /* device_info_plus */ = { - isa = PBXGroup; - children = ( - 5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */, - ); - path = device_info_plus; - sourceTree = ""; - }; 53CDD29681161166962B9FA5 /* Pods */ = { isa = PBXGroup; children = ( @@ -133,31 +114,34 @@ path = Pods; sourceTree = ""; }; - 5815557FAD15751414018354 /* shared_preferences_foundation */ = { - isa = PBXGroup; - children = ( - E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */, - 0FF71CD2416844A1FC485D77 /* messages.g.swift */, - ); - path = shared_preferences_foundation; - sourceTree = ""; - }; 5833EC5B503BBB4E370BA1B7 /* Frameworks */ = { isa = PBXGroup; children = ( - 5F435CB4590A55958615A7AD /* libPods-Runner.a */, + 824D2C68D1F1206932E118C2 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; - 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 = ""; + }; + 7CEBB3A4438717E61CFCBA1E /* device_info_plus */ = { + isa = PBXGroup; + children = ( + 98ED539F08FAE8E8B964C7BE /* DeviceInfoPlusPlugin.swift */, + ); + name = device_info_plus; + path = device_info_plus; sourceTree = ""; }; 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 = ""; }; - 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 = ""; + }; + ACB880260B35BB030208DF38 /* shared_preferences_foundation */ = { + isa = PBXGroup; + children = ( + 35FB5275C65B7F4CC7DD45DF /* SharedPreferencesPlugin.swift */, + 0C8E3486BF50440024D81B1E /* messages.g.swift */, + ); + name = shared_preferences_foundation; + path = shared_preferences_foundation; sourceTree = ""; }; BF0B7DFE378943E9D7865D18 /* MpvPlayer */ = { @@ -231,28 +221,6 @@ path = Runner/MpvPlayer; sourceTree = ""; }; - C538704E1F003452B22FE0C7 /* os_media_controls */ = { - isa = PBXGroup; - children = ( - 7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */, - ); - path = os_media_controls; - sourceTree = ""; - }; - 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 = ""; - }; /* 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; }; diff --git a/tvos/Runner/AppDelegate.swift b/tvos/Runner/AppDelegate.swift index ce11c546..7ab44e7e 100644 --- a/tvos/Runner/AppDelegate.swift +++ b/tvos/Runner/AppDelegate.swift @@ -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) } diff --git a/tvos/Runner/Plugins/os_media_controls/OsMediaControlsPlugin.swift b/tvos/Runner/Plugins/os_media_controls/OsMediaControlsPlugin.swift deleted file mode 100644 index d3c3c087..00000000 --- a/tvos/Runner/Plugins/os_media_controls/OsMediaControlsPlugin.swift +++ /dev/null @@ -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 - } -} diff --git a/tvos/Runner/Plugins/universal_gamepad/ButtonMapping.swift b/tvos/Runner/Plugins/universal_gamepad/ButtonMapping.swift deleted file mode 100644 index 1667bfb2..00000000 --- a/tvos/Runner/Plugins/universal_gamepad/ButtonMapping.swift +++ /dev/null @@ -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 [] - } -} diff --git a/tvos/Runner/Plugins/universal_gamepad/GCControllerManager.swift b/tvos/Runner/Plugins/universal_gamepad/GCControllerManager.swift deleted file mode 100644 index eef68820..00000000 --- a/tvos/Runner/Plugins/universal_gamepad/GCControllerManager.swift +++ /dev/null @@ -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) - } -} diff --git a/tvos/Runner/Plugins/universal_gamepad/GamepadPlugin.swift b/tvos/Runner/Plugins/universal_gamepad/GamepadPlugin.swift deleted file mode 100644 index 115d879d..00000000 --- a/tvos/Runner/Plugins/universal_gamepad/GamepadPlugin.swift +++ /dev/null @@ -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) - } - } -} diff --git a/tvos/Runner/Plugins/universal_gamepad/GamepadStreamHandler.swift b/tvos/Runner/Plugins/universal_gamepad/GamepadStreamHandler.swift deleted file mode 100644 index 60e4b930..00000000 --- a/tvos/Runner/Plugins/universal_gamepad/GamepadStreamHandler.swift +++ /dev/null @@ -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() - } - } -} diff --git a/tvos/Runner/Runner-Bridging-Header.h b/tvos/Runner/Runner-Bridging-Header.h index 308a2a56..e69de29b 100644 --- a/tvos/Runner/Runner-Bridging-Header.h +++ b/tvos/Runner/Runner-Bridging-Header.h @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/tvos/scripts/pod_install.sh b/tvos/scripts/pod_install.sh new file mode 100755 index 00000000..778fb999 --- /dev/null +++ b/tvos/scripts/pod_install.sh @@ -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 "$@" diff --git a/tvos/scripts/wire_plugins.rb b/tvos/scripts/wire_plugins.rb index 50d614c0..589934d4 100755 --- a/tvos/scripts/wire_plugins.rb +++ b/tvos/scripts/wire_plugins.rb @@ -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